diff --git a/.env b/.env index dadb86b..e1d2cdf 100644 --- a/.env +++ b/.env @@ -7,3 +7,8 @@ VITE_FOO=bar # chat woot VITE_CHATWOOT_TOKEN=jmoXp9BPMSPEYHeJX5YKT15Q VITE_CHATWOOT_URL=https://app.chatwoot.com + +# Chainflip + +VITE_CHAINFLIP_API_KEY=09bc0796ff40435482c0a54fa6ae2784 +VITE_CHAINFLIP_API_URL=https://chainflip-broker.io diff --git a/package.json b/package.json index d46b46e..4b33caa 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "@chakra-ui/react": "^2.10.3", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", + "@lukemorales/query-key-factory": "^1.3.4", "@shapeshiftoss/caip": "^8.15.0", "@shapeshiftoss/types": "^8.6.0", + "@tanstack/react-query": "^5.65.1", "axios": "^1.6.5", "framer-motion": "^10.17.9", "match-sorter": "^6.3.1", diff --git a/src/App.tsx b/src/App.tsx index f070c74..d9d47fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,15 @@ import './App.css' import { Center } from '@chakra-ui/react' +import { QueryClientProvider } from '@tanstack/react-query' import { createBrowserRouter, RouterProvider } from 'react-router-dom' import { ChatwootButton } from 'components/Chatwoot' import { SelectPair } from 'components/SelectPair' import { Status } from 'components/Status/Status' import { TradeInput } from 'components/TradeInput' +import { queryClient } from './config/react-query' + const router = createBrowserRouter([ { path: '/', @@ -26,10 +29,12 @@ function App() { console.log(import.meta.env.VITE_FOO) return ( -
- - -
+ +
+ + +
+
) } diff --git a/src/config/react-query.ts b/src/config/react-query.ts new file mode 100644 index 0000000..9f69260 --- /dev/null +++ b/src/config/react-query.ts @@ -0,0 +1,3 @@ +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() diff --git a/src/constants/caip.ts b/src/constants/caip.ts new file mode 100644 index 0000000..9a7c928 --- /dev/null +++ b/src/constants/caip.ts @@ -0,0 +1,60 @@ +// Subset of web caip constants with Chainflip-supported assets only +// We need to redeclare things here as @shapeshiftoss/caip is actually a monorepo project and not published on npm + +import type { AssetId, ChainId } from '@shapeshiftoss/caip' + +export const btcAssetId: AssetId = 'bip122:000000000019d6689c085ae165831e93/slip44:0' + +export const ethAssetId: AssetId = 'eip155:1/slip44:60' +export const arbitrumAssetId: AssetId = 'eip155:42161/slip44:60' +export const solAssetId: AssetId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501' +export const wrappedSolAssetId: AssetId = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:So11111111111111111111111111111111111111112' +export const usdtAssetId: AssetId = 'eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7' +export const usdcAssetId: AssetId = 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' +export const usdcOnArbitrumOneAssetId: AssetId = + 'eip155:42161/erc20:0xaf88d065e77c8cc2239327c5edb3a432268e5831' +export const usdcOnSolanaAssetId: AssetId = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' +export const flipAssetId: AssetId = 'eip155:1/erc20:0x826180541412d574cf1336d22c0c0a287822678a' + +export const btcChainId: ChainId = 'bip122:000000000019d6689c085ae165831e93' + +export const ethChainId: ChainId = 'eip155:1' +export const arbitrumChainId: ChainId = 'eip155:42161' +export const baseChainId: ChainId = 'eip155:8453' + +export const solanaChainId: ChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' + +export const CHAIN_NAMESPACE = { + Evm: 'eip155', + Utxo: 'bip122', + CosmosSdk: 'cosmos', + Solana: 'solana', +} as const + +export const CHAIN_REFERENCE = { + EthereumMainnet: '1', + BitcoinMainnet: '000000000019d6689c085ae165831e93', + ArbitrumMainnet: '42161', + ArbitrumNovaMainnet: '42170', + BaseMainnet: '8453', + SolanaMainnet: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', +} as const + +export const ASSET_NAMESPACE = { + erc20: 'erc20', + erc721: 'erc721', + erc1155: 'erc1155', + slip44: 'slip44', + splToken: 'token', +} as const + +export const ASSET_REFERENCE = { + Bitcoin: '0', + Ethereum: '60', + Arbitrum: '60', // evm chain which uses ethereum derivation path as common practice + Solana: '501', +} as const + +export const FEE_ASSET_IDS = [ethAssetId, btcAssetId, arbitrumAssetId, solAssetId] diff --git a/src/env.d.ts b/src/env.d.ts index 2278f39..b37958a 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -7,6 +7,8 @@ interface ImportMetaEnv { readonly VITE_CHATWOOT_URL: string readonly VITE_CHATWOOT_TOKEN: string readonly VITE_FEATURE_CHATWOOT: boolean + readonly VITE_CHAINFLIP_API_KEY: string + readonly VITE_CHAINFLIP_API_URL: string } interface ImportMeta { diff --git a/src/queries/chainflip/assets.test.ts b/src/queries/chainflip/assets.test.ts new file mode 100644 index 0000000..945faf3 --- /dev/null +++ b/src/queries/chainflip/assets.test.ts @@ -0,0 +1,91 @@ +import { btcAssetId, ethAssetId, usdcOnSolanaAssetId } from 'constants/caip' +import { describe, expect, it } from 'vitest' + +import { transformChainflipAssets } from './assets' +import type { ChainflipAssetsResponse } from './types' + +describe('transformChainflipAssets', () => { + it('should transform multiple chainflip assets to assetIds', () => { + const mockResponse: ChainflipAssetsResponse = { + assets: [ + { + id: 'btc.btc', + direction: 'both', + ticker: 'BTC', + name: 'Bitcoin', + network: 'Bitcoin', + networkLogo: '/networks/btc/logo.svg', + assetLogo: '/assets/btc.btc/logo.svg', + decimals: 8, + minimalAmount: 0.0007, + minimalAmountNative: '70000', + usdPrice: 102680.1024594243, + usdPriceNative: '102680102459', + }, + { + id: 'eth.eth', + direction: 'both', + ticker: 'ETH', + name: 'Ethereum', + network: 'Ethereum', + networkLogo: '/networks/eth/logo.svg', + assetLogo: '/assets/eth.eth/logo.svg', + decimals: 18, + minimalAmount: 0.01, + minimalAmountNative: '10000000000000000', + usdPrice: 3175.43595522135, + usdPriceNative: '3175435955', + }, + ], + } + + expect(transformChainflipAssets(mockResponse)).toEqual([btcAssetId, ethAssetId]) + }) + + it('should transform single chainflip asset to assetId', () => { + const mockResponse: ChainflipAssetsResponse = { + assets: [ + { + id: 'usdc.sol', + direction: 'both', + ticker: 'USDC', + name: 'solUSDC', + network: 'Solana', + contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + networkLogo: '/networks/sol/logo.svg', + assetLogo: '/assets/usdc.sol/logo.svg', + decimals: 6, + minimalAmount: 10, + minimalAmountNative: '10000000', + usdPrice: 1.0014781977, + usdPriceNative: '1001478', + }, + ], + } + + expect(transformChainflipAssets(mockResponse)).toEqual([usdcOnSolanaAssetId]) + }) + + it('should return empty array for unsupported assets', () => { + const mockResponse: ChainflipAssetsResponse = { + assets: [ + { + id: 'unsupported.asset', + direction: 'both', + ticker: 'UNSUPPORTED', + name: 'Unsupported Asset', + network: 'Unknown', + networkLogo: '/networks/unknown/logo.svg', + assetLogo: '/assets/unknown.asset/logo.svg', + decimals: 18, + minimalAmount: 1, + minimalAmountNative: '1000000000000000000', + usdPrice: 1, + usdPriceNative: '1000000000000000000', + }, + ], + } + + expect(transformChainflipAssets(mockResponse)).toEqual([]) + }) +}) diff --git a/src/queries/chainflip/assets.ts b/src/queries/chainflip/assets.ts new file mode 100644 index 0000000..de6db1e --- /dev/null +++ b/src/queries/chainflip/assets.ts @@ -0,0 +1,39 @@ +import { useQuery } from '@tanstack/react-query' +import { + arbitrumAssetId, + btcAssetId, + ethAssetId, + flipAssetId, + solAssetId, + usdcAssetId, + usdcOnArbitrumOneAssetId, + usdcOnSolanaAssetId, + usdtAssetId, +} from 'constants/caip' + +import { reactQueries } from '../react-queries' +import type { ChainflipAssetsResponse } from './types' + +// Map Chainflip internal asset IDs to CAIPs +const chainflipToAssetId: Record = { + 'btc.btc': btcAssetId, + 'eth.arb': arbitrumAssetId, + 'eth.eth': ethAssetId, + 'flip.eth': flipAssetId, + 'sol.sol': solAssetId, + 'usdc.arb': usdcOnArbitrumOneAssetId, + 'usdc.eth': usdcAssetId, + 'usdc.sol': usdcOnSolanaAssetId, + 'usdt.eth': usdtAssetId, +} + +export const transformChainflipAssets = (data: ChainflipAssetsResponse) => { + return data.assets.map(asset => chainflipToAssetId[asset.id]).filter(Boolean) +} + +export const useChainflipAssetsQuery = () => { + return useQuery({ + ...reactQueries.chainflip.assets, + select: transformChainflipAssets, + }) +} diff --git a/src/queries/chainflip/quote.ts b/src/queries/chainflip/quote.ts new file mode 100644 index 0000000..5b76f92 --- /dev/null +++ b/src/queries/chainflip/quote.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query' + +import { reactQueries } from '../react-queries' +import type { ChainflipQuoteParams } from './types' + +export const useChainflipQuoteQuery = (params: ChainflipQuoteParams) => { + return useQuery({ + ...reactQueries.chainflip.quote(params), + }) +} diff --git a/src/queries/chainflip/status.ts b/src/queries/chainflip/status.ts new file mode 100644 index 0000000..f2b5271 --- /dev/null +++ b/src/queries/chainflip/status.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query' + +import { reactQueries } from '../react-queries' + +type ChainflipStatusQueryParams = { + swapId: number + enabled?: boolean +} + +export const useChainflipStatusQuery = ({ swapId, enabled = true }: ChainflipStatusQueryParams) => { + return useQuery({ + ...reactQueries.chainflip.status(swapId), + refetchInterval: 15000, + enabled, + }) +} diff --git a/src/queries/chainflip/swap.ts b/src/queries/chainflip/swap.ts new file mode 100644 index 0000000..cae8ab1 --- /dev/null +++ b/src/queries/chainflip/swap.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query' + +import { reactQueries } from '../react-queries' +import type { ChainflipSwapParams } from './types' + +export const useChainflipSwapQuery = (params: ChainflipSwapParams) => { + return useQuery({ + ...reactQueries.chainflip.swap(params), + }) +} diff --git a/src/queries/chainflip/types.ts b/src/queries/chainflip/types.ts new file mode 100644 index 0000000..ae89630 --- /dev/null +++ b/src/queries/chainflip/types.ts @@ -0,0 +1,109 @@ +export type ChainflipAsset = { + id: string + direction: 'both' | 'ingress' | 'egress' + ticker: string + name: string + network: string + contractAddress?: string + networkLogo: string + assetLogo: string + decimals: number + minimalAmount: number + minimalAmountNative: string + usdPrice: number + usdPriceNative: string +} + +export type ChainflipAssetsResponse = { + assets: ChainflipAsset[] +} + +export type ChainflipQuoteFee = { + type: 'ingress' | 'network' | 'broker' | 'egress' | 'liquidity' + asset: string + amount: number + amountNative: string +} + +export type ChainflipPoolInfo = { + baseAsset: string + quoteAsset: string + fee: { + asset: string + amount: number + amountNative: string + } +} + +export type ChainflipQuote = { + type: 'regular' | 'dca' + ingressAsset: string + ingressAmount: number + ingressAmountNative: string + intermediateAsset: string | null + intermediateAmount: number | null + intermediateAmountNative: string | null + egressAsset: string + egressAmount: number + egressAmountNative: string + includedFees: ChainflipQuoteFee[] + recommendedSlippageTolerancePercent: number + lowLiquidityWarning: boolean + poolInfo: ChainflipPoolInfo[] + estimatedDurationSeconds: number + estimatedDurationsSeconds: { + deposit: number + swap: number + egress: number + } + estimatedPrice: number + chunkIntervalBlocks?: number | null + numberOfChunks?: number | null + boostQuote?: ChainflipQuote +} + +export type ChainflipQuoteParams = { + sourceAsset: string + destinationAsset: string + amount: string + commissionBps?: number +} + +export type ChainflipSwapResponse = { + id: number + address: string + issuedBlock: number + network: string + channelId: number + sourceExpiryBlock: number + explorerUrl: string + channelOpeningFee: number + channelOpeningFeeNative: string +} + +export type ChainflipSwapParams = { + sourceAsset: string + destinationAsset: string + destinationAddress: string + minimumPrice: string + refundAddress: string + maxBoostFee?: number + retryDurationInBlocks?: number + commissionBps?: number + numberOfChunks?: number + chunkIntervalBlocks?: number +} + +export type ChainflipSwapEgress = { + transactionReference?: string +} + +export type ChainflipSwapStatus = { + state: 'waiting' | 'receiving' | 'swapping' | 'sending' | 'sent' | 'completed' | 'failed' + swapEgress?: ChainflipSwapEgress +} + +export type ChainflipStatusResponse = { + id: number + status: ChainflipSwapStatus +} diff --git a/src/queries/marketData/index.test.ts b/src/queries/marketData/index.test.ts new file mode 100644 index 0000000..9d9a196 --- /dev/null +++ b/src/queries/marketData/index.test.ts @@ -0,0 +1,111 @@ +import axios from 'axios' +import { btcAssetId, ethAssetId } from 'constants/caip' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { findByAssetId } from '.' + +vi.mock('axios') + +describe('coingecko queries', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('findByAssetId', () => { + it('should return market data for ETH', async () => { + const result = { + price: '3611.19', + marketCap: '424970837706', + changePercent24Hr: 2.19682, + volume: '21999495657', + supply: '120839129.44', + } + + const market_data = { + current_price: { + usd: Number(result.price), + }, + market_cap: { + usd: Number(result.marketCap), + }, + price_change_percentage_24h: result.changePercent24Hr, + total_volume: { + usd: Number(result.volume), + }, + circulating_supply: Number(result.supply), + max_supply: null, + total_supply: null, + } + + vi.mocked(axios.get).mockResolvedValue({ data: { market_data } }) + expect(await findByAssetId(ethAssetId)).toEqual(result) + }) + + it('should return market data with maxSupply for BTC', async () => { + const result = { + price: '54810', + marketCap: '1032270421549', + changePercent24Hr: -0.33384, + volume: '38267223547', + supply: '18840237', + maxSupply: '21000000', + } + + const market_data = { + current_price: { + usd: Number(result.price), + }, + market_cap: { + usd: Number(result.marketCap), + }, + price_change_percentage_24h: result.changePercent24Hr, + total_volume: { + usd: Number(result.volume), + }, + circulating_supply: Number(result.supply), + max_supply: Number(result.maxSupply), + total_supply: Number(result.maxSupply), + } + + vi.mocked(axios.get).mockResolvedValue({ data: { market_data } }) + expect(await findByAssetId(btcAssetId)).toEqual(result) + }) + + it('should return null if asset not found on coingecko', async () => { + const result = await findByAssetId('invalid-asset-id') + expect(result).toBeNull() + }) + + it('should throw on network error', async () => { + vi.mocked(axios.get).mockRejectedValue(new Error()) + await expect(findByAssetId(ethAssetId)).rejects.toThrow( + 'CoinGeckoMarketService(findByAssetId): error fetching market data', + ) + }) + + it('should return null if market data is missing', async () => { + vi.mocked(axios.get).mockResolvedValue({ data: {} }) + const result = await findByAssetId(ethAssetId) + expect(result).toBeNull() + }) + + it('should handle missing optional fields', async () => { + const result = { + price: '0', + marketCap: '0', + changePercent24Hr: 0, + supply: '0', + } + + const market_data = { + current_price: {}, + market_cap: {}, + price_change_percentage_24h: 0, + circulating_supply: 0, + } + + vi.mocked(axios.get).mockResolvedValue({ data: { market_data } }) + expect(await findByAssetId(ethAssetId)).toEqual(result) + }) + }) +}) diff --git a/src/queries/marketData/index.ts b/src/queries/marketData/index.ts new file mode 100644 index 0000000..57b900c --- /dev/null +++ b/src/queries/marketData/index.ts @@ -0,0 +1,63 @@ +import { useQuery } from '@tanstack/react-query' +import axios from 'axios' +import { + arbitrumAssetId, + btcAssetId, + ethAssetId, + flipAssetId, + solAssetId, + usdcAssetId, + usdcOnArbitrumOneAssetId, + usdcOnSolanaAssetId, + usdtAssetId, +} from 'constants/caip' + +import { reactQueries } from '../react-queries' +import type { CoinGeckoAssetData, MarketData } from './types' + +const COINGECKO_BASE_URL = 'https://api.proxy.shapeshift.com/api/v1/markets' + +// i.e supported assets on Chainflip only +const ASSET_ID_TO_COINGECKO_ID: Record = { + [btcAssetId]: 'bitcoin', + [ethAssetId]: 'ethereum', + [flipAssetId]: 'chainflip', + [usdcAssetId]: 'usd-coin', + [usdtAssetId]: 'tether', + [arbitrumAssetId]: 'ethereum', + [usdcOnArbitrumOneAssetId]: 'usd-coin', + [solAssetId]: 'solana', + [usdcOnSolanaAssetId]: 'usd-coin', +} + +export const findByAssetId = async (assetId: string): Promise => { + const coingeckoId = ASSET_ID_TO_COINGECKO_ID[assetId] + if (!coingeckoId) return null + + try { + const { data } = await axios.get( + `${COINGECKO_BASE_URL}/coins/${coingeckoId}`, + ) + + const marketData = data?.market_data + if (!marketData) return null + + return { + price: marketData.current_price?.['usd']?.toString() ?? '0', + marketCap: marketData.market_cap?.['usd']?.toString() ?? '0', + volume: marketData.total_volume?.['usd']?.toString(), + changePercent24Hr: marketData.price_change_percentage_24h, + supply: marketData.circulating_supply.toString(), + maxSupply: marketData.max_supply?.toString() ?? marketData.total_supply?.toString(), + } + } catch (e) { + console.warn(e) + throw new Error('CoinGeckoMarketService(findByAssetId): error fetching market data') + } +} + +export const useMarketDataByAssetIdQuery = (assetId: string) => { + return useQuery({ + ...reactQueries.marketData.byAssetId(assetId), + }) +} diff --git a/src/queries/marketData/types.ts b/src/queries/marketData/types.ts new file mode 100644 index 0000000..b821a62 --- /dev/null +++ b/src/queries/marketData/types.ts @@ -0,0 +1,24 @@ +export type CoinGeckoMarketData = { + current_price: Record + market_cap: Record + total_volume?: Record + high_24h?: Record + low_24h?: Record + circulating_supply: number + total_supply?: number + max_supply: number + price_change_percentage_24h: number +} + +export type CoinGeckoAssetData = { + market_data: CoinGeckoMarketData +} + +export type MarketData = { + price: string + marketCap: string + volume?: string + changePercent24Hr: number + supply: string + maxSupply?: string +} diff --git a/src/queries/queryKeys.ts b/src/queries/queryKeys.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/queries/react-queries.ts b/src/queries/react-queries.ts new file mode 100644 index 0000000..715792b --- /dev/null +++ b/src/queries/react-queries.ts @@ -0,0 +1,80 @@ +import type { inferQueryKeyStore } from '@lukemorales/query-key-factory' +import { createQueryKeyStore } from '@lukemorales/query-key-factory' +import axios from 'axios' + +import type { + ChainflipAssetsResponse, + ChainflipQuote, + ChainflipQuoteParams, + ChainflipStatusResponse, + ChainflipSwapParams, + ChainflipSwapResponse, +} from './chainflip/types' +import { findByAssetId } from './marketData' + +const CHAINFLIP_API_URL = import.meta.env.VITE_CHAINFLIP_API_URL +const CHAINFLIP_API_KEY = import.meta.env.VITE_CHAINFLIP_API_KEY + +export const reactQueries = createQueryKeyStore({ + chainflip: { + assets: { + queryKey: null, + queryFn: async () => { + const { data } = await axios.get(`${CHAINFLIP_API_URL}/assets`) + return data + }, + }, + quote: (params: ChainflipQuoteParams) => ({ + queryKey: [params], + queryFn: async () => { + const { data } = await axios.get(`${CHAINFLIP_API_URL}/quotes-native`, { + params: { + apiKey: CHAINFLIP_API_KEY, + sourceAsset: params.sourceAsset, + destinationAsset: params.destinationAsset, + amount: params.amount, + ...(params.commissionBps && { commissionBps: params.commissionBps }), + }, + }) + return data[0] + }, + }), + swap: (params: ChainflipSwapParams) => ({ + queryKey: [params], + queryFn: async () => { + const { data } = await axios.get(`${CHAINFLIP_API_URL}/swap`, { + params: { + apiKey: CHAINFLIP_API_KEY, + ...params, + boostFee: params.maxBoostFee ?? 0, + retryDurationInBlocks: params.retryDurationInBlocks ?? 150, + }, + }) + return data + }, + }), + status: (swapId: number) => ({ + queryKey: [swapId], + queryFn: async () => { + const { data } = await axios.get( + `${CHAINFLIP_API_URL}/status-by-id`, + { + params: { + apiKey: CHAINFLIP_API_KEY, + swapId, + }, + }, + ) + return data + }, + }), + }, + marketData: { + byAssetId: (assetId: string) => ({ + queryKey: [assetId], + queryFn: () => findByAssetId(assetId), + }), + }, +}) + +export type QueryKeys = inferQueryKeyStore diff --git a/yarn.lock b/yarn.lock index 6a2afa6..714353f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2365,6 +2365,16 @@ __metadata: languageName: node linkType: hard +"@lukemorales/query-key-factory@npm:^1.3.4": + version: 1.3.4 + resolution: "@lukemorales/query-key-factory@npm:1.3.4" + peerDependencies: + "@tanstack/query-core": ">= 4.0.0" + "@tanstack/react-query": ">= 4.0.0" + checksum: 10c0/d4e829aad970c159e3e3d6545f4f1e47d1a02621cd692bc0e13b7cc36861541e205f801d7ab8081ed8b1c8c3fd365483e1656537ef1ab165cb14743b4b1f1d6c + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -2753,6 +2763,24 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:5.65.0": + version: 5.65.0 + resolution: "@tanstack/query-core@npm:5.65.0" + checksum: 10c0/8c957082819dc90aa162256e0cc9d7d33e90f4ba9a55ec788ef5ec36bbb2b1863663dda594f9b6eb820ec9855f8d6aecd01324a020d16b58ad012c99270fe989 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^5.65.1": + version: 5.65.1 + resolution: "@tanstack/react-query@npm:5.65.1" + dependencies: + "@tanstack/query-core": "npm:5.65.0" + peerDependencies: + react: ^18 || ^19 + checksum: 10c0/0b0ed414c59ee1d7a5a8e72d2e2f2513f7367b59ee33d5663b114fcf5108438786b9bbbd12173ae7f8124538f313cd4dff942c7080b60d26d011c18918b1d563 + languageName: node + linkType: hard + "@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -7311,8 +7339,10 @@ __metadata: "@chakra-ui/react": "npm:^2.10.3" "@emotion/react": "npm:^11.11.3" "@emotion/styled": "npm:^11.11.0" + "@lukemorales/query-key-factory": "npm:^1.3.4" "@shapeshiftoss/caip": "npm:^8.15.0" "@shapeshiftoss/types": "npm:^8.6.0" + "@tanstack/react-query": "npm:^5.65.1" "@types/inquirer": "npm:^9.0.7" "@types/mixpanel-browser": "npm:^2.48.1" "@types/node": "npm:^20.10.7"