diff --git a/src/__swaps__/screens/Swap/components/CoinRowButton.tsx b/src/__swaps__/screens/Swap/components/CoinRowButton.tsx index 5c486b7842b..d39aea4437b 100644 --- a/src/__swaps__/screens/Swap/components/CoinRowButton.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRowButton.tsx @@ -5,7 +5,7 @@ import { Box, TextIcon, useColorMode, useForegroundColor } from '@/design-system import { TextWeight } from '@/design-system/components/Text/Text'; import { TextSize } from '@/design-system/typography/typeHierarchy'; import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR, THICK_BORDER_WIDTH } from '../constants'; -import { opacity } from '../utils'; +import { opacity } from '../utils/swaps'; export const CoinRowButton = ({ icon, diff --git a/src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx b/src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx index 3e9308e2c11..5c595bd6ab1 100644 --- a/src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx +++ b/src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Box, Inline, Text, TextIcon, useColorMode, useForegroundColor } from '@/design-system'; import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR, THICK_BORDER_WIDTH } from '../constants'; -import { opacity } from '../utils'; +import { opacity } from '../utils/swaps'; import { ButtonPressAnimation } from '@/components/animations'; import Animated from 'react-native-reanimated'; import { useSwapContext } from '../providers/swap-provider'; diff --git a/src/__swaps__/screens/Swap/components/FlipButton.tsx b/src/__swaps__/screens/Swap/components/FlipButton.tsx index 80ca54167c6..a9408ddbb86 100644 --- a/src/__swaps__/screens/Swap/components/FlipButton.tsx +++ b/src/__swaps__/screens/Swap/components/FlipButton.tsx @@ -8,7 +8,7 @@ import { AnimatedSpinner, spinnerExitConfig } from '@/__swaps__/components/anima import { Bleed, Box, IconContainer, Text, globalColors, useColorMode } from '@/design-system'; import { colors } from '@/styles'; import { SEPARATOR_COLOR, THICK_BORDER_WIDTH } from '../constants'; -import { opacity } from '../utils'; +import { opacity } from '../utils/swaps'; import { IS_ANDROID, IS_IOS } from '@/env'; import { AnimatedBlurView } from './AnimatedBlurView'; import { useSwapContext } from '../providers/swap-provider'; diff --git a/src/__swaps__/screens/Swap/components/SearchInput.tsx b/src/__swaps__/screens/Swap/components/SearchInput.tsx index 6cd4bcde738..6ad9dc704b5 100644 --- a/src/__swaps__/screens/Swap/components/SearchInput.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInput.tsx @@ -4,7 +4,7 @@ import { ButtonPressAnimation } from '@/components/animations'; import { Input } from '@/components/inputs'; import { Bleed, Box, Column, Columns, Text, useColorMode, useForegroundColor } from '@/design-system'; import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR, THICK_BORDER_WIDTH } from '../constants'; -import { opacity } from '../utils'; +import { opacity } from '../utils/swaps'; export const SearchInput = ({ color, diff --git a/src/__swaps__/screens/Swap/components/SwapBackground.tsx b/src/__swaps__/screens/Swap/components/SwapBackground.tsx index 48116215e14..fcf029a7331 100644 --- a/src/__swaps__/screens/Swap/components/SwapBackground.tsx +++ b/src/__swaps__/screens/Swap/components/SwapBackground.tsx @@ -3,7 +3,7 @@ import { useColorMode, Box } from '@/design-system'; import LinearGradient from 'react-native-linear-gradient'; import { useDimensions } from '@/hooks'; import { ETH_COLOR, ETH_COLOR_DARK } from '../constants'; -import { getTintedBackgroundColor } from '../utils'; +import { getTintedBackgroundColor } from '../utils/swaps'; import { IS_ANDROID } from '@/env'; import { ScreenCornerRadius } from 'react-native-screen-corner-radius'; import { navbarHeight } from '@/components/navbar/Navbar'; diff --git a/src/__swaps__/screens/Swap/components/SwapNavbar.tsx b/src/__swaps__/screens/Swap/components/SwapNavbar.tsx index f484bc1f82f..370bd55231f 100644 --- a/src/__swaps__/screens/Swap/components/SwapNavbar.tsx +++ b/src/__swaps__/screens/Swap/components/SwapNavbar.tsx @@ -17,7 +17,7 @@ import { safeAreaInsetValues } from '@/utils'; import { ETH_COLOR, ETH_COLOR_DARK, THICK_BORDER_WIDTH } from '../constants'; import { OUTPUT_COLOR } from '../dummyValues'; -import { getHighContrastColor, opacity } from '../utils'; +import { getHighContrastColor, opacity } from '../utils/swaps'; import { IS_ANDROID, IS_IOS } from '@/env'; import { useSwapContext } from '../providers/swap-provider'; diff --git a/src/__swaps__/screens/Swap/components/SwapNumberPad.tsx b/src/__swaps__/screens/Swap/components/SwapNumberPad.tsx index 1097f449184..c23521668fa 100644 --- a/src/__swaps__/screens/Swap/components/SwapNumberPad.tsx +++ b/src/__swaps__/screens/Swap/components/SwapNumberPad.tsx @@ -12,7 +12,7 @@ import Animated, { } from 'react-native-reanimated'; import { Box, Columns, HitSlop, Separator, Text, useColorMode, useForegroundColor } from '@/design-system'; -import { stripCommas } from '../utils'; +import { stripCommas } from '../utils/swaps'; import { CUSTOM_KEYBOARD_HEIGHT, LIGHT_SEPARATOR_COLOR, diff --git a/src/__swaps__/screens/Swap/components/SwapSlider.tsx b/src/__swaps__/screens/Swap/components/SwapSlider.tsx index 1a68b3e4be8..4b9e712e2a5 100644 --- a/src/__swaps__/screens/Swap/components/SwapSlider.tsx +++ b/src/__swaps__/screens/Swap/components/SwapSlider.tsx @@ -33,7 +33,7 @@ import { snappySpringConfig, springConfig, } from '../constants'; -import { clamp, opacity } from '../utils'; +import { clamp, opacity } from '../utils/swaps'; import { useSwapContext } from '../providers/swap-provider'; import { SwapCoinIcon } from './SwapCoinIcon'; import { INPUT_ADDRESS, INPUT_NETWORK, INPUT_SYMBOL } from '../dummyValues'; diff --git a/src/__swaps__/screens/Swap/components/TokenList.tsx b/src/__swaps__/screens/Swap/components/TokenList.tsx index b24a9822d45..ce940948ca1 100644 --- a/src/__swaps__/screens/Swap/components/TokenList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList.tsx @@ -9,7 +9,7 @@ import { useTheme } from '@/theme'; import { SwapCoinIcon } from './SwapCoinIcon'; import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../constants'; import { DAI_ADDRESS, ETH_ADDRESS, USDC_ADDRESS } from '../dummyValues'; -import { opacity } from '../utils'; +import { opacity } from '../utils/swaps'; import { CoinRow } from './CoinRow'; import { SearchInput } from './SearchInput'; diff --git a/src/__swaps__/screens/Swap/components/controls/SwapActions.tsx b/src/__swaps__/screens/Swap/components/controls/SwapActions.tsx index f57511e7d4c..820eb332078 100644 --- a/src/__swaps__/screens/Swap/components/controls/SwapActions.tsx +++ b/src/__swaps__/screens/Swap/components/controls/SwapActions.tsx @@ -7,7 +7,7 @@ import { safeAreaInsetValues } from '@/utils'; import { SwapActionButton } from '../../components/SwapActionButton'; import { GasButton } from '../../components/GasButton'; import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR, THICK_BORDER_WIDTH } from '../../constants'; -import { opacity } from '../../utils'; +import { opacity } from '../../utils/swaps'; import { IS_ANDROID } from '@/env'; import { useSwapContext } from '../../providers/swap-provider'; diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputStyles.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputStyles.ts index ade91228f1e..155e20bc2bf 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputStyles.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputStyles.ts @@ -13,7 +13,7 @@ import { fadeConfig, springConfig, } from '../constants'; -import { opacity } from '../utils'; +import { opacity } from '../utils/swaps'; export const useSwapInputStyles = ({ bottomInput, diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index 5176a2d73e3..b17ff010666 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -12,7 +12,7 @@ import { useDebouncedCallback } from 'use-debounce'; import { SCRUBBER_WIDTH, SLIDER_WIDTH, snappySpringConfig } from '../constants'; import { IS_INPUT_STABLECOIN, IS_OUTPUT_STABLECOIN, SWAP_FEE } from '../dummyValues'; -import { inputKeys, inputMethods } from '../types'; +import { inputKeys, inputMethods } from '../types/swap'; import { addCommasToNumber, clamp, @@ -22,7 +22,7 @@ import { niceIncrementFormatter, trimTrailingZeros, valueBasedDecimalFormatter, -} from '../utils'; +} from '../utils/swaps'; export function useSwapInputsController({ focusedInput, diff --git a/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts b/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts index a4ef7835d4a..36bc871bfa9 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts @@ -21,8 +21,8 @@ import { sliderConfig, slowFadeConfig, } from '../constants'; -import { inputKeys, inputMethods } from '../types'; -import { opacity } from '../utils'; +import { inputKeys, inputMethods } from '../types/swap'; +import { opacity } from '../utils/swaps'; export function useSwapTextStyles({ bottomColor, diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index a35bdff8c30..a0cc3f94055 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, ReactNode, SetStateAction, Dispatch, useState } from 'react'; import { SharedValue, useAnimatedStyle, useDerivedValue, useSharedValue } from 'react-native-reanimated'; -import { inputKeys } from '../types'; +import { inputKeys } from '../types/swap'; import { INITIAL_SLIDER_POSITION, SLIDER_COLLAPSED_HEIGHT, SLIDER_HEIGHT, SLIDER_WIDTH, ETH_COLOR_DARK, ETH_COLOR } from '../constants'; import { INPUT_ASSET_BALANCE, INPUT_ASSET_USD_PRICE, OUTPUT_ASSET_USD_PRICE, OUTPUT_COLOR } from '../dummyValues'; import { useColorMode } from '@/design-system'; diff --git a/src/__swaps__/screens/Swap/types/assets.ts b/src/__swaps__/screens/Swap/types/assets.ts new file mode 100644 index 00000000000..b4c78ea6e1c --- /dev/null +++ b/src/__swaps__/screens/Swap/types/assets.ts @@ -0,0 +1,177 @@ +import { Address } from 'viem'; + +import { ChainId, ChainName } from '@/__swaps__/screens/Swap/types/chains'; + +import { ETH_ADDRESS } from '@/references'; + +import { SearchAsset } from './search'; + +export type AddressOrEth = Address | typeof ETH_ADDRESS; + +export interface ParsedAsset { + address: AddressOrEth; + chainId: ChainId; + chainName: ChainName; + colors?: { + primary?: string; + fallback?: string; + shadow?: string; + }; + isNativeAsset: boolean; + name: string; + native: { + price?: { + change: string; + amount: number; + display: string; + }; + }; + mainnetAddress?: AddressOrEth; + price?: ZerionAssetPrice; + symbol: string; + uniqueId: UniqueId; + decimals: number; + icon_url?: string; + type?: AssetType; + smallBalance?: boolean; + standard?: 'erc-721' | 'erc-1155'; + networks?: AssetApiResponse['networks']; + bridging?: { + isBridgeable: boolean; + networks: { [id in ChainId]?: { bridgeable: boolean } }; + }; +} + +export interface ParsedUserAsset extends ParsedAsset { + balance: { + amount: string; + display: string; + }; + native: { + balance: { + amount: string; + display: string; + }; + price?: { + change: string; + amount: number; + display: string; + }; + }; +} + +export type SearchAssetWithPrice = SearchAsset & ParsedAsset; +export type ParsedSearchAsset = SearchAsset & ParsedUserAsset; + +export type ParsedAssetsDict = Record; + +export type ParsedAssetsDictByChain = Record; + +export interface ZerionAssetPrice { + value: number; + relative_change_24h?: number; +} + +export type AssetApiResponse = { + asset_code: AddressOrEth; + decimals: number; + icon_url: string; + name: string; + chain_id?: number; + price?: { + value: number; + changed_at: number; + relative_change_24h: number; + }; + symbol: string; + colors?: { primary?: string; fallback?: string; shadow?: string }; + network?: ChainName; + networks?: { + [chainId in ChainId]?: { + address: chainId extends ChainId.mainnet ? AddressOrEth : Address; + decimals: number; + }; + }; + type?: AssetType; + interface?: 'erc-721' | 'erc-1155'; +}; + +type AssetType = ProtocolType | 'nft'; + +export interface ZerionAsset { + asset_code: AddressOrEth; + colors?: { + primary: string; + fallback: string; + }; + implementations?: Record; + mainnet_address?: AddressOrEth; + name: string; + symbol: string; + decimals: number; + type?: AssetType; + icon_url?: string; + is_displayable?: boolean; + is_verified?: boolean; + price?: ZerionAssetPrice; + network?: ChainName; + bridging: { + bridgeable: boolean; + networks: { [id in ChainId]?: { bridgeable: boolean } }; + }; +} + +// protocols https://github.com/rainbow-me/go-utils-lib/blob/master/pkg/enums/token_type.go#L44 +export type ProtocolType = + | 'aave-v2' + | 'balancer' + | 'curve' + | 'compound' + | 'compound-v3' + | 'maker' + | 'one-inch' + | 'piedao-pool' + | 'yearn' + | 'yearn-v2' + | 'uniswap-v2' + | 'aave-v3' + | 'harvest' + | 'lido' + | 'uniswap-v3' + | 'convex' + | 'convex-frax' + | 'pancake-swap' + | 'balancer-v2' + | 'frax' + | 'gmx' + | 'aura' + | 'pickle' + | 'yearn-v3' + | 'venus' + | 'sushiswap'; + +export type AssetMetadata = { + circulatingSupply: number; + colors?: { primary: string; fallback?: string; shadow?: string }; + decimals: number; + description: string; + fullyDilutedValuation: number; + iconUrl: string; + marketCap: number; + name: string; + networks?: { + [chainId in ChainId]?: { + address: chainId extends ChainId.mainnet ? AddressOrEth : Address; + decimals: number; + }; + }; + price: { + value: number; + relativeChange24h: number; + }; + symbol: string; + totalSupply: number; + volume1d: number; +}; + +export type UniqueId = `${Address}_${ChainId}`; diff --git a/src/__swaps__/screens/Swap/types/chains.ts b/src/__swaps__/screens/Swap/types/chains.ts new file mode 100644 index 00000000000..bb8480a686e --- /dev/null +++ b/src/__swaps__/screens/Swap/types/chains.ts @@ -0,0 +1,232 @@ +import * as chain from 'viem/chains'; +import type { Chain as _Chain } from 'viem/chains'; + +export type Chain = _Chain & { + network: string; +}; + +const HARDHAT_CHAIN_ID = 1337; +const BLAST_CHAIN_ID = 81457; +const HARDHAT_OP_CHAIN_ID = 1338; + +export const chainHardhat: Chain = { + id: HARDHAT_CHAIN_ID, + name: 'Hardhat', + network: 'hardhat', + nativeCurrency: { + decimals: 18, + name: 'Hardhat', + symbol: 'ETH', + }, + rpcUrls: { + public: { http: ['http://127.0.0.1:8545'] }, + default: { http: ['http://127.0.0.1:8545'] }, + }, + testnet: true, +}; + +export const chainBlast: Chain = { + id: BLAST_CHAIN_ID, + name: 'Blast', + network: 'blast', + rpcUrls: { + public: { http: [process.env.BLAST_MAINNET_RPC as string] }, + default: { + http: [process.env.BLAST_MAINNET_RPC as string], + }, + }, + blockExplorers: { + default: { name: 'Blastscan', url: 'https://blastscan.io/' }, + }, + nativeCurrency: { + name: 'Blast', + symbol: 'BLAST', + decimals: 18, + }, +}; + +export const chainHardhatOptimism: Chain = { + id: HARDHAT_OP_CHAIN_ID, + name: 'Hardhat OP', + network: 'hardhat-optimism', + nativeCurrency: { + decimals: 18, + name: 'Hardhat OP', + symbol: 'ETH', + }, + rpcUrls: { + public: { http: ['http://127.0.0.1:8545'] }, + default: { http: ['http://127.0.0.1:8545'] }, + }, + testnet: true, +}; + +export enum ChainName { + arbitrum = 'arbitrum', + arbitrumNova = 'arbitrum-nova', + arbitrumSepolia = 'arbitrum-sepolia', + avalanche = 'avalanche', + avalancheFuji = 'avalanche-fuji', + base = 'base', + blast = 'blast', + bsc = 'bsc', + celo = 'celo', + gnosis = 'gnosis', + linea = 'linea', + manta = 'manta', + optimism = 'optimism', + polygon = 'polygon', + polygonZkEvm = 'polygon-zkevm', + rari = 'rari', + scroll = 'scroll', + zora = 'zora', + mainnet = 'mainnet', + holesky = 'holesky', + hardhat = 'hardhat', + hardhatOptimism = 'hardhat-optimism', + goerli = 'goerli', + sepolia = 'sepolia', + optimismSepolia = 'optimism-sepolia', + bscTestnet = 'bsc-testnet', + polygonMumbai = 'polygon-mumbai', + arbitrumGoerli = 'arbitrum-goerli', + baseSepolia = 'base-sepolia', + zoraSepolia = 'zora-sepolia', +} + +export enum ChainId { + arbitrum = chain.arbitrum.id, + arbitrumNova = chain.arbitrumNova.id, + avalanche = chain.avalanche.id, + avalancheFuji = chain.avalancheFuji.id, + base = chain.base.id, + blast = BLAST_CHAIN_ID, + bsc = chain.bsc.id, + celo = chain.celo.id, + gnosis = chain.gnosis.id, + linea = chain.linea.id, + manta = chain.manta.id, + optimism = chain.optimism.id, + mainnet = chain.mainnet.id, + polygon = chain.polygon.id, + polygonZkEvm = chain.polygonZkEvm.id, + rari = 1380012617, + zora = chain.zora.id, + hardhat = HARDHAT_CHAIN_ID, + hardhatOptimism = chainHardhatOptimism.id, + goerli = chain.goerli.id, + sepolia = chain.sepolia.id, + scroll = chain.scroll.id, + holesky = chain.holesky.id, + optimismSepolia = chain.optimismSepolia.id, + bscTestnet = chain.bscTestnet.id, + polygonMumbai = chain.polygonMumbai.id, + arbitrumGoerli = chain.arbitrumGoerli.id, + arbitrumSepolia = chain.arbitrumSepolia.id, + baseSepolia = chain.baseSepolia.id, + zoraSepolia = chain.zoraSepolia.id, +} + +export const chainNameToIdMapping: { + [key in ChainName | 'ethereum' | 'ethereum-sepolia']: ChainId; +} = { + ['ethereum']: ChainId.mainnet, + [ChainName.arbitrum]: ChainId.arbitrum, + [ChainName.arbitrumNova]: ChainId.arbitrumNova, + [ChainName.arbitrumSepolia]: ChainId.arbitrumSepolia, + [ChainName.avalanche]: ChainId.avalanche, + [ChainName.avalancheFuji]: ChainId.avalancheFuji, + [ChainName.base]: ChainId.base, + [ChainName.bsc]: ChainId.bsc, + [ChainName.celo]: ChainId.celo, + [ChainName.gnosis]: ChainId.gnosis, + [ChainName.linea]: ChainId.linea, + [ChainName.manta]: ChainId.manta, + [ChainName.optimism]: ChainId.optimism, + [ChainName.polygon]: ChainId.polygon, + [ChainName.polygonZkEvm]: ChainId.polygonZkEvm, + [ChainName.rari]: ChainId.rari, + [ChainName.scroll]: ChainId.scroll, + [ChainName.zora]: ChainId.zora, + [ChainName.mainnet]: ChainId.mainnet, + [ChainName.holesky]: ChainId.holesky, + [ChainName.hardhat]: ChainId.hardhat, + [ChainName.hardhatOptimism]: ChainId.hardhatOptimism, + [ChainName.goerli]: ChainId.goerli, + ['ethereum-sepolia']: ChainId.sepolia, + [ChainName.sepolia]: ChainId.sepolia, + [ChainName.optimismSepolia]: ChainId.optimismSepolia, + [ChainName.bscTestnet]: ChainId.bscTestnet, + [ChainName.polygonMumbai]: ChainId.polygonMumbai, + [ChainName.arbitrumGoerli]: ChainId.arbitrumGoerli, + [ChainName.baseSepolia]: ChainId.baseSepolia, + [ChainName.zoraSepolia]: ChainId.zoraSepolia, + [ChainName.blast]: ChainId.blast, +}; + +export const chainIdToNameMapping: { + [key in ChainId]: ChainName; +} = { + [ChainId.arbitrum]: ChainName.arbitrum, + [ChainId.arbitrumNova]: ChainName.arbitrumNova, + [ChainId.arbitrumSepolia]: ChainName.arbitrumSepolia, + [ChainId.avalanche]: ChainName.avalanche, + [ChainId.avalancheFuji]: ChainName.avalancheFuji, + [ChainId.base]: ChainName.base, + [ChainId.blast]: ChainName.blast, + [ChainId.bsc]: ChainName.bsc, + [ChainId.celo]: ChainName.celo, + [ChainId.gnosis]: ChainName.gnosis, + [ChainId.linea]: ChainName.linea, + [ChainId.manta]: ChainName.manta, + [ChainId.optimism]: ChainName.optimism, + [ChainId.polygon]: ChainName.polygon, + [ChainId.polygonZkEvm]: ChainName.polygonZkEvm, + [ChainId.rari]: ChainName.rari, + [ChainId.scroll]: ChainName.scroll, + [ChainId.zora]: ChainName.zora, + [ChainId.mainnet]: ChainName.mainnet, + [ChainId.holesky]: ChainName.holesky, + [ChainId.hardhat]: ChainName.hardhat, + [ChainId.hardhatOptimism]: ChainName.hardhatOptimism, + [ChainId.goerli]: ChainName.goerli, + [ChainId.sepolia]: ChainName.sepolia, + [ChainId.optimismSepolia]: ChainName.optimismSepolia, + [ChainId.bscTestnet]: ChainName.bscTestnet, + [ChainId.polygonMumbai]: ChainName.polygonMumbai, + [ChainId.arbitrumGoerli]: ChainName.arbitrumGoerli, + [ChainId.baseSepolia]: ChainName.baseSepolia, + [ChainId.zoraSepolia]: ChainName.zoraSepolia, +}; + +export const ChainNameDisplay = { + [ChainId.arbitrum]: 'Arbitrum', + [ChainId.arbitrumNova]: chain.arbitrumNova.name, + [ChainId.avalanche]: 'Avalanche', + [ChainId.avalancheFuji]: 'Avalanche Fuji', + [ChainId.base]: 'Base', + [ChainId.blast]: 'Blast', + [ChainId.bsc]: 'BSC', + [ChainId.celo]: chain.celo.name, + [ChainId.linea]: 'Linea', + [ChainId.manta]: 'Manta', + [ChainId.optimism]: 'Optimism', + [ChainId.polygon]: 'Polygon', + [ChainId.polygonZkEvm]: chain.polygonZkEvm.name, + [ChainId.rari]: 'RARI Chain', + [ChainId.scroll]: chain.scroll.name, + [ChainId.zora]: 'Zora', + [ChainId.mainnet]: 'Ethereum', + [ChainId.hardhat]: 'Hardhat', + [ChainId.hardhatOptimism]: chainHardhatOptimism.name, + [ChainId.goerli]: chain.goerli.name, + [ChainId.sepolia]: chain.sepolia.name, + [ChainId.holesky]: chain.holesky.name, + [ChainId.optimismSepolia]: chain.optimismSepolia.name, + [ChainId.bscTestnet]: 'BSC Testnet', + [ChainId.polygonMumbai]: chain.polygonMumbai.name, + [ChainId.arbitrumGoerli]: chain.arbitrumGoerli.name, + [ChainId.arbitrumSepolia]: chain.arbitrumSepolia.name, + [ChainId.baseSepolia]: chain.baseSepolia.name, + [ChainId.zoraSepolia]: 'Zora Sepolia', +} as const; diff --git a/src/__swaps__/screens/Swap/types/search.ts b/src/__swaps__/screens/Swap/types/search.ts new file mode 100644 index 00000000000..1b340ebcbe7 --- /dev/null +++ b/src/__swaps__/screens/Swap/types/search.ts @@ -0,0 +1,33 @@ +import { Address } from 'viem'; + +import { AddressOrEth, ParsedAsset, UniqueId } from '@/__swaps__/screens/Swap/types/assets'; +import { ChainId } from '@/__swaps__/screens/Swap/types/chains'; + +export type TokenSearchAssetKey = keyof ParsedAsset; + +export type TokenSearchThreshold = 'CONTAINS' | 'CASE_SENSITIVE_EQUAL'; + +export type TokenSearchListId = 'highLiquidityAssets' | 'lowLiquidityAssets' | 'verifiedAssets'; + +export type SearchAsset = { + address: AddressOrEth; + chainId: ChainId; + colors?: { primary?: string; fallback?: string }; + decimals: number; + highLiquidity: boolean; + icon_url: string; + isRainbowCurated: boolean; + isNativeAsset: boolean; + isVerified: boolean; + mainnetAddress: AddressOrEth; + name: string; + networks: { + [chainId in ChainId]?: { + address: chainId extends ChainId.mainnet ? AddressOrEth : Address; + decimals: number; + }; + }; + rainbowMetadataId: number; + symbol: string; + uniqueId: UniqueId; +}; diff --git a/src/__swaps__/screens/Swap/types.ts b/src/__swaps__/screens/Swap/types/swap.ts similarity index 100% rename from src/__swaps__/screens/Swap/types.ts rename to src/__swaps__/screens/Swap/types/swap.ts diff --git a/src/__swaps__/screens/Swap/utils/assets.ts b/src/__swaps__/screens/Swap/utils/assets.ts new file mode 100644 index 00000000000..ed3d9c685db --- /dev/null +++ b/src/__swaps__/screens/Swap/utils/assets.ts @@ -0,0 +1,340 @@ +import { AddressZero } from '@ethersproject/constants'; +import isValidDomain from 'is-valid-domain'; + +import { ETH_ADDRESS, SupportedCurrencyKey } from '@/references'; +import { + AddressOrEth, + AssetApiResponse, + AssetMetadata, + ParsedAsset, + ParsedSearchAsset, + ParsedUserAsset, + UniqueId, + ZerionAsset, + ZerionAssetPrice, +} from '@/__swaps__/screens/Swap/types/assets'; +import { ChainId, ChainName } from '@/__swaps__/screens/Swap/types/chains'; + +import * as i18n from '@/languages'; +import { SearchAsset } from '../types/search'; + +import { chainIdFromChainName, chainNameFromChainId, customChainIdsToAssetNames, isNativeAsset } from './chains'; +import { + convertAmountAndPriceToNativeDisplay, + convertAmountToBalanceDisplay, + convertAmountToNativeDisplay, + convertAmountToPercentageDisplay, + convertRawAmountToDecimalFormat, +} from './numbers'; + +const get24HrChange = (priceData?: ZerionAssetPrice) => { + const twentyFourHrChange = priceData?.relative_change_24h; + return twentyFourHrChange ? convertAmountToPercentageDisplay(twentyFourHrChange) : ''; +}; + +export const getCustomChainIconUrl = (chainId: ChainId, address: AddressOrEth) => { + if (!chainId || !customChainIdsToAssetNames[chainId]) return ''; + const baseUrl = 'https://raw.githubusercontent.com/rainbow-me/assets/master/blockchains/'; + + if (address === AddressZero || address === ETH_ADDRESS) { + return `${baseUrl}${customChainIdsToAssetNames[chainId]}/info/logo.png`; + } else { + return `${baseUrl}${customChainIdsToAssetNames[chainId]}/assets/${address}/logo.png`; + } +}; + +export const getNativeAssetPrice = ({ priceData, currency }: { priceData?: ZerionAssetPrice; currency: SupportedCurrencyKey }) => { + const priceUnit = priceData?.value; + return { + change: get24HrChange(priceData), + amount: priceUnit || 0, + display: convertAmountToNativeDisplay(priceUnit || 0, currency), + }; +}; + +export const getNativeAssetBalance = ({ + currency, + priceUnit, + value, +}: { + currency: SupportedCurrencyKey; + decimals: number; + priceUnit: number; + value: string | number; +}) => { + return convertAmountAndPriceToNativeDisplay(value, priceUnit, currency); +}; + +const isZerionAsset = (asset: ZerionAsset | AssetApiResponse): asset is ZerionAsset => 'implementations' in asset || !('networks' in asset); + +const getUniqueIdForAsset = ({ asset }: { asset: ZerionAsset | AssetApiResponse }): UniqueId => { + const address = asset.asset_code; + const chainName = asset.network ?? ChainName.mainnet; + const networks = 'networks' in asset ? asset.networks || {} : {}; + const chainId = ('chain_id' in asset && asset.chain_id) || chainIdFromChainName(chainName) || Number(Object.keys(networks)[0]); + + // ZerionAsset should be removed when we move fully away from websckets/refraction api + const mainnetAddress = isZerionAsset(asset) + ? asset.mainnet_address || asset.implementations?.[ChainName.mainnet]?.address || undefined + : networks[ChainId.mainnet]?.address; + + return `${mainnetAddress || address}_${chainId}`; +}; + +export function parseAsset({ asset, currency }: { asset: ZerionAsset | AssetApiResponse; currency: SupportedCurrencyKey }): ParsedAsset { + const address = asset.asset_code; + const chainName = asset.network ?? ChainName.mainnet; + const networks = 'networks' in asset ? asset.networks || {} : {}; + const chainId = ('chain_id' in asset && asset.chain_id) || chainIdFromChainName(chainName) || Number(Object.keys(networks)[0]); + + // ZerionAsset should be removed when we move fully away from websckets/refraction api + const mainnetAddress = isZerionAsset(asset) + ? asset.mainnet_address || asset.implementations?.[ChainName.mainnet]?.address || undefined + : networks[ChainId.mainnet]?.address; + + const standard = 'interface' in asset ? asset.interface : undefined; + const uniqueId = getUniqueIdForAsset({ asset }); + const parsedAsset = { + address, + uniqueId, + chainId, + chainName, + mainnetAddress, + isNativeAsset: isNativeAsset(address, chainId), + native: { + price: getNativeAssetPrice({ + currency, + priceData: asset?.price, + }), + }, + name: asset.name || i18n.t('tokens_tab.unknown_token'), + price: asset.price, + symbol: asset.symbol, + type: asset.type, + decimals: asset.decimals, + icon_url: asset.icon_url || getCustomChainIconUrl(chainId, address), + colors: asset.colors, + standard, + ...('networks' in asset && { networks: asset.networks }), + ...('bridging' in asset && { + bridging: { + isBridgeable: asset.bridging.bridgeable, + networks: asset.bridging.networks, + }, + }), + }; + + return parsedAsset; +} + +export function parseAssetMetadata({ + address, + asset, + chainId, + currency, +}: { + address: AddressOrEth; + asset: AssetMetadata; + chainId: ChainId; + currency: SupportedCurrencyKey; +}): ParsedAsset { + const mainnetAddress = asset.networks?.[ChainId.mainnet]?.address || address; + const uniqueId = getUniqueIdForAsset({ + asset: { + ...asset, + asset_code: address, + chain_id: chainId, + icon_url: '', + price: { + changed_at: -1, + relative_change_24h: asset.price.relativeChange24h, + value: asset.price.value, + }, + } as AssetApiResponse, + }); + const priceData = { + relative_change_24h: asset?.price?.relativeChange24h, + value: asset?.price?.value, + }; + const parsedAsset = { + address, + chainId, + chainName: chainNameFromChainId(chainId), + colors: asset?.colors, + decimals: asset?.decimals, + icon_url: asset?.iconUrl, + isNativeAsset: isNativeAsset(address, chainId), + mainnetAddress, + name: asset?.name || i18n.t('tokens_tab.unknown_token'), + native: { + price: getNativeAssetPrice({ + currency, + priceData, + }), + }, + price: priceData, + symbol: asset?.symbol, + uniqueId, + networks: asset?.networks, + } satisfies ParsedAsset; + return parsedAsset; +} + +export function parseUserAsset({ + asset, + currency, + balance, + smallBalance, +}: { + asset: ZerionAsset | AssetApiResponse; + currency: SupportedCurrencyKey; + balance: string; + smallBalance?: boolean; +}) { + const parsedAsset = parseAsset({ asset, currency }); + return parseUserAssetBalances({ + asset: parsedAsset, + currency, + balance, + smallBalance, + }); +} + +export function parseUserAssetBalances({ + asset, + currency, + balance, + smallBalance = false, +}: { + asset: ParsedAsset; + currency: SupportedCurrencyKey; + balance: string; + smallBalance?: boolean; +}) { + const { decimals, symbol, price } = asset; + const amount = convertRawAmountToDecimalFormat(balance, decimals); + + return { + ...asset, + balance: { + amount, + display: convertAmountToBalanceDisplay(amount, { decimals, symbol }), + }, + native: { + ...asset.native, + balance: getNativeAssetBalance({ + currency, + decimals, + priceUnit: price?.value || 0, + value: amount, + }), + }, + smallBalance, + }; +} + +export function parseParsedUserAsset({ + parsedAsset, + currency, + quantity, +}: { + parsedAsset: ParsedUserAsset; + currency: SupportedCurrencyKey; + quantity: string; +}): ParsedUserAsset { + const amount = convertRawAmountToDecimalFormat(quantity, parsedAsset?.decimals); + return { + ...parsedAsset, + balance: { + amount, + display: convertAmountToBalanceDisplay(amount, { + decimals: parsedAsset?.decimals, + symbol: parsedAsset?.symbol, + }), + }, + native: { + ...parsedAsset.native, + balance: getNativeAssetBalance({ + currency, + decimals: parsedAsset?.decimals, + priceUnit: parsedAsset?.price?.value || 0, + value: amount, + }), + }, + }; +} + +export const parseSearchAsset = ({ + assetWithPrice, + searchAsset, + userAsset, +}: { + assetWithPrice?: ParsedAsset; + searchAsset: ParsedSearchAsset | SearchAsset; + userAsset?: ParsedUserAsset; +}): ParsedSearchAsset => ({ + ...searchAsset, + address: searchAsset.address, + chainId: searchAsset.chainId, + chainName: chainNameFromChainId(searchAsset.chainId), + native: { + balance: userAsset?.native.balance || { + amount: '0', + display: '0.00', + }, + price: assetWithPrice?.native.price || userAsset?.native?.price, + }, + price: assetWithPrice?.price || userAsset?.price, + balance: userAsset?.balance || { amount: '0', display: '0.00' }, + icon_url: userAsset?.icon_url || assetWithPrice?.icon_url || searchAsset?.icon_url, + colors: userAsset?.colors || assetWithPrice?.colors || searchAsset?.colors, + type: userAsset?.type || assetWithPrice?.type, +}); + +export function filterAsset(asset: ZerionAsset) { + const nameFragments = asset?.name?.split(' '); + const nameContainsURL = nameFragments.some(f => isValidDomain(f)); + const symbolFragments = asset?.symbol?.split(' '); + const symbolContainsURL = symbolFragments.some(f => isValidDomain(f)); + const shouldFilter = nameContainsURL || symbolContainsURL; + return shouldFilter; +} + +const assetQueryFragment = ( + address: AddressOrEth, + chainId: ChainId, + currency: SupportedCurrencyKey, + index: number, + withPrice?: boolean +) => { + const priceQuery = withPrice ? 'price { value relativeChange24h }' : ''; + return `Q${index}: token(address: "${address}", chainID: ${chainId}, currency: "${currency}") { + colors { + primary + fallback + shadow + } + decimals + iconUrl + name + networks + symbol + ${priceQuery} + }`; +}; + +export const chunkArray = (arr: TItem[], chunkSize: number) => { + const result = []; + + for (let i = 0; i < arr.length; i += chunkSize) { + result.push(arr.slice(i, i + chunkSize)); + } + + return result; +}; + +export const createAssetQuery = (addresses: AddressOrEth[], chainId: ChainId, currency: SupportedCurrencyKey, withPrice?: boolean) => { + return `{ + ${addresses.map((a, i) => assetQueryFragment(a, chainId, currency, i, withPrice)).join(',')} + }`; +}; diff --git a/src/__swaps__/screens/Swap/utils/chains.ts b/src/__swaps__/screens/Swap/utils/chains.ts new file mode 100644 index 00000000000..e726a3b4327 --- /dev/null +++ b/src/__swaps__/screens/Swap/utils/chains.ts @@ -0,0 +1,175 @@ +import { AddressZero } from '@ethersproject/constants'; +import { celo, fantom, harmonyOne, moonbeam } from 'viem/chains'; + +import { ChainId, ChainName, ChainNameDisplay, chainIdToNameMapping, chainNameToIdMapping } from '@/__swaps__/screens/Swap/types/chains'; + +import { AddressOrEth } from '../types/assets'; + +import { isLowerCaseMatch } from './strings'; +import { ETH_ADDRESS } from '@/references'; +import { Address } from 'viem'; +import { getNetworkFromChainId } from '@/utils/ethereumUtils'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +export const customChainIdsToAssetNames: Record = { + 42170: 'arbitrumnova', + 1313161554: 'aurora', + 43114: 'avalanchex', + 81457: 'blast', + 168587773: 'blastsepolia', + 288: 'boba', + 42220: 'celo', + 61: 'classic', + 25: 'cronos', + 2000: 'dogechain', + 250: 'fantom', + 314: 'filecoin', + 1666600000: 'harmony', + 13371: 'immutablezkevm', + 2222: 'kavaevm', + 8217: 'klaytn', + 59144: 'linea', + 957: 'lyra', + 169: 'manta', + 5000: 'mantle', + 1088: 'metis', + 34443: 'mode', + 1284: 'moonbeam', + 7700: 'nativecanto', + 204: 'opbnb', + 11297108109: 'palm', + 424: 'pgn', + 1101: 'polygonzkevm', + 369: 'pulsechain', + 1380012617: 'rari', + 1918988905: 'raritestnet', + 17001: 'redstoneholesky', + 534352: 'scroll', + 100: 'xdai', + 324: 'zksync', +}; + +export const NATIVE_ASSETS_PER_CHAIN: Record = { + [ChainId.mainnet]: ETH_ADDRESS as Address, + [ChainId.hardhat]: AddressZero as Address, + [ChainId.goerli]: AddressZero as Address, + [ChainId.sepolia]: AddressZero as Address, + [ChainId.holesky]: AddressZero as Address, + [ChainId.arbitrum]: AddressZero as Address, + [ChainId.arbitrumGoerli]: AddressZero as Address, + [ChainId.arbitrumSepolia]: AddressZero as Address, + [ChainId.bsc]: AddressZero as Address, + [ChainId.bscTestnet]: AddressZero as Address, + [ChainId.optimism]: AddressZero as Address, + [ChainId.hardhatOptimism]: AddressZero as Address, + [ChainId.optimismSepolia]: AddressZero as Address, + [ChainId.rari]: AddressZero as Address, + [ChainId.base]: AddressZero as Address, + [ChainId.baseSepolia]: AddressZero as Address, + [ChainId.zora]: AddressZero as Address, + [ChainId.zoraSepolia]: AddressZero as Address, + [ChainId.polygon]: AddressZero as Address, + [ChainId.polygonMumbai]: AddressZero as Address, + [ChainId.avalanche]: AddressZero as Address, + [ChainId.avalancheFuji]: AddressZero as Address, + [ChainId.blast]: AddressZero as Address, +}; + +export const getChainName = ({ chainId }: { chainId: number }) => { + const network = getNetworkFromChainId(chainId); + return ChainNameDisplay[chainId] || network; +}; + +/** + * @desc Checks if the given chain is a Layer 2. + * @param chain The chain name to check. + * @return Whether or not the chain is an L2 network. + */ +export const isL2Chain = (chain: ChainName | ChainId): boolean => { + switch (chain) { + case ChainName.arbitrum: + case ChainName.base: + case ChainName.bsc: + case ChainName.optimism: + case ChainName.polygon: + case ChainName.zora: + case ChainName.avalanche: + case ChainId.arbitrum: + case ChainId.base: + case ChainId.bsc: + case ChainId.optimism: + case ChainId.polygon: + case ChainId.zora: + case ChainId.avalanche: + return true; + default: + return false; + } +}; + +export function isNativeAsset(address: AddressOrEth, chainId: ChainId) { + return isLowerCaseMatch(NATIVE_ASSETS_PER_CHAIN[chainId], address); +} + +export function chainIdFromChainName(chainName: ChainName) { + return chainNameToIdMapping[chainName]; +} + +export function chainNameFromChainId(chainId: ChainId): ChainName { + return chainIdToNameMapping[chainId]; +} + +export const chainIdToUse = (connectedToHardhat: boolean, connectedToHardhatOp: boolean, activeSessionChainId: number) => { + if (connectedToHardhat) { + return ChainId.hardhat; + } + if (connectedToHardhatOp) { + return ChainId.hardhatOptimism; + } + return activeSessionChainId; +}; + +export const deriveChainIdByHostname = (hostname: string) => { + switch (hostname) { + case 'etherscan.io': + return ChainId.mainnet; + case 'goerli.etherscan.io': + return ChainId.goerli; + case 'arbiscan.io': + return ChainId.arbitrum; + case 'explorer-mumbai.maticvigil.com': + case 'explorer-mumbai.matic.today': + case 'mumbai.polygonscan.com': + return ChainId.polygonMumbai; + case 'polygonscan.com': + return ChainId.polygon; + case 'optimistic.etherscan.io': + return ChainId.optimism; + case 'bscscan.com': + return ChainId.bsc; + case 'ftmscan.com': + return fantom.id; + case 'explorer.celo.org': + return celo.id; + case 'explorer.harmony.one': + return harmonyOne.id; + case 'explorer.avax.network': + case 'subnets.avax.network': + case 'snowtrace.io': + return ChainId.avalanche; + case 'subnets-test.avax.network': + case 'testnet.snowtrace.io': + return ChainId.avalancheFuji; + case 'moonscan.io': + return moonbeam.id; + case 'explorer.holesky.redstone.xyz': + return 17001; + case 'blastscan.io': + return ChainId.blast; + case 'testnet.blastscan.io': + return 168587773; + default: + return ChainId.mainnet; + } +}; diff --git a/src/__swaps__/screens/Swap/utils/hex.ts b/src/__swaps__/screens/Swap/utils/hex.ts new file mode 100644 index 00000000000..7e540be352c --- /dev/null +++ b/src/__swaps__/screens/Swap/utils/hex.ts @@ -0,0 +1,30 @@ +import { isHexString } from '@ethersproject/bytes'; +import BigNumber from 'bignumber.js'; +import { startsWith } from 'lodash'; + +export type BigNumberish = number | string | BigNumber; + +/** + * @desc Checks if a hex string, ignoring prefixes and suffixes. + * @param value The string. + * @return Whether or not the string is a hex string. + */ +export const isHexStringIgnorePrefix = (value: string): boolean => { + if (!value) return false; + const trimmedValue = value.trim(); + const updatedValue = addHexPrefix(trimmedValue); + return isHexString(updatedValue); +}; + +/** + * @desc Adds an "0x" prefix to a string if one is not present. + * @param value The starting string. + * @return The prefixed string. + */ +export const addHexPrefix = (value: string): string => (startsWith(value, '0x') ? value : `0x${value}`); + +export const convertStringToHex = (stringToConvert: string): string => new BigNumber(stringToConvert).toString(16); + +export const toHex = (stringToConvert: string): string => addHexPrefix(convertStringToHex(stringToConvert)); + +export const toHexNoLeadingZeros = (value: string): string => toHex(value).replace(/^0x0*/, '0x'); diff --git a/src/__swaps__/screens/Swap/utils/numbers.ts b/src/__swaps__/screens/Swap/utils/numbers.ts new file mode 100644 index 00000000000..42f7ba22704 --- /dev/null +++ b/src/__swaps__/screens/Swap/utils/numbers.ts @@ -0,0 +1,309 @@ +import { BigNumber as EthersBigNumber } from '@ethersproject/bignumber'; +import BigNumber from 'bignumber.js'; +import currency from 'currency.js'; +import { isNil } from 'lodash'; + +import { supportedNativeCurrencies } from '@/references'; +import { BigNumberish } from '@/__swaps__/screens/Swap/utils/hex'; + +type nativeCurrencyType = typeof supportedNativeCurrencies; + +export const toBigNumber = (v?: string | number | BigNumber) => (v ? EthersBigNumber.from(v) : undefined); + +export const abs = (value: BigNumberish): string => new BigNumber(value).abs().toFixed(); + +export const isPositive = (value: BigNumberish): boolean => new BigNumber(value).isPositive(); + +export const subtract = (numberOne: BigNumberish, numberTwo: BigNumberish): string => + new BigNumber(numberOne).minus(new BigNumber(numberTwo)).toFixed(); + +export const convertAmountToRawAmount = (value: BigNumberish, decimals: number | string): string => + new BigNumber(value).times(new BigNumber(10).pow(decimals)).toFixed(); + +export const isZero = (value: BigNumberish): boolean => new BigNumber(value).isZero(); + +export const toFixedDecimals = (value: BigNumberish, decimals: number): string => new BigNumber(value).toFixed(decimals); + +export const convertNumberToString = (value: BigNumberish): string => new BigNumber(value).toFixed(); + +export const greaterThan = (numberOne: BigNumberish, numberTwo: BigNumberish): boolean => new BigNumber(numberOne).gt(numberTwo); + +export const greaterThanOrEqualTo = (numberOne: BigNumberish, numberTwo: BigNumberish): boolean => new BigNumber(numberOne).gte(numberTwo); + +export const isEqual = (numberOne: BigNumberish, numberTwo: BigNumberish): boolean => new BigNumber(numberOne).eq(numberTwo); + +export const formatFixedDecimals = (value: BigNumberish, decimals: number): string => { + const _value = convertNumberToString(value); + const _decimals = convertStringToNumber(decimals); + return new BigNumber(new BigNumber(_value).toFixed(_decimals)).toFixed(); +}; + +export const mod = (numberOne: BigNumberish, numberTwo: BigNumberish): string => + new BigNumber(numberOne).mod(new BigNumber(numberTwo)).toFixed(); + +/** + * @desc real floor divides two numbers + * @param {Number} numberOne + * @param {Number} numberTwo + * @return {String} + */ +export const floorDivide = (numberOne: BigNumberish, numberTwo: BigNumberish): string => + new BigNumber(numberOne).dividedToIntegerBy(new BigNumber(numberTwo)).toFixed(); + +/** + * @desc count value's number of decimals places + * @param {String} value + * @return {String} + */ +export const countDecimalPlaces = (value: BigNumberish): number => new BigNumber(value).dp(); + +/** + * @desc update the amount to display precision + * equivalent to ~0.01 of the native price + * or use most significant decimal + * if the updated precision amounts to zero + * @param {String} amount + * @param {String} nativePrice + * @param {Boolean} use rounding up mode + * @return {String} updated amount + */ +export const updatePrecisionToDisplay = (amount: BigNumberish, nativePrice?: BigNumberish, roundUp = false): string => { + if (!amount) return '0'; + const roundingMode = roundUp ? BigNumber.ROUND_UP : BigNumber.ROUND_DOWN; + if (!nativePrice) return new BigNumber(amount).decimalPlaces(6, roundingMode).toFixed(); + const bnAmount = new BigNumber(amount); + const significantDigitsOfNativePriceInteger = new BigNumber(nativePrice).decimalPlaces(0, BigNumber.ROUND_DOWN).sd(true); + const truncatedPrecision = new BigNumber(significantDigitsOfNativePriceInteger).plus(2, 10).toNumber(); + const truncatedAmount = bnAmount.decimalPlaces(truncatedPrecision, BigNumber.ROUND_DOWN); + return truncatedAmount.isZero() + ? new BigNumber(bnAmount.toPrecision(1, roundingMode)).toFixed() + : bnAmount.decimalPlaces(truncatedPrecision, roundingMode).toFixed(); +}; + +/** + * @desc format inputOne value to signficant decimals given inputTwo + * @param {String} inputOne + * @param {String} inputTwo + * @return {String} + */ +// TODO revisit logic, at least rename so it is not native amount dp +export const formatInputDecimals = (inputOne: BigNumberish, inputTwo: BigNumberish): string => { + const _nativeAmountDecimalPlaces = countDecimalPlaces(inputTwo); + const decimals = _nativeAmountDecimalPlaces > 8 ? _nativeAmountDecimalPlaces : 8; + const result = new BigNumber(formatFixedDecimals(inputOne, decimals)).toFormat().replace(/,/g, ''); + return result; +}; + +export const add = (numberOne: BigNumberish, numberTwo: BigNumberish): string => new BigNumber(numberOne).plus(numberTwo).toFixed(); + +export const minus = (numberOne: BigNumberish, numberTwo: BigNumberish): string => new BigNumber(numberOne).minus(numberTwo).toFixed(); + +export const addDisplay = (numberOne: string, numberTwo: string): string => { + const unit = numberOne.replace(/[\d.-]/g, ''); + const leftAlignedUnit = numberOne.indexOf(unit) === 0; + return currency(0, { symbol: unit, pattern: leftAlignedUnit ? '!#' : '#!' }) + .add(numberOne) + .add(numberTwo) + .format(); +}; + +export const multiply = (numberOne: BigNumberish, numberTwo: BigNumberish): string => new BigNumber(numberOne).times(numberTwo).toFixed(); + +export const addBuffer = (numberOne: BigNumberish, buffer: BigNumberish = '1.2'): string => + new BigNumber(numberOne).times(buffer).toFixed(0); + +export const divide = (numberOne: BigNumberish, numberTwo: BigNumberish): string => { + if (!(numberOne || numberTwo)) return '0'; + return new BigNumber(numberOne).dividedBy(numberTwo).toFixed(); +}; + +export const fraction = (target: BigNumberish, numerator: BigNumberish, denominator: BigNumberish): string => { + if (!target || !numerator || !denominator) return '0'; + return new BigNumber(target).times(numerator).dividedBy(denominator).toFixed(0); +}; + +/** + * @desc convert to asset amount units from native price value units + * @param {String} value + * @param {Object} asset + * @param {Number} priceUnit + * @return {String} + */ +export const convertAmountFromNativeValue = (value: BigNumberish, priceUnit: BigNumberish, decimals = 18): string => { + if (isNil(priceUnit) || isZero(priceUnit)) return '0'; + return new BigNumber(new BigNumber(value).dividedBy(priceUnit).toFixed(decimals, BigNumber.ROUND_DOWN)).toFixed(); +}; + +export const convertStringToNumber = (value: BigNumberish) => new BigNumber(value).toNumber(); + +export const lessThan = (numberOne: BigNumberish, numberTwo: BigNumberish): boolean => new BigNumber(numberOne).lt(numberTwo); + +export const lessOrEqualThan = (numberOne: BigNumberish, numberTwo: BigNumberish): boolean => + new BigNumber(numberOne).lt(numberTwo) || new BigNumber(numberOne).eq(numberTwo); + +export const handleSignificantDecimalsWithThreshold = (value: BigNumberish, decimals: number, threshold = '0.0001') => { + const result = toFixedDecimals(value, decimals); + return lessThan(result, threshold) ? `< ${threshold}` : result; +}; + +export const handleSignificantDecimals = (value: BigNumberish, decimals: number, buffer = 3, skipDecimals = false): string => { + let dec; + if (lessThan(new BigNumber(value).abs(), 1)) { + dec = new BigNumber(value).toFixed()?.slice?.(2).search(/[^0]/g) + buffer; + dec = Math.min(decimals, 8); + } else { + dec = Math.min(decimals, buffer); + } + const result = new BigNumber(new BigNumber(value).toFixed(dec)).toFixed(); + const resultBN = new BigNumber(result); + return resultBN.dp() <= 2 ? resultBN.toFormat(skipDecimals ? 0 : 2) : resultBN.toFormat(); +}; + +export const handleSignificantDecimalsAsNumber = (value: BigNumberish, decimals: number): string => { + return new BigNumber(new BigNumber(multiply(value, new BigNumber(10).pow(decimals))).toFixed(0)) + .dividedBy(new BigNumber(10).pow(decimals)) + .toFixed(); +}; + +/** + * @desc convert from asset BigNumber amount to native price BigNumber amount + */ +export const convertAmountToNativeAmount = (amount: BigNumberish, priceUnit: BigNumberish): string => multiply(amount, priceUnit); + +/** + * @desc convert from amount to display formatted string + */ +export const convertAmountAndPriceToNativeDisplay = ( + amount: BigNumberish, + priceUnit: BigNumberish, + nativeCurrency: keyof nativeCurrencyType, + buffer?: number, + skipDecimals = false +): { amount: string; display: string } => { + const nativeBalanceRaw = convertAmountToNativeAmount(amount, priceUnit); + const nativeDisplay = convertAmountToNativeDisplay(nativeBalanceRaw, nativeCurrency, buffer, skipDecimals); + return { + amount: nativeBalanceRaw, + display: nativeDisplay, + }; +}; + +export const convertAmountAndPriceToNativeDisplayWithThreshold = ( + amount: BigNumberish, + priceUnit: BigNumberish, + nativeCurrency: keyof nativeCurrencyType +): { amount: string; display: string } => { + const nativeBalanceRaw = convertAmountToNativeAmount(amount, priceUnit); + const nativeDisplay = convertAmountToNativeDisplayWithThreshold(nativeBalanceRaw, nativeCurrency); + return { + amount: nativeBalanceRaw, + display: nativeDisplay, + }; +}; + +/** + * @desc convert from raw amount to display formatted string + */ +export const convertRawAmountToNativeDisplay = ( + rawAmount: BigNumberish, + assetDecimals: number, + priceUnit: BigNumberish, + nativeCurrency: keyof nativeCurrencyType, + buffer?: number +) => { + const assetBalance = convertRawAmountToDecimalFormat(rawAmount, assetDecimals); + const ret = convertAmountAndPriceToNativeDisplay(assetBalance, priceUnit, nativeCurrency, buffer); + return ret; +}; + +/** + * @desc convert from raw amount to balance object + */ +export const convertRawAmountToBalance = (value: BigNumberish, asset: { decimals: number; symbol?: string }, buffer?: number) => { + const decimals = asset?.decimals ?? 18; + const assetBalance = convertRawAmountToDecimalFormat(value, decimals); + + return { + amount: assetBalance, + display: convertAmountToBalanceDisplay(assetBalance, asset, buffer), + }; +}; + +/** + * @desc convert from amount value to display formatted string + */ +export const convertAmountToBalanceDisplay = (value: BigNumberish, asset: { decimals: number; symbol?: string }, buffer?: number) => { + const decimals = asset?.decimals ?? 18; + const display = handleSignificantDecimals(value, decimals, buffer); + return `${display} ${asset?.symbol || ''}`; +}; + +/** + * @desc convert from amount to display formatted string + */ +export const convertAmountToPercentageDisplay = (value: BigNumberish, buffer?: number, skipDecimals?: boolean, decimals = 2): string => { + const display = handleSignificantDecimals(value, decimals, buffer, skipDecimals); + return `${display}%`; +}; + +/** + * @desc convert from amount to display formatted string + * with a threshold percent + */ +export const convertAmountToPercentageDisplayWithThreshold = (value: BigNumberish, decimals = 2, threshold = '0.0001'): string => { + if (lessThan(value, threshold)) { + return '< 0.01%'; + } else { + const display = new BigNumber(value).times(100).toFixed(decimals); + return `${display}%`; + } +}; + +/** + * @desc convert from bips amount to percentage format + */ +export const convertBipsToPercentage = (value: BigNumberish, decimals = 2): string => { + if (value === null) return '0'; + return new BigNumber(value || 0).shiftedBy(-2).toFixed(decimals); +}; + +/** + * @desc convert from amount value to display formatted string + */ +export const convertAmountToNativeDisplay = ( + value: BigNumberish, + nativeCurrency: keyof nativeCurrencyType, + buffer?: number, + skipDecimals?: boolean +) => { + const nativeSelected = supportedNativeCurrencies?.[nativeCurrency]; + const { decimals } = nativeSelected; + const display = handleSignificantDecimals(value, decimals, buffer, skipDecimals); + if (nativeSelected.alignment === 'left') { + return `${nativeSelected.symbol}${display}`; + } + return `${display} ${nativeSelected.symbol}`; +}; + +export const convertAmountToNativeDisplayWithThreshold = (value: BigNumberish, nativeCurrency: keyof nativeCurrencyType) => { + const nativeSelected = supportedNativeCurrencies?.[nativeCurrency]; + const display = handleSignificantDecimalsWithThreshold(value, nativeSelected.decimals, nativeSelected.decimals < 4 ? '0.01' : '0.0001'); + if (nativeSelected.alignment === 'left') { + return `${nativeSelected.symbol}${display}`; + } + return `${display} ${nativeSelected.symbol}`; +}; + +/** + * @desc convert from raw amount to decimal format + */ +export const convertRawAmountToDecimalFormat = (value: BigNumberish, decimals = 18): string => + new BigNumber(value).dividedBy(new BigNumber(10).pow(decimals)).toFixed(); + +/** + * @desc convert from decimal format to raw amount + */ +export const convertDecimalFormatToRawAmount = (value: string, decimals = 18): string => + new BigNumber(value).multipliedBy(new BigNumber(10).pow(decimals)).toFixed(0); + +export const fromWei = (number: BigNumberish): string => convertRawAmountToDecimalFormat(number, 18); diff --git a/src/__swaps__/screens/Swap/utils/strings.ts b/src/__swaps__/screens/Swap/utils/strings.ts new file mode 100644 index 00000000000..7462c0946df --- /dev/null +++ b/src/__swaps__/screens/Swap/utils/strings.ts @@ -0,0 +1,7 @@ +export const isLowerCaseMatch = (a?: string, b?: string) => a?.toLowerCase() === b?.toLowerCase(); + +export const capitalize = (s = '') => s.charAt(0).toUpperCase() + s.slice(1); + +export const truncateString = (txt = '', maxLength = 22) => { + return `${txt?.slice(0, maxLength)}${txt.length > maxLength ? '…' : ''}`; +}; diff --git a/src/__swaps__/screens/Swap/utils.ts b/src/__swaps__/screens/Swap/utils/swaps.ts similarity index 99% rename from src/__swaps__/screens/Swap/utils.ts rename to src/__swaps__/screens/Swap/utils/swaps.ts index a13acc6030b..171458fd743 100644 --- a/src/__swaps__/screens/Swap/utils.ts +++ b/src/__swaps__/screens/Swap/utils/swaps.ts @@ -1,6 +1,6 @@ import c from 'chroma-js'; import { globalColors } from '@/design-system'; -import { SCRUBBER_WIDTH, SLIDER_WIDTH } from './constants'; +import { SCRUBBER_WIDTH, SLIDER_WIDTH } from '../constants'; // /---- 🎨 Color functions 🎨 ----/ // // diff --git a/src/components/DappBrowser/WebViewBorder.tsx b/src/components/DappBrowser/WebViewBorder.tsx index 1dea4f5723d..8fcbab87274 100644 --- a/src/components/DappBrowser/WebViewBorder.tsx +++ b/src/components/DappBrowser/WebViewBorder.tsx @@ -4,7 +4,7 @@ import Animated, { interpolate, useAnimatedStyle } from 'react-native-reanimated import { Box, Cover, globalColors } from '@/design-system'; import { IS_ANDROID } from '@/env'; import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; -import { opacity } from '@/__swaps__/screens/Swap/utils'; +import { opacity } from '@/__swaps__/screens/Swap/utils/swaps'; import { useBrowserContext } from './BrowserContext'; export const WebViewBorder = ({ enabled, isActiveTab }: { enabled?: boolean; isActiveTab: boolean }) => { diff --git a/src/components/DappBrowser/address-bar/AddressInput.tsx b/src/components/DappBrowser/address-bar/AddressInput.tsx index 0975bb292f5..da9e1c49949 100644 --- a/src/components/DappBrowser/address-bar/AddressInput.tsx +++ b/src/components/DappBrowser/address-bar/AddressInput.tsx @@ -11,7 +11,7 @@ import { ToolbarIcon } from '../BrowserToolbar'; import { IS_IOS } from '@/env'; import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; -import { opacity } from '@/__swaps__/screens/Swap/utils'; +import { opacity } from '@/__swaps__/screens/Swap/utils/swaps'; import { DappBrowserShadows } from '../DappBrowserShadows'; const AnimatedBlurView = Animated.createAnimatedComponent(BlurView); diff --git a/src/components/DappBrowser/address-bar/TabButton.tsx b/src/components/DappBrowser/address-bar/TabButton.tsx index 531b5382019..985324f1056 100644 --- a/src/components/DappBrowser/address-bar/TabButton.tsx +++ b/src/components/DappBrowser/address-bar/TabButton.tsx @@ -1,5 +1,5 @@ import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; -import { opacity } from '@/__swaps__/screens/Swap/utils'; +import { opacity } from '@/__swaps__/screens/Swap/utils/swaps'; import ButtonPressAnimation from '@/components/animations/ButtonPressAnimation'; import { Box, Text, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { IS_IOS } from '@/env';