diff --git a/packages/caip/src/adapters/coingecko/index.ts b/packages/caip/src/adapters/coingecko/index.ts index f8065e6ff9b..d609a8326b1 100644 --- a/packages/caip/src/adapters/coingecko/index.ts +++ b/packages/caip/src/adapters/coingecko/index.ts @@ -4,7 +4,21 @@ import type { AssetId } from '../../assetId/assetId' import { fromAssetId } from '../../assetId/assetId' import type { ChainId } from '../../chainId/chainId' import { fromChainId, toChainId } from '../../chainId/chainId' -import { CHAIN_NAMESPACE, CHAIN_REFERENCE } from '../../constants' +import { + arbitrumChainId, + arbitrumNovaChainId, + avalancheChainId, + baseChainId, + bscChainId, + CHAIN_NAMESPACE, + CHAIN_REFERENCE, + cosmosChainId, + ethChainId, + gnosisChainId, + optimismChainId, + polygonChainId, + thorchainChainId, +} from '../../constants' import * as adapters from './generated' // https://api.coingecko.com/api/v3/asset_platforms @@ -101,6 +115,35 @@ export const chainIdToCoingeckoAssetPlatform = (chainId: ChainId): string => { } } +export const coingeckoAssetPlatformToChainId = (platform: CoingeckoAssetPlatform): ChainId => { + switch (platform) { + case CoingeckoAssetPlatform.Ethereum: + return ethChainId + case CoingeckoAssetPlatform.Avalanche: + return avalancheChainId + case CoingeckoAssetPlatform.Optimism: + return optimismChainId + case CoingeckoAssetPlatform.BnbSmartChain: + return bscChainId + case CoingeckoAssetPlatform.Polygon: + return polygonChainId + case CoingeckoAssetPlatform.Gnosis: + return gnosisChainId + case CoingeckoAssetPlatform.Arbitrum: + return arbitrumChainId + case CoingeckoAssetPlatform.ArbitrumNova: + return arbitrumNovaChainId + case CoingeckoAssetPlatform.Base: + return baseChainId + case CoingeckoAssetPlatform.Cosmos: + return cosmosChainId + case CoingeckoAssetPlatform.Thorchain: + return thorchainChainId + default: + throw new Error(`Unsupported Coingecko asset platform: ${platform}`) + } +} + export const makeCoingeckoAssetUrl = (assetId: AssetId): string | undefined => { const id = assetIdToCoingecko(assetId) if (!id) return diff --git a/src/assets/nightsky.jpg b/src/assets/nightsky.jpg deleted file mode 100644 index 89d6ba53d24..00000000000 Binary files a/src/assets/nightsky.jpg and /dev/null differ diff --git a/src/assets/splash-sidebar.jpg b/src/assets/splash-sidebar.jpg new file mode 100644 index 00000000000..c45b1e21276 Binary files /dev/null and b/src/assets/splash-sidebar.jpg differ diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 38ea6f54738..4848a7a00fb 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -287,12 +287,16 @@ }, "connectWalletPage": { "title": "The Original Multichain Exchange", + "primaryTitle": "Welcome to Multichain DeFi.", + "secondaryTitle": "Private. Decentralized. Non-custodial.", + "primaryDescription": "Trade, bridge, and earn rewards effortlessly. Experience Native Bitcoin, Dogecoin, and more. Manage Liquidity and DeFi Positions in One-Click.", + "snapDescription": "Download the ShapeShift Multichain Snap and Unlock Bitcoin, Dogecoin, and more for your MetaMask.", "shapeshift": "ShapeShift", "exploreThe": "Explore the", "defiUniverse": "DeFi Universe", "body": "Trade, bridge & earn. Private. Community owned. Non-custodial. Decentralized.", "header": "Please connect a wallet to get started", - "cta": "Connect Wallet", + "cta": "Connect or Create Wallet", "viewADemo": "View a Demo", "dontHaveWallet": "Don't have a wallet?", "welcomeBack": "Welcome back!", @@ -1544,8 +1548,6 @@ "title": "Multichain Snap needs updating", "subtitle": "Click 'Update' to continue using ShapeShift's multichain features with MetaMask!" }, - "secondaryTitle": "The best Multichain experience for MetaMask: Powered by ShapeShift", - "secondaryBody": "Send, receive, track, trade, and earn with the ShapeShift Multichain Snap on the following chains:", "connectMetaMask": "Connect MetaMask", "andMore": "...and more", "snapInstalledToast": "ShapeShift Multichain MetaMask Snap Installed", diff --git a/src/lib/coingecko/constants.ts b/src/lib/coingecko/constants.ts new file mode 100644 index 00000000000..b0adc5f43f8 --- /dev/null +++ b/src/lib/coingecko/constants.ts @@ -0,0 +1,31 @@ +import type { AssetId } from '@shapeshiftoss/caip' +import { + adapters, + baseAssetId, + bchAssetId, + bscAssetId, + btcAssetId, + cosmosAssetId, + dogeAssetId, + ethAssetId, + gnosisAssetId, + ltcAssetId, + polygonAssetId, + thorchainAssetId, +} from '@shapeshiftoss/caip' + +export const COINGECKO_NATIVE_ASSET_ID_TO_ASSET_ID: Partial> = { + bitcoin: btcAssetId, + 'bitcoin-cash': bchAssetId, + dogecoin: dogeAssetId, + litecoin: ltcAssetId, + [adapters.CoingeckoAssetPlatform.Ethereum]: ethAssetId, + [adapters.CoingeckoAssetPlatform.Thorchain]: thorchainAssetId, + [adapters.CoingeckoAssetPlatform.Gnosis]: gnosisAssetId, + [adapters.CoingeckoAssetPlatform.Cosmos]: cosmosAssetId, + // This isn't a mistake - the network and id are different in the case of MATIC/POS + 'polygon-ecosystem-token': polygonAssetId, + [adapters.CoingeckoAssetPlatform.Base]: baseAssetId, + // This isn't a mistake - the network and id are different in the case of BSC + binanceCoin: bscAssetId, +} diff --git a/src/lib/coingecko/types.ts b/src/lib/coingecko/types.ts new file mode 100644 index 00000000000..43c6754275a --- /dev/null +++ b/src/lib/coingecko/types.ts @@ -0,0 +1,80 @@ +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import type { + CoinGeckoMarketCap, + CoinGeckoMarketData, +} from 'lib/market-service/coingecko/coingecko-types' + +// Non-exhaustive types, refer to https://docs.coingecko.com/reference/coins-id and other endpoints for full response schema +export type CoingeckoAssetDetails = { + market_data: CoinGeckoMarketData + asset_platform_id: string + id: string + image: Record + name: string + symbol: string + detail_platforms: Record + platforms: Record +} + +export type MoverAsset = Pick< + CoinGeckoMarketCap, + 'id' | 'symbol' | 'name' | 'image' | 'market_cap_rank' +> & { + usd: string + usd_24h_vol: string + usd_1y_change: string +} + +export type MoversResponse = { + top_gainers: MoverAsset[] + top_losers: MoverAsset[] +} + +// Non-exhaustive +export type TrendingCoin = { + id: string + coin_id: number + name: string + symbol: string + market_cap_rank: number + thumb: string + small: string + large: string + slug: string + price_btc: number + score: number + data: { + price: number + price_btc: string + price_change_percentage_24h: Record + market_cap: string + market_cap_btc: string + total_volume: string + } +} + +export type TrendingResponse = { + coins: { + item: TrendingCoin + }[] +} + +export type RecentlyAddedCoin = { + id: string + symbol: string + name: string + activated_at: number +} + +export type RecentlyAddedResponse = RecentlyAddedCoin[] + +export type CoingeckoAsset = { + assetId: AssetId + details: CoingeckoAssetDetails +} + +export type CoingeckoList = { + byId: Record + ids: AssetId[] + chainIds: ChainId[] +} diff --git a/src/lib/coingecko/utils.ts b/src/lib/coingecko/utils.ts new file mode 100644 index 00000000000..031b7e1c458 --- /dev/null +++ b/src/lib/coingecko/utils.ts @@ -0,0 +1,120 @@ +import { adapters, ASSET_NAMESPACE, bscChainId, toAssetId } from '@shapeshiftoss/caip' +import type { CoingeckoAssetPlatform } from '@shapeshiftoss/caip/src/adapters' +import axios from 'axios' +import { queryClient } from 'context/QueryClientProvider/queryClient' +import type { CoinGeckoMarketCap } from 'lib/market-service/coingecko/coingecko-types' + +import { COINGECKO_NATIVE_ASSET_ID_TO_ASSET_ID } from './constants' +import type { + CoingeckoAsset, + CoingeckoAssetDetails, + MoverAsset, + MoversResponse, + RecentlyAddedCoin, + RecentlyAddedResponse, + TrendingCoin, + TrendingResponse, +} from './types' + +const coingeckoBaseUrl = 'https://api.proxy.shapeshift.com/api/v1/markets' + +const getCoinDetails = async ( + marketCap: CoinGeckoMarketCap | RecentlyAddedCoin | TrendingCoin | MoverAsset, + i: number, + all: CoingeckoAsset[], +) => { + try { + const { data } = await queryClient.fetchQuery({ + queryKey: ['coingecko', 'coin', marketCap.id], + // Shared query across consumers, so make it infinite as there will be a lot of overlap + queryFn: () => axios.get(`${coingeckoBaseUrl}/coins/${marketCap.id}`), + gcTime: Infinity, + staleTime: Infinity, + }) + const { asset_platform_id, id } = data + + const address = data.platforms?.[asset_platform_id] + + if (!address) return + + const assetId = (() => { + // Handles native assets, which *may* not contain a platform_id + if (COINGECKO_NATIVE_ASSET_ID_TO_ASSET_ID[id]) + return COINGECKO_NATIVE_ASSET_ID_TO_ASSET_ID[id] + + const chainId = adapters.coingeckoAssetPlatformToChainId( + asset_platform_id as CoingeckoAssetPlatform, + ) + if (!chainId) return + + const assetId = toAssetId({ + chainId, + assetNamespace: chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20, + assetReference: address, + }) + return assetId + })() + + if (!assetId) return marketCap + + all[i] = { + assetId, + details: data, + } + } catch (error) { + console.error(`Error fetching asset details for ${marketCap.id}:`, error) + return null + } +} + +export const getCoingeckoTopMovers = async (): Promise => { + const { data } = await axios.get( + `${coingeckoBaseUrl}/coins/top_gainers_losers?vs_currency=usd`, + ) + + const all: CoingeckoAsset[] = [] + + await Promise.allSettled( + data.top_gainers + .concat(data.top_losers) + .map((marketData, i) => getCoinDetails(marketData, i, all)), + ) + + return all.filter(mover => Boolean(mover.assetId)) +} + +export const getCoingeckoTrending = async (): Promise => { + const { data } = await axios.get(`${coingeckoBaseUrl}/search/trending`) + + const all: CoingeckoAsset[] = [] + + await Promise.allSettled( + data.coins.map(({ item }) => item).map((marketData, i) => getCoinDetails(marketData, i, all)), + ) + + return all.filter(mover => Boolean(mover.assetId)) +} + +export const getCoingeckoRecentlyAdded = async (): Promise => { + const { data } = await axios.get(`${coingeckoBaseUrl}/coins/list/new`) + + const all: CoingeckoAsset[] = [] + + await Promise.allSettled(data.map((marketData, i) => getCoinDetails(marketData, i, all))) + + return all.filter(mover => Boolean(mover.assetId)) +} + +export const getCoingeckoMarkets = async ( + order: 'market_cap_desc' | 'volume_desc', +): Promise => { + const { data } = await axios.get( + `${coingeckoBaseUrl}/coins/markets?vs_currency=usd&order=${order}`, + ) + + const all: CoingeckoAsset[] = [] + + await Promise.allSettled(data.map((marketData, i) => getCoinDetails(marketData, i, all))) + + return all.filter(mover => Boolean(mover.assetId)) +} diff --git a/src/pages/ConnectWallet/ConnectWallet.tsx b/src/pages/ConnectWallet/ConnectWallet.tsx index f18d213bd66..e078f71924b 100644 --- a/src/pages/ConnectWallet/ConnectWallet.tsx +++ b/src/pages/ConnectWallet/ConnectWallet.tsx @@ -1,8 +1,7 @@ -import type { ResponsiveValue } from '@chakra-ui/react' import { + Box, Button, Center, - Circle, Divider, Flex, Heading, @@ -10,16 +9,15 @@ import { Link, Stack, Tooltip, + useColorModeValue, } from '@chakra-ui/react' -import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import { KnownChainIds } from '@shapeshiftoss/types' import { knownChainIds } from 'constants/chains' import { useCallback, useEffect, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { generatePath, matchPath, useHistory } from 'react-router-dom' -import NightSky from 'assets/nightsky.jpg' +import SplashSidebar from 'assets/splash-sidebar.jpg' import { AssetIcon } from 'components/AssetIcon' -import { FoxIcon } from 'components/Icons/FoxIcon' import { MetaMaskIcon } from 'components/Icons/MetaMaskIcon' import { LanguageSelector } from 'components/LanguageSelector' import { Page } from 'components/Layout/Page' @@ -48,9 +46,7 @@ const IncludeChains = [ ] const containerPt = { base: 8, lg: 0 } -const flexAlign = { base: 'center', lg: 'flex-start' } const flexRightAlign = { base: 'center', lg: 'flex-end' } -const textAlign: ResponsiveValue = { base: 'center', lg: 'left' } const margin = { base: 0, lg: 'auto' } const spacing = { base: 6, lg: 8 } const display = { base: 'none', lg: 'flex' } @@ -68,6 +64,7 @@ export const ConnectWallet = () => { const { state, dispatch, connectDemo, connect } = useWallet() const hasWallet = Boolean(state.walletInfo?.deviceId) const isSnapEnabled = useFeatureFlag('Snaps') + const snapInfoBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50') const allNativeAssets = useMemo(() => { return knownChainIds @@ -80,17 +77,6 @@ export const ConnectWallet = () => { .filter(isSome) }, []) - const evmChains = useMemo(() => { - return knownChainIds - .filter(isEvmChainId) - .map(knownChainId => { - const assetId = getChainAdapterManager().get(knownChainId)?.getFeeAssetId()! - const asset = selectAssetById(store.getState(), assetId) - return asset - }) - .filter(isSome) - }, []) - const history = useHistory() const translate = useTranslate() const query = useQuery<{ returnUrl: string }>() @@ -129,10 +115,6 @@ export const ConnectWallet = () => { )) }, [allNativeAssets]) - const renderEvmChainText = useMemo(() => { - return evmChains.map(asset => asset.networkName).join(', ') - }, [evmChains]) - const handleConnectClick = useCallback( () => dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }), [dispatch], @@ -178,83 +160,64 @@ export const ConnectWallet = () => { maxWidth={maxWidth} width={width} > - {isMobile ? ( - - - - {translate('connectWalletPage.exploreThe')}{' '} - - {translate('connectWalletPage.defiUniverse')} - + + + + + {translate('connectWalletPage.secondaryTitle')} + + + {translate('connectWalletPage.primaryTitle')} + + + {translate('connectWalletPage.primaryDescription')} - - - - ) : isSnapEnabled ? ( - <> - - - - {translate('walletProvider.metaMaskSnap.secondaryTitle')} - - - {translate('walletProvider.metaMaskSnap.secondaryBody')} - - - {renderChains} - - - {translate('walletProvider.metaMaskSnap.andMore')} - - - - - - - - - - - - ) : ( - - - {translate('connectWalletPage.welcomeBack')} - - - {translate('connectWalletPage.welcomeBody')} - + + {isSnapEnabled && ( + <> + + + + + + + + {renderChains} + + + {translate('connectWalletPage.snapDescription')} + + + + + )} - )} - + {translate('connectWalletPage.dontHaveWallet')}