diff --git a/examples/provide-liquidity/api/apr.ts b/examples/provide-liquidity/api/apr.ts deleted file mode 100644 index 467d538fd..000000000 --- a/examples/provide-liquidity/api/apr.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { handleError } from './tokens'; - -export const getSuperfluidApr = async (): Promise => { - const url = 'https://api-osmosis.imperator.co/apr/v2/all'; - return fetch(url) - .then(handleError) - .then((res) => res.json()) - .then((res) => { - const superfluidApr = res - .find((apr: { apr_list: any[] }) => - apr.apr_list.find((a) => a.symbol === 'ECH') - ) - .apr_list.find( - (a: { symbol: string }) => a.symbol === 'ECH' - ).apr_superfluid; - - return superfluidApr; - }); -}; diff --git a/examples/provide-liquidity/api/fees.ts b/examples/provide-liquidity/api/fees.ts deleted file mode 100644 index d72fee7e6..000000000 --- a/examples/provide-liquidity/api/fees.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Fee } from '../components'; -import { handleError } from './tokens'; - -export const getFees = async (): Promise => { - const url = 'https://api-osmosis.imperator.co/fees/v1/pools'; - return fetch(url) - .then(handleError) - .then((res) => res.json()) - .then((res) => res.data); -}; diff --git a/examples/provide-liquidity/api/index.ts b/examples/provide-liquidity/api/index.ts deleted file mode 100644 index 521e52804..000000000 --- a/examples/provide-liquidity/api/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './apr'; -export * from './fees'; -export * from './tokens'; -export * from './prices'; -export * from './rewards'; diff --git a/examples/provide-liquidity/api/prices.ts b/examples/provide-liquidity/api/prices.ts deleted file mode 100644 index 3a1828332..000000000 --- a/examples/provide-liquidity/api/prices.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { handleError } from './tokens'; - -type CoinGeckoId = string; -type CoinGeckoUSD = { usd: number }; -type CoinGeckoUSDResponse = Record; - -export const getPrices = async ( - geckoIds: string[] -): Promise => { - const url = `https://api.coingecko.com/api/v3/simple/price?ids=${geckoIds.join()}&vs_currencies=usd`; - - return fetch(url) - .then(handleError) - .then((res) => res.json()); -}; diff --git a/examples/provide-liquidity/api/rewards.ts b/examples/provide-liquidity/api/rewards.ts deleted file mode 100644 index 444c1744d..000000000 --- a/examples/provide-liquidity/api/rewards.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { handleError } from './tokens'; - -type PoolReward = { - day_usd: number; - month_usd: number; - year_usd: number; -}; - -type Rewards = { - pools: { - [key: number]: PoolReward; - }; - total_day_usd: number; - total_month_usd: number; - total_year_usd: number; -}; - -export const getRewards = async (address: string): Promise => { - const url = `https://api-osmosis-chain.imperator.co/lp/v1/rewards/estimation/${address}`; - return fetch(url) - .then(handleError) - .then((res) => res.json()); -}; diff --git a/examples/provide-liquidity/api/tokens.ts b/examples/provide-liquidity/api/tokens.ts deleted file mode 100644 index de31cb9b9..000000000 --- a/examples/provide-liquidity/api/tokens.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type Token = { - price: number; - denom: string; - symbol: string; - liquidity: number; - volume_24h: number; - volume_24h_change: number; - name: string; - price_24h_change: number; - price_7d_change: number; - exponent: number; - display: string; -}; - -export const handleError = (resp: Response) => { - if (!resp.ok) throw Error(resp.statusText); - return resp; -}; - -export const getTokens = async (): Promise => { - const url = 'https://api-osmosis.imperator.co/tokens/v2/all'; - return fetch(url) - .then(handleError) - .then((res) => res.json()); -}; diff --git a/examples/provide-liquidity/components/index.tsx b/examples/provide-liquidity/components/index.tsx index bab37c333..e9e859cb6 100644 --- a/examples/provide-liquidity/components/index.tsx +++ b/examples/provide-liquidity/components/index.tsx @@ -1,4 +1,3 @@ -export * from './types'; export * from './pools'; export * from './wallet'; export * from './common'; diff --git a/examples/provide-liquidity/components/pools/PoolCard.tsx b/examples/provide-liquidity/components/pools/PoolCard.tsx index 4bff2dc57..ba23c50db 100644 --- a/examples/provide-liquidity/components/pools/PoolCard.tsx +++ b/examples/provide-liquidity/components/pools/PoolCard.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Box, - Divider, Flex, Text, Image, @@ -9,11 +8,11 @@ import { useMediaQuery, useColorModeValue, } from '@chakra-ui/react'; -import { Pool } from './ProvideLiquidity'; import { getLogoUrlFromDenom } from './PoolList'; import BigNumber from 'bignumber.js'; import { truncDecimals } from './modals/PoolDetailModal'; -import { getSymbolForDenom } from '../../utils'; +import { getSymbolForDenom, ExtendedPool } from '@/utils'; +import { PoolApr } from '@/hooks'; const formatNumber = (number: number | string) => { const formatter = Intl.NumberFormat('en', { @@ -63,11 +62,13 @@ export const ChainLogo = ({ const PoolCard = ({ pool, setPool, + poolApr, openPoolDetailModal, isFetchingApr, }: { - pool: Pool; - setPool: (pool: Pool) => void; + pool: ExtendedPool; + poolApr: PoolApr | undefined; + setPool: (pool: ExtendedPool) => void; openPoolDetailModal: () => void; isFetchingApr: boolean; }) => { @@ -125,7 +126,7 @@ const PoolCard = ({ gap="2px" > - {truncDecimals(pool.apr['14'].totalApr, 2)} + {truncDecimals(poolApr?.['14'].totalApr, 2)} % @@ -149,20 +150,6 @@ const PoolCard = ({ 7D Fees ${pool.fees7D.toLocaleString()} - {/* */} - {/* - Your Liquidity - $1329.32 - - - Bonded - $600.00 - */} ); }; diff --git a/examples/provide-liquidity/components/pools/PoolList.tsx b/examples/provide-liquidity/components/pools/PoolList.tsx index d28d4a15e..3c250ce7f 100644 --- a/examples/provide-liquidity/components/pools/PoolList.tsx +++ b/examples/provide-liquidity/components/pools/PoolList.tsx @@ -26,10 +26,14 @@ import { useColorModeValue, } from '@chakra-ui/react'; import { SlOptionsVertical } from 'react-icons/sl'; -import { Pool } from './ProvideLiquidity'; import BigNumber from 'bignumber.js'; import { truncDecimals } from './modals/PoolDetailModal'; -import { getOsmoAssetByDenom, getSymbolForDenom } from '../../utils'; +import { + getOsmoAssetByDenom, + getSymbolForDenom, + ExtendedPool, +} from '../../utils'; +import { PoolsApr } from '@/hooks'; export const getLogoUrlFromDenom = (denom: string | undefined) => { if (!denom) return ''; @@ -72,7 +76,13 @@ export const ChainLogo = ({ ); }; -const PoolName = ({ isMyPools, pool }: { isMyPools: boolean; pool: Pool }) => { +const PoolName = ({ + isMyPools, + pool, +}: { + isMyPools: boolean; + pool: ExtendedPool; +}) => { const myPoolsColor = useColorModeValue('#2C3137', '#EEF2F8'); const allPoolsColor = useColorModeValue('#697584', '#A7B4C2'); const poolIdColor = useColorModeValue('#697584', '#A7B4C2'); @@ -103,7 +113,7 @@ const ChainLogoGroup = ({ style, logoWidth, }: { - pool: Pool; + pool: ExtendedPool; logoWidth: number; style?: React.CSSProperties; }) => { @@ -174,7 +184,7 @@ const MenuPopover = ({ handleRemoveLiquidityClick, handleViewDetailClick, }: { - pool: Pool; + pool: ExtendedPool; handleAddLiquidityClick: () => void; handleRemoveLiquidityClick: () => void; handleViewDetailClick: () => void; @@ -257,12 +267,14 @@ const PoolList = ({ openPoolDetailModal, isFetchingApr, openModals, + poolsApr, }: { - pools: Pool[]; + pools: ExtendedPool[]; isMyPools?: boolean; - setPool: (pool: Pool) => void; + setPool: (pool: ExtendedPool) => void; openPoolDetailModal: () => void; isFetchingApr: boolean; + poolsApr: PoolsApr | undefined; openModals: { onAddLiquidityOpen: () => void; onRemoveLiquidityOpen: () => void; @@ -286,7 +298,7 @@ const PoolList = ({ const hasMultiTokens = pools.some(({ poolAssets }) => poolAssets.length > 2); const [isMobile] = useMediaQuery('(max-width: 780px)'); - const transformData = (isMyPools: boolean, pool: Pool) => { + const transformData = (isMyPools: boolean, pool: ExtendedPool) => { const dataSource = isMyPools ? [pool.myLiquidity, pool.bonded] : [pool.volume24H, pool.fees7D]; @@ -343,7 +355,12 @@ const PoolList = ({ h="100%" > @@ -451,7 +468,10 @@ const PoolList = ({ isMyPools ? myPoolsStatColor : allPoolsStatColor } > - {truncDecimals(pool.apr['14'].totalApr, 2) + '%'} + {truncDecimals( + poolsApr?.[pool.denom]?.['14'].totalApr, + 2 + ) + '%'} diff --git a/examples/provide-liquidity/components/pools/PoolsOverview.tsx b/examples/provide-liquidity/components/pools/PoolsOverview.tsx index 7c1a44e11..ea100c90a 100644 --- a/examples/provide-liquidity/components/pools/PoolsOverview.tsx +++ b/examples/provide-liquidity/components/pools/PoolsOverview.tsx @@ -8,14 +8,13 @@ import { Box, useColorModeValue, } from '@chakra-ui/react'; -import { useChain, useManager } from '@cosmos-kit/react'; -import { EpochInfo } from 'osmojs/dist/codegen/osmosis/epochs/genesis'; -import { ReactElement, useCallback, useEffect, useRef, useState } from 'react'; -import { defaultChainName } from '../../config'; +import { useManager } from '@cosmos-kit/react'; +import { ReactElement, useEffect, useState } from 'react'; +import { coin, defaultChainName } from '@/config'; import { RewardText } from './modals/ModalComponents'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; -import { osmosis } from 'osmojs'; +import { useEpochs, usePrices } from '@/hooks'; dayjs.extend(duration); @@ -51,56 +50,39 @@ const StatBox = ({ }; export const PoolsOverview = ({ - osmoPrice, - totalRewardPerDay, + totalDayReward, }: { - osmoPrice: string | number; - totalRewardPerDay: number; + totalDayReward: number; }) => { const [countdown, setCountdown] = useState(['00', '00', '00']); - const isMountedRef = useRef(false); + const { epochs, updateEpochs } = useEpochs(); + const { data: prices } = usePrices(); - const { getRpcEndpoint } = useChain(defaultChainName); const { getChainLogo } = useManager(); - const [isMobile] = useMediaQuery('(max-width: 660px)'); - const getEpoch = useCallback(async () => { - let rpcEndpoint = await getRpcEndpoint(); - - if (!rpcEndpoint) { - console.log('no rpc endpoint — using a fallback'); - rpcEndpoint = `https://rpc.cosmos.directory/${defaultChainName}`; - } - - const client = await osmosis.ClientFactory.createRPCQueryClient({ - rpcEndpoint, - }); - - const { epochs } = await client.osmosis.epochs.v1beta1.epochInfos(); - - const currentEpoch = epochs.find( - (epoch) => epoch.identifier === 'day' - ) as EpochInfo; + useEffect(() => { + if (!epochs) return; + const currentEpoch = epochs.find((epoch) => epoch.identifier === 'day')!; const startTime = currentEpoch.currentEpochStartTime; const duration = Number(currentEpoch.duration?.seconds) || 60 * 60 * 24; const endTime = dayjs(startTime).add(duration, 'second'); - const countdownInterval = setInterval(async () => { - if (dayjs().isAfter(endTime)) clearInterval(countdownInterval); + const countdownInterval = setInterval(() => { + if (dayjs().isAfter(endTime)) { + clearInterval(countdownInterval); + updateEpochs(); + } const leftTime = dayjs.duration(endTime.diff(dayjs())).format('HH:mm:ss'); setCountdown(leftTime.split(':')); }, 1000); - }, [getRpcEndpoint]); - useEffect(() => { - if (!isMountedRef.current) { - getEpoch(); - isMountedRef.current = true; - } - }, [getEpoch]); + return () => clearInterval(countdownInterval); + }, [epochs, updateEpochs]); + + const osmoPrice = prices?.[coin.base] || 0; const titleColor = useColorModeValue('#697584', '#A7B4C2'); const amountColor = useColorModeValue('#2C3137', '#EEF2F8'); @@ -183,7 +165,7 @@ export const PoolsOverview = ({ > Currently earning - + diff --git a/examples/provide-liquidity/components/pools/ProvideLiquidity.tsx b/examples/provide-liquidity/components/pools/ProvideLiquidity.tsx index 5710c5b27..4b92aaee8 100644 --- a/examples/provide-liquidity/components/pools/ProvideLiquidity.tsx +++ b/examples/provide-liquidity/components/pools/ProvideLiquidity.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useChain } from '@cosmos-kit/react'; +import React, { useState } from 'react'; import { Box, Center, @@ -13,11 +12,6 @@ import { useDisclosure, } from '@chakra-ui/react'; import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'; -import { Pool as OsmosisPool } from 'osmojs/dist/codegen/osmosis/gamm/pool-models/balancer/balancerPool'; -import { Asset } from '@chain-registry/types'; -import { asset_list, assets } from '@chain-registry/osmosis'; -import BigNumber from 'bignumber.js'; -import { defaultChainName } from '../../config'; import PoolList from './PoolList'; import PoolCard from './PoolCard'; import { @@ -26,366 +20,120 @@ import { PoolDetailModal, RemoveLiquidityModal, } from './modals'; -import { - ExtraPoolProperties, - GaugeQueryResult, - Peroid, - PoolApr, -} from '../types'; -import { calcPoolAprs, addPropertiesToPool } from '../../utils'; -import { PriceHash } from '../../utils/types'; import { PoolsOverview } from './PoolsOverview'; -import * as api from '../../api'; -import { useOsmosisRequests, useQueuedRequests, useRequest } from '../../hooks'; - -export type Pool = OsmosisPool & ExtraPoolProperties; - -const osmosisAssets: Asset[] = [...assets.assets, ...asset_list.assets]; +import { Durations, usePoolsApr, usePoolsData } from '@/hooks'; +import { ExtendedPool } from '@/utils'; +import { useChain } from '@cosmos-kit/react'; +import { defaultChainName } from '@/config'; export const ProvideLiquidity = () => { const [showAll, setShowAll] = useState(false); - const [period, setPeriod] = useState('14'); - const [pool, setPool] = useState(); + const [period, setPeriod] = useState('14'); + const [pool, setPool] = useState(); + + const { data, isLoading, refetch } = usePoolsData(); + const { poolsApr, isFetchingApr } = usePoolsApr(data?.uniquePools || []); + const { isWalletConnected } = useChain(defaultChainName); const bondSharesModal = useDisclosure(); const poolDetailModal = useDisclosure(); const addLiquidityModal = useDisclosure(); const removeLiquidityModal = useDisclosure(); - const { address, assets } = useChain(defaultChainName); const { colorMode } = useColorMode(); + const headingColor = useColorModeValue('#697584', '#A7B4C2'); - const getFees = useRequest(api.getFees); - const getTokens = useRequest(api.getTokens); - const getRewards = useRequest(api.getRewards); - const getSuperfluidApr = useRequest( - api.getSuperfluidApr + const sectionTitle = ( + + Liquidity Pools + ); - const getGaugesRequest = useQueuedRequests({ - queueLength: 10, - queueGap: 1000, - }); - - const { - getAllBalances, - getAllPools, - getLockedCoins, - getLockableDurations, - getLocks, - getSuperfluidAssets, - getSuperfluidDelegations, - getActiveGauges, - } = useOsmosisRequests(defaultChainName); - - const prices = useMemo(() => { - if (!getTokens.data) return; - const prices: PriceHash = getTokens.data.reduce( - (prev, cur) => ({ - ...prev, - [cur.denom]: cur.price, - }), - {} - ); - return prices; - }, [getTokens.data]); - - useEffect(() => { - getFees.request(); - getTokens.request(); - getSuperfluidApr.request(); - getLockableDurations.request(); - getSuperfluidAssets.request(); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!address) return; - getRewards.request(address); - getAllBalances.request({ - address, - pagination: { - key: new Uint8Array(), - offset: BigInt(0), - limit: BigInt(1000), - countTotal: false, - reverse: false, - }, - }); - getLockedCoins.request({ owner: address }); - getLocks.request({ - owner: address, - duration: { - seconds: 0n, - nanos: 0, - }, - }); - getSuperfluidDelegations.request({ delegatorAddress: address }); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [address]); - - useEffect(() => { - if (!prices) return; - getAllPools.request(prices); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [prices]); + const overview = ( + + ); - const osmoPrice = useMemo(() => { - if (!getTokens.data) return 0; + if (!isWalletConnected) { return ( - getTokens.data.find((token) => token.denom === assets?.assets[0].base) - ?.price || 0 - ); - }, [assets, getTokens.data]); - - const prettyPools = useMemo(() => { - const fees = getFees.data; - const allPools = getAllPools.data; - const balances = getAllBalances.data; - const lockedCoins = getLockedCoins.data; - - if (!allPools || !prices || !fees || !balances || !lockedCoins) return; - - return allPools.map((pool) => - addPropertiesToPool(pool, fees, balances, lockedCoins, prices) + + {sectionTitle} + {overview} +
+ + Connect the wallet to provide liquidity + +
+
); - }, [ - prices, - getFees.data, - getAllPools.data, - getLockedCoins.data, - getAllBalances.data, - ]); - - const allPoolAprs = useMemo(() => { - if ( - !prices || - !prettyPools || - !getSuperfluidApr.data || - !getSuperfluidAssets.data || - !getLockableDurations.data || - getGaugesRequest.data.length === 0 - ) - return; - - const allPoolAprs: { [key: number]: PoolApr } = - getGaugesRequest.data.reduce((prev, cur) => { - const pool = prettyPools.find(({ id }) => Number(id) === cur.poolId)!; - const swapFee = new BigNumber(pool.poolParams!.swapFee) - .shiftedBy(-18) - .toString(); - const apr = ['1', '7', '14'].reduce((aprs, duration) => { - return { - ...aprs, - [duration]: calcPoolAprs({ - pool, - assets: osmosisAssets, - prices, - swapFee, - activeGauges: cur.gauges, - aprSuperfluid: getSuperfluidApr.data!, - lockupDurations: getLockableDurations.data!, - superfluidPools: getSuperfluidAssets.data!, - volume7d: pool.volume7d, - lockup: duration, - }), - }; - }, {}); - return { ...prev, [cur.poolId]: apr }; - }, {}); - - return allPoolAprs; - }, [ - getGaugesRequest.data, - getLockableDurations.data, - getSuperfluidApr.data, - getSuperfluidAssets.data, - prettyPools, - prices, - ]); - - const addAprToPool = useCallback( - (pool: Pool) => { - if (!pool || !allPoolAprs || !allPoolAprs[Number(pool.id)]) return pool; - return { - ...pool, - apr: allPoolAprs[Number(pool.id)], - }; - }, - [allPoolAprs] - ); - - const allPools = useMemo(() => { - if (!prettyPools) return; - return prettyPools - .sort((a, b) => (new BigNumber(a.liquidity).lt(b.liquidity) ? 1 : -1)) - .slice(0, 20) - .map(addAprToPool); - }, [addAprToPool, prettyPools]); + } - const highlightedPools = useMemo(() => { - if (!getSuperfluidAssets.data || !prettyPools) return; - const poolsData = getSuperfluidAssets.data - .map(({ denom }) => { - const poolId = parseInt(denom.match(/\d+/)![0]); - const poolData = prettyPools.find(({ id }) => Number(id) === poolId); - if (!poolData) return; - return poolData; - }) - .filter(Boolean) as Pool[]; - - return poolsData - .sort((a, b) => (new BigNumber(a!.liquidity).lt(b!.liquidity) ? 1 : -1)) - .slice(0, 3) - .map(addAprToPool); - }, [addAprToPool, getSuperfluidAssets.data, prettyPools]); - - const myPools = useMemo(() => { - if (!getAllBalances.data || !prettyPools || !address) return; - const poolsData = getAllBalances.data - .filter(({ denom }) => denom.startsWith('gamm/pool')) - .map((coin) => { - const poolId = parseInt(coin.denom.match(/\d+/)![0]); - const poolData = prettyPools.find(({ id }) => Number(id) === poolId); - if (!poolData) return; - return poolData; - }) - .filter(Boolean) as Pool[]; - - return poolsData.map(addAprToPool); - }, [addAprToPool, getAllBalances.data, prettyPools, address]); - - useEffect(() => { - if (!allPools || !highlightedPools || !myPools || !prettyPools) return; - const poolIds = [...allPools, ...highlightedPools, ...myPools].map( - (pool) => pool.id + if (isLoading) { + return ( + + {sectionTitle} + {overview} +
+ +
+
); - - const poolIdsWithDenom: { id: number; denom: string }[] = [ - ...new Set(poolIds), - ] - .map((id) => prettyPools.find((pool) => pool.id === id)!) - .map((pool) => ({ - id: Number(pool.id), - denom: pool.totalShares!.denom, - })) - .filter(({ id }) => !allPoolAprs || !allPoolAprs[Number(id)]); - - if (poolIdsWithDenom.length === 0) return; - - const activeGaugesQueries = poolIdsWithDenom.map(({ id, denom }) => { - return getActiveGauges({ - denom, - pagination: { - key: new Uint8Array(), - offset: BigInt(0), - limit: BigInt(1000), - countTotal: false, - reverse: false, - }, - }).then((res) => ({ - poolId: id, - gauges: res.data, - })); - }); - - getGaugesRequest.sendQueuedRequests(activeGaugesQueries); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allPools, highlightedPools, myPools, prettyPools]); - - const updatePoolsData = () => { - if (!address) return; - getLockedCoins.request({ owner: address }); - getAllBalances.request({ - address, - pagination: { - key: new Uint8Array(), - offset: BigInt(0), - limit: BigInt(1000), - countTotal: false, - reverse: false, - }, - }); - }; - - const headingColor = useColorModeValue('#697584', '#A7B4C2'); - - const isLoading = - getLocks.loading || - getRewards.loading || - getAllBalances.loading || - getLockedCoins.loading || - getSuperfluidDelegations.loading; + } return ( - - Liquidity Pools - - - - - {/* MY POOLS */} + {sectionTitle} + {overview} My Pools - {isLoading ? ( -
- -
- ) : ( - <> - {myPools && myPools.length > 0 && ( - - )} - + {data?.myPools && data.myPools.length > 0 && ( + )}
- {/* HIGHLIGHTED POOLS */} Highlighted Pools - {highlightedPools?.map((pool) => ( + {data?.highlightedPools?.map((pool) => ( ))} - {/* ALL POOLS */} All Pools - {allPools && allPools.length > 0 && ( + {data?.allPools && data.allPools.length > 0 && ( { }} /> )} - {allPools && allPools.length > 6 && ( + {data?.allPools && data.allPools.length > 6 && ( { - {pool && prices && getLocks.data && getSuperfluidDelegations.data && ( + {pool && data && ( { /> )} - {pool && prices && getAllBalances.data && ( + {pool && data && ( )} - {pool && prices && ( + {pool && data?.prices && ( )} - {pool && prices && ( + {pool && data?.prices && ( diff --git a/examples/provide-liquidity/components/pools/hooks.tsx b/examples/provide-liquidity/components/pools/hooks.tsx deleted file mode 100644 index 252cdfc9b..000000000 --- a/examples/provide-liquidity/components/pools/hooks.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useToast, Text, Box } from '@chakra-ui/react'; -import { TransactionResult } from '../types'; - -export const useTransactionToast = () => { - const toast = useToast({ - position: 'top-right', - containerStyle: { - maxWidth: '150px', - }, - }); - - const showToast = (code: number, res?: any) => { - toast({ - title: `Transaction ${ - code === TransactionResult.Success ? 'successful' : 'failed' - }`, - status: code === TransactionResult.Success ? 'success' : 'error', - duration: code === TransactionResult.Success ? 5000 : 20000, - isClosable: true, - description: ( - - {res?.message} - {res?.rawLog} - - ), - }); - }; - - return { showToast }; -}; diff --git a/examples/provide-liquidity/components/pools/modals/AddLiquidityModal.tsx b/examples/provide-liquidity/components/pools/modals/AddLiquidityModal.tsx index 6a17557f1..21c96af30 100644 --- a/examples/provide-liquidity/components/pools/modals/AddLiquidityModal.tsx +++ b/examples/provide-liquidity/components/pools/modals/AddLiquidityModal.tsx @@ -12,11 +12,10 @@ import { useMediaQuery, useColorModeValue, } from '@chakra-ui/react'; -import { Pool } from '../ProvideLiquidity'; import { useChain } from '@cosmos-kit/react'; -import { osmosis } from 'osmojs'; -import { Coin } from 'osmojs/dist/codegen/cosmos/base/v1beta1/coin'; -import { defaultChainName } from '../../../config/defaults'; +import { osmosis } from 'osmo-query'; +import { Coin } from 'osmo-query/dist/codegen/cosmos/base/v1beta1/coin'; +import { defaultChainName } from '@/config'; import { LargeButton } from './ModalComponents'; import { getSymbolForDenom, @@ -27,13 +26,13 @@ import { getOsmoDenomForSymbol, baseUnitsToDollarValue, convertDollarValueToCoins, -} from '../../../utils'; -import { PriceHash } from '../../../utils/types'; + ExtendedPool, +} from '@/utils'; +import { PriceHash } from '@/utils/types'; import BigNumber from 'bignumber.js'; -import { TransactionResult } from '../../types'; import { coin, coins as aminoCoins } from '@cosmjs/amino'; -import { useTransactionToast } from '../hooks'; import { TokenInput } from './TokenInput'; +import { useTx } from '@/hooks'; type InputToken = { denom: string; @@ -67,7 +66,7 @@ export const AddLiquidityModal = ({ }: { isOpen: boolean; onClose: () => void; - currentPool: Pool; + currentPool: ExtendedPool; balances: Coin[]; prices: PriceHash; updatePoolsData: () => void; @@ -82,7 +81,7 @@ export const AddLiquidityModal = ({ })) ); - const { showToast } = useTransactionToast(); + const { tx } = useTx(defaultChainName); const [isMobile] = useMediaQuery('(max-width: 480px)'); useEffect(() => { @@ -94,7 +93,7 @@ export const AddLiquidityModal = ({ ); }, [currentPool]); - const { getSigningStargateClient, address } = useChain(defaultChainName); + const { address } = useChain(defaultChainName); const poolName = currentPool?.poolAssets.map(({ token }) => getSymbolForDenom(token!.denom) @@ -145,14 +144,9 @@ export const AddLiquidityModal = ({ }; const handleClick = async () => { - setIsLoading(true); - - const stargateClient = await getSigningStargateClient(); + if (!address) return; - if (!stargateClient || !address) { - console.error('stargateClient undefined or address undefined.'); - return; - } + setIsLoading(true); const allCoins = inputTokens.map(({ denom, inputAmount }) => ({ denom, @@ -161,7 +155,7 @@ export const AddLiquidityModal = ({ .toString(), })); - let msg = []; + let msgs = []; if (singleToken) { const inputCoin = allCoins.find( @@ -184,7 +178,7 @@ export const AddLiquidityModal = ({ DEFAULT_SLIPPAGE ), }); - msg.push(joinSwapExternAmountInMsg); + msgs.push(joinSwapExternAmountInMsg); } else { const shareOutAmount = calcShareOutAmount(pool, allCoins); const tokenInMaxs = allCoins.map((c: Coin) => { @@ -199,7 +193,7 @@ export const AddLiquidityModal = ({ ), tokenInMaxs, }); - msg.push(joinPoolMsg); + msgs.push(joinPoolMsg); } const fee = { @@ -207,21 +201,16 @@ export const AddLiquidityModal = ({ gas: '240000', }; - try { - const res = await stargateClient.signAndBroadcast(address, msg, fee); - if (res?.code !== TransactionResult.Success) throw res; - stargateClient.disconnect(); - setIsLoading(false); - showToast(res.code); - closeModal(); - closeDetailModal(); - updatePoolsData(); - } catch (error) { - console.error(error); - stargateClient.disconnect(); - setIsLoading(false); - showToast(TransactionResult.Failed, error); - } + await tx(msgs, { + fee, + onSuccess: () => { + closeModal(); + closeDetailModal(); + updatePoolsData(); + }, + }); + + setIsLoading(false); }; const titleColor = useColorModeValue('#697584', '#A7B4C2'); diff --git a/examples/provide-liquidity/components/pools/modals/BondSharesModal.tsx b/examples/provide-liquidity/components/pools/modals/BondSharesModal.tsx index 076f52a81..17093736a 100644 --- a/examples/provide-liquidity/components/pools/modals/BondSharesModal.tsx +++ b/examples/provide-liquidity/components/pools/modals/BondSharesModal.tsx @@ -14,18 +14,19 @@ import { useMediaQuery, useColorModeValue, } from '@chakra-ui/react'; -import { Pool } from '../ProvideLiquidity'; import { LargeButton } from './ModalComponents'; -import { useTransactionToast } from '../hooks'; -import { convertDollarValueToShares, getSymbolForDenom } from '../../../utils'; -import { PriceHash } from '../../../utils/types'; -import { truncDecimals } from './PoolDetailModal'; +import { + convertDollarValueToShares, + getSymbolForDenom, + ExtendedPool, +} from '@/utils'; +import { PriceHash } from '@/utils/types'; import BigNumber from 'bignumber.js'; -import { osmosis } from 'osmojs'; +import { osmosis } from 'osmo-query'; import { useChain } from '@cosmos-kit/react'; -import { defaultChainName } from '../../../config/defaults'; -import { Peroid, TransactionResult } from '../../types'; +import { defaultChainName } from '@/config'; import { coins as aminoCoins } from '@cosmjs/amino'; +import { Durations, useTx } from '@/hooks'; const { lockTokens } = osmosis.lockup.MessageComposer.withTypeUrl; @@ -44,17 +45,17 @@ export const BondSharesModal = ({ }: { isOpen: boolean; onClose: () => void; - currentPool: Pool; + currentPool: ExtendedPool; prices: PriceHash; updatePoolsData: () => void; - period: Peroid; + period: Durations; closeDetailModal: () => void; }) => { const [inputShares, setInputShares] = useState(''); const [isLoading, setIsLoading] = useState(false); - const { getSigningStargateClient, address } = useChain(defaultChainName); - const { showToast } = useTransactionToast(); + const { tx } = useTx(defaultChainName); + const { address } = useChain(defaultChainName); const [isMobile] = useMediaQuery('(max-width: 480px)'); const poolName = currentPool?.poolAssets.map(({ token }) => @@ -88,14 +89,9 @@ export const BondSharesModal = ({ }; const handleClick = async () => { - setIsLoading(true); + if (!address) return; - const stargateClient = await getSigningStargateClient(); - - if (!stargateClient || !address) { - console.error('stargateClient undefined or address undefined.'); - return; - } + setIsLoading(true); const coins = [ { @@ -118,21 +114,16 @@ export const BondSharesModal = ({ gas: '450000', }; - try { - const res = await stargateClient.signAndBroadcast(address, [msg], fee); - if (res?.code !== TransactionResult.Success) throw res; - stargateClient.disconnect(); - setIsLoading(false); - showToast(res.code); - closeModal(); - closeDetailModal(); - updatePoolsData(); - } catch (error) { - console.error(error); - stargateClient.disconnect(); - setIsLoading(false); - showToast(TransactionResult.Failed, error); - } + await tx([msg], { + fee, + onSuccess: () => { + closeModal(); + closeDetailModal(); + updatePoolsData(); + }, + }); + + setIsLoading(false); }; const titleColor = useColorModeValue('#697584', '#A7B4C2'); diff --git a/examples/provide-liquidity/components/pools/modals/PoolDetailModal.tsx b/examples/provide-liquidity/components/pools/modals/PoolDetailModal.tsx index d95ef185d..7f8f23312 100644 --- a/examples/provide-liquidity/components/pools/modals/PoolDetailModal.tsx +++ b/examples/provide-liquidity/components/pools/modals/PoolDetailModal.tsx @@ -13,34 +13,32 @@ import { useMediaQuery, useColorModeValue, } from '@chakra-ui/react'; -import { Pool } from '../ProvideLiquidity'; import { getLogoUrlFromDenom } from '../PoolList'; import { ChainLogo } from '../PoolCard'; -import { Coin } from 'osmojs/dist/codegen/cosmos/base/v1beta1/coin'; +import { Coin } from 'osmo-query/dist/codegen/cosmos/base/v1beta1/coin'; import { NormalButton, PoolAssetDisplay, BondLiquidityCard, RewardText, } from './ModalComponents'; -import { useTransactionToast } from '../hooks'; import BigNumber from 'bignumber.js'; import { convertDollarValueToShares, convertDollarValueToCoins, convertGammTokenToDollarValue, getSymbolForDenom, -} from '../../../utils'; -import { PriceHash } from '../../../utils/types'; -import { osmosis } from 'osmojs'; + ExtendedPool, +} from '@/utils'; +import { PriceHash } from '@/utils/types'; +import { osmosis } from 'osmo-query'; import { useChain } from '@cosmos-kit/react'; -import { defaultChainName } from '../../../config/defaults'; -import { PeriodLock } from 'osmojs/dist/codegen/osmosis/lockup/lock'; +import { defaultChainName } from '@/config'; +import { PeriodLock } from 'osmo-query/dist/codegen/osmosis/lockup/lock'; import { daysToSeconds } from './BondSharesModal'; -import Long from 'long'; import dayjs from 'dayjs'; import { coins as aminoCoins } from '@cosmjs/amino'; -import { Peroid, TransactionResult } from '../../types'; +import { durations, Durations, PoolApr, useTx } from '@/hooks'; export const truncDecimals = ( val: string | number | undefined, @@ -53,8 +51,6 @@ const { beginUnlocking } = osmosis.lockup.MessageComposer.withTypeUrl; const { superfluidUnbondLock, superfluidUndelegate } = osmosis.superfluid.MessageComposer.withTypeUrl; -const durations = ['1', '7', '14'] as const; - export const PoolDetailModal = ({ isOpen, onClose, @@ -66,16 +62,18 @@ export const PoolDetailModal = ({ rewardPerDay, setPeroid, locks, + poolApr, }: { isOpen: boolean; onClose: () => void; - pool: Pool; + pool: ExtendedPool; prices: PriceHash; locks: PeriodLock[]; delegatedCoins: Coin[]; updatePoolsData: () => void; rewardPerDay: number; - setPeroid: (peroid: Peroid) => void; + setPeroid: (peroid: Durations) => void; + poolApr: PoolApr | undefined; openModals: { onAddLiquidityOpen: () => void; onRemoveLiquidityOpen: () => void; @@ -86,8 +84,8 @@ export const PoolDetailModal = ({ [key: string]: boolean; }>(); - const { getSigningStargateClient, address } = useChain(defaultChainName); - const { showToast } = useTransactionToast(); + const { tx } = useTx(defaultChainName); + const { address } = useChain(defaultChainName); const [isMobile] = useMediaQuery('(max-width: 480px)'); const poolId = pool?.id; @@ -97,7 +95,7 @@ export const PoolDetailModal = ({ ); const swapFee = new BigNumber(pool!.poolParams!.swapFee) - .shiftedBy(-16) + .shiftedBy(2) .toString(); const totalBalance = new BigNumber(pool?.myLiquidity || 0).plus( @@ -124,7 +122,7 @@ export const PoolDetailModal = ({ const bondedData = durations .filter((duration) => { - const gaugeAprs = pool.apr[duration].gaugeAprs; + const gaugeAprs = poolApr?.[duration].gaugeAprs; return gaugeAprs && gaugeAprs.length > 0; }) .map((duration) => { @@ -138,7 +136,7 @@ export const PoolDetailModal = ({ if (!lock) { return { ID: '', - apr: pool.apr[duration], + apr: poolApr?.[duration], value: '0', shares: '0', duration, @@ -150,7 +148,7 @@ export const PoolDetailModal = ({ return { ID: lock.ID.toString(), - apr: pool.apr[duration], + apr: poolApr?.[duration], value, shares, duration, @@ -158,16 +156,9 @@ export const PoolDetailModal = ({ }); const handleUnbondClick = async (ID: string | null, duration: string) => { - if (!ID) return; + if (!ID || !address) return; setUnbondingStatus((prev) => ({ ...prev, [ID]: true })); - const stargateClient = await getSigningStargateClient(); - - if (!stargateClient || !address) { - console.error('stargateClient undefined or address undefined.'); - return; - } - const hasOsmoToken = pool.poolAssets.some( ({ token }) => token?.denom === 'uosmo' ); @@ -177,7 +168,7 @@ export const PoolDetailModal = ({ const isSuperfluidBonded = hasOsmoToken && duration === '14' && hasDelegatedCoin; - let msg = []; + let msgs = []; if (isSuperfluidBonded) { const superfluidUndelegateMsg = superfluidUndelegate({ @@ -188,14 +179,14 @@ export const PoolDetailModal = ({ lockId: BigInt(ID), sender: address, }); - msg = [superfluidUndelegateMsg, superfluidUnbondLockMsg]; + msgs = [superfluidUndelegateMsg, superfluidUnbondLockMsg]; } else { const beginUnlockingMsg = beginUnlocking({ ID: BigInt(ID), coins: [], owner: address, }); - msg.push(beginUnlockingMsg); + msgs.push(beginUnlockingMsg); } const fee = { @@ -203,20 +194,15 @@ export const PoolDetailModal = ({ gas: '600000', }; - try { - const res = await stargateClient.signAndBroadcast(address, msg, fee); - if (res?.code !== TransactionResult.Success) throw res; - stargateClient.disconnect(); - setUnbondingStatus((prev) => ({ ...prev, [ID]: false })); - showToast(res.code); - onClose(); - updatePoolsData(); - } catch (error) { - console.error(error); - stargateClient.disconnect(); - setUnbondingStatus((prev) => ({ ...prev, [ID]: false })); - showToast(TransactionResult.Failed, error); - } + await tx(msgs, { + fee, + onSuccess: () => { + onClose(); + updatePoolsData(); + }, + }); + + setUnbondingStatus((prev) => ({ ...prev, [ID]: false })); }; const titleColor = useColorModeValue('#697584', '#A7B4C2'); @@ -402,7 +388,7 @@ export const PoolDetailModal = ({ {bondedData.map((bonded) => ( { diff --git a/examples/provide-liquidity/components/pools/modals/RemoveLiquidityModal.tsx b/examples/provide-liquidity/components/pools/modals/RemoveLiquidityModal.tsx index cbb377b7d..ff9bdee0e 100644 --- a/examples/provide-liquidity/components/pools/modals/RemoveLiquidityModal.tsx +++ b/examples/provide-liquidity/components/pools/modals/RemoveLiquidityModal.tsx @@ -17,25 +17,24 @@ import { useMediaQuery, useColorModeValue, } from '@chakra-ui/react'; -import { Pool } from '../ProvideLiquidity'; import { LargeButton, PoolAssetDisplay } from './ModalComponents'; -import { useTransactionToast } from '../hooks'; import { convertDollarValueToCoins, convertDollarValueToShares, getSymbolForDenom, noDecimals, -} from '../../../utils'; + ExtendedPool, +} from '@/utils'; import { truncDecimals } from './PoolDetailModal'; -import { Coin } from 'osmojs/dist/codegen/cosmos/base/v1beta1/coin'; -import { PriceHash } from '../../../utils/types'; +import { Coin } from 'osmo-query/dist/codegen/cosmos/base/v1beta1/coin'; +import { PriceHash } from '@/utils/types'; import { getLogoUrlFromDenom } from '../PoolList'; import BigNumber from 'bignumber.js'; -import { osmosis } from 'osmojs'; -import { defaultChainName } from '../../../config/defaults'; +import { osmosis } from 'osmo-query'; +import { defaultChainName } from '@/config'; import { useChain } from '@cosmos-kit/react'; -import { TransactionResult } from '../../types'; import { coin, coins as aminoCoins } from '@cosmjs/amino'; +import { useTx } from '@/hooks'; const { exitPool } = osmosis.gamm.v1beta1.MessageComposer.withTypeUrl; @@ -49,15 +48,15 @@ export const RemoveLiquidityModal = ({ }: { isOpen: boolean; onClose: () => void; - currentPool: Pool; + currentPool: ExtendedPool; prices: PriceHash; updatePoolsData: () => void; closeDetailModal: () => void; }) => { const [percent, setPercent] = useState(50); const [isLoading, setIsLoading] = useState(false); - const { getSigningStargateClient, address } = useChain(defaultChainName); - const { showToast } = useTransactionToast(); + const { tx } = useTx(defaultChainName); + const { address } = useChain(defaultChainName); const [isMobile] = useMediaQuery('(max-width: 480px)'); const myLiquidity = new BigNumber(currentPool.myLiquidity || 0) @@ -87,14 +86,9 @@ export const RemoveLiquidityModal = ({ }; const handleClick = async () => { - setIsLoading(true); - - const stargateClient = await getSigningStargateClient(); + if (!address) return; - if (!stargateClient || !address) { - console.error('stargateClient undefined or address undefined.'); - return; - } + setIsLoading(true); const coinsNeeded = coins.map(({ denom, amount }) => { const slippage = 2.5; @@ -130,21 +124,16 @@ export const RemoveLiquidityModal = ({ gas: '280000', }; - try { - const res = await stargateClient.signAndBroadcast(address, [msg], fee); - if (res?.code !== TransactionResult.Success) throw res; - stargateClient.disconnect(); - setIsLoading(false); - showToast(res.code); - closeModal(); - closeDetailModal(); - updatePoolsData(); - } catch (error) { - console.error(error); - stargateClient.disconnect(); - setIsLoading(false); - showToast(TransactionResult.Failed, error); - } + await tx([msg], { + fee, + onSuccess: () => { + closeModal(); + closeDetailModal(); + updatePoolsData(); + }, + }); + + setIsLoading(false); }; const titleColor = useColorModeValue('#697584', '#A7B4C2'); diff --git a/examples/provide-liquidity/components/pools/modals/TokenInput.tsx b/examples/provide-liquidity/components/pools/modals/TokenInput.tsx index 8ea60cbc2..18e54d435 100644 --- a/examples/provide-liquidity/components/pools/modals/TokenInput.tsx +++ b/examples/provide-liquidity/components/pools/modals/TokenInput.tsx @@ -11,7 +11,7 @@ import { useMediaQuery, useColorModeValue, } from '@chakra-ui/react'; -import { Coin } from 'osmojs/dist/codegen/cosmos/base/v1beta1/coin'; +import { Coin } from 'osmo-query/dist/codegen/cosmos/base/v1beta1/coin'; import { AddIcon, MinusIcon } from '@chakra-ui/icons'; import { prettyPool, @@ -19,10 +19,10 @@ import { baseUnitsToDisplayUnits, calcMaxCoinsForPool, getSymbolForDenom, -} from '../../../utils'; +} from '@/utils'; import { getLogoUrlFromDenom } from '../PoolList'; import { ChainLogo } from '../PoolCard'; -import { PoolPretty, PriceHash } from '../../../utils/types'; +import { PoolPretty, PriceHash } from '@/utils/types'; import BigNumber from 'bignumber.js'; import { truncDecimals } from './PoolDetailModal'; diff --git a/examples/provide-liquidity/components/types.tsx b/examples/provide-liquidity/components/types.tsx deleted file mode 100644 index 6727772e6..000000000 --- a/examples/provide-liquidity/components/types.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Gauge } from 'osmojs/dist/codegen/osmosis/incentives/gauge'; -import { calcPoolAprs } from '../utils'; - -export enum TransactionResult { - Success = 0, - Failed = 1, -} - -export type Peroid = '1' | '7' | '14'; - -export type PoolApr = { - [K in Peroid]: ReturnType; -}; - -export type ExtraPoolProperties = { - fees7D: number; - volume24H: number; - volume7d: number; - liquidity: string | number; - myLiquidity?: string | number; - bonded?: string | number; - apr: PoolApr; -}; - -export type GaugeQueryResult = { - poolId: number; - gauges: Gauge[]; -}; - -export type Fee = { - pool_id: string; - volume_24h: number; - volume_7d: number; - fees_spent_24h: number; - fees_spent_7d: number; - fees_percentage: string; -}; diff --git a/examples/provide-liquidity/config/defaults.ts b/examples/provide-liquidity/config/defaults.ts index d4d4ddca8..40527d9c4 100644 --- a/examples/provide-liquidity/config/defaults.ts +++ b/examples/provide-liquidity/config/defaults.ts @@ -11,7 +11,7 @@ import { ibcAminoConverters, osmosisAminoConverters, osmosisProtoRegistry, -} from 'osmojs'; +} from 'osmo-query'; export const defaultChainName = 'osmosis'; diff --git a/examples/provide-liquidity/config/features.ts b/examples/provide-liquidity/config/features.ts index d290dc3e0..01724891d 100644 --- a/examples/provide-liquidity/config/features.ts +++ b/examples/provide-liquidity/config/features.ts @@ -40,7 +40,7 @@ export const products: FeatureProps[] = [ export const dependencies: FeatureProps[] = [ { title: 'OsmoJS', - text: 'OsmosJS makes it easy to compose and broadcast messages.', + text: 'Use osmo-query to simplify data fetching.', href: 'https://github.com/osmosis-labs/osmojs', }, { diff --git a/examples/provide-liquidity/hooks/index.ts b/examples/provide-liquidity/hooks/index.ts index 8d8cad596..c4dc947f0 100644 --- a/examples/provide-liquidity/hooks/index.ts +++ b/examples/provide-liquidity/hooks/index.ts @@ -1,5 +1,4 @@ -export * from './useClient'; -export * from './useRequest'; -export * from './useQueuedRequests'; -export * from './useOsmosisRequests'; -export * from './useTransactionToast'; +export * from './queries'; + +export * from './useTx'; +export * from './useToaster'; diff --git a/examples/provide-liquidity/hooks/queries/external/index.ts b/examples/provide-liquidity/hooks/queries/external/index.ts new file mode 100644 index 000000000..85b43a2ea --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/external/index.ts @@ -0,0 +1,4 @@ +export * from './useFees'; +export * from './usePrices'; +export * from './useRewards'; +export * from './useSuperfluidApr'; diff --git a/examples/provide-liquidity/hooks/queries/external/useFees.ts b/examples/provide-liquidity/hooks/queries/external/useFees.ts new file mode 100644 index 000000000..f6eedd945 --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/external/useFees.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; +import { handleError } from './usePrices'; + +export interface Fee { + pool_id: string; + volume_24h: number; + volume_7d: number; + fees_spent_24h: number; + fees_spent_7d: number; + fees_percentage: string; +} + +const fetchFees = async (): Promise => { + const url = 'https://api-osmosis.imperator.co/fees/v1/pools'; + return fetch(url) + .then(handleError) + .then((res) => res.json()) + .then((res) => res.data); +}; + +export const useFees = () => { + return useQuery({ + queryKey: ['fees'], + queryFn: fetchFees, + staleTime: Infinity, + }); +}; diff --git a/examples/provide-liquidity/hooks/queries/external/usePrices.ts b/examples/provide-liquidity/hooks/queries/external/usePrices.ts new file mode 100644 index 000000000..2b0233ae8 --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/external/usePrices.ts @@ -0,0 +1,52 @@ +import { Asset } from '@chain-registry/types'; +import { useQuery } from '@tanstack/react-query'; +import { osmosisAssets } from '@/utils'; + +type CoinGeckoId = string; +type CoinGeckoUSD = { usd: number }; +type CoinGeckoUSDResponse = Record; + +export const handleError = (resp: Response) => { + if (!resp.ok) throw Error(resp.statusText); + return resp; +}; + +const getAssetsWithGeckoIds = (assets: Asset[]) => { + return assets.filter((asset) => !!asset?.coingecko_id); +}; + +const getGeckoIds = (assets: Asset[]) => { + return assets.map((asset) => asset.coingecko_id) as string[]; +}; + +const formatPrices = ( + prices: CoinGeckoUSDResponse, + assets: Asset[] +): Record => { + return Object.entries(prices).reduce((priceHash, cur) => { + const denom = assets.find((asset) => asset.coingecko_id === cur[0])!.base; + return { ...priceHash, [denom]: cur[1].usd }; + }, {}); +}; + +const fetchPrices = async ( + geckoIds: string[] +): Promise => { + const url = `https://api.coingecko.com/api/v3/simple/price?ids=${geckoIds.join()}&vs_currencies=usd`; + + return fetch(url) + .then(handleError) + .then((res) => res.json()); +}; + +export const usePrices = () => { + const assetsWithGeckoIds = getAssetsWithGeckoIds(osmosisAssets); + const geckoIds = getGeckoIds(assetsWithGeckoIds); + + return useQuery({ + queryKey: ['prices'], + queryFn: () => fetchPrices(geckoIds), + select: (data) => formatPrices(data, assetsWithGeckoIds), + staleTime: Infinity, + }); +}; diff --git a/examples/provide-liquidity/hooks/queries/external/useRewards.ts b/examples/provide-liquidity/hooks/queries/external/useRewards.ts new file mode 100644 index 000000000..73a806966 --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/external/useRewards.ts @@ -0,0 +1,37 @@ +import { defaultChainName } from '@/config'; +import { useChain } from '@cosmos-kit/react'; +import { useQuery } from '@tanstack/react-query'; +import { handleError } from './usePrices'; + +type PoolReward = { + day_usd: number; + month_usd: number; + year_usd: number; +}; + +type Rewards = { + pools: { + [key: number]: PoolReward; + }; + total_day_usd: number; + total_month_usd: number; + total_year_usd: number; +}; + +const fetchRewards = async (address: string): Promise => { + const url = `https://api-osmosis-chain.imperator.co/lp/v1/rewards/estimation/${address}`; + return fetch(url) + .then(handleError) + .then((res) => res.json()); +}; + +export const useRewards = () => { + const { address } = useChain(defaultChainName); + + return useQuery({ + queryKey: ['rewards'], + queryFn: () => fetchRewards(address || ''), + enabled: !!address, + staleTime: Infinity, + }); +}; diff --git a/examples/provide-liquidity/hooks/queries/external/useSuperfluidApr.ts b/examples/provide-liquidity/hooks/queries/external/useSuperfluidApr.ts new file mode 100644 index 000000000..e13d8095b --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/external/useSuperfluidApr.ts @@ -0,0 +1,41 @@ +import { useQuery } from '@tanstack/react-query'; +import { handleError } from './usePrices'; + +interface Apr { + pool_id: number; + apr_list: Aprlist[]; +} + +interface Aprlist { + start_date: string; + denom: string; + symbol: string; + apr_1d: number; + apr_7d: number; + apr_14d: number; + apr_superfluid: number; +} + +const parseSuperfluidApr = (res: Apr[]): number => { + return ( + res + .find(({ apr_list }) => apr_list.find(({ symbol }) => symbol === 'ECH')) + ?.apr_list.find(({ symbol }) => symbol === 'ECH')?.apr_superfluid || 0 + ); +}; + +const fetchSuperfluidApr = async (): Promise => { + const url = 'https://api-osmosis.imperator.co/apr/v2/all'; + return fetch(url) + .then(handleError) + .then((res) => res.json()) + .then(parseSuperfluidApr); +}; + +export const useSuperfluidApr = () => { + return useQuery({ + queryKey: ['superfluidApr'], + queryFn: fetchSuperfluidApr, + staleTime: Infinity, + }); +}; diff --git a/examples/provide-liquidity/hooks/queries/index.ts b/examples/provide-liquidity/hooks/queries/index.ts new file mode 100644 index 000000000..ed656ad0d --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/index.ts @@ -0,0 +1,7 @@ +export * from './external'; + +export * from './useEpochs'; +export * from './usePoolsApr'; +export * from './usePoolsData'; +export * from './useQueryHooks'; +export * from './useRpcQueryClient'; diff --git a/examples/provide-liquidity/hooks/queries/useEpochs.ts b/examples/provide-liquidity/hooks/queries/useEpochs.ts new file mode 100644 index 000000000..38ce34a7a --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/useEpochs.ts @@ -0,0 +1,23 @@ +import { defaultChainName } from '@/config'; +import { UseQueryResult } from '@tanstack/react-query'; +import { EpochInfo } from 'osmo-query/dist/codegen/osmosis/epochs/genesis'; +import { useQueryHooks } from './useQueryHooks'; + +export const useEpochs = () => { + const { osmosisQuery, isReady, isFetching } = useQueryHooks(defaultChainName); + + const epochsQuery: UseQueryResult = + osmosisQuery.epochs.v1beta1.useEpochInfos({ + options: { + enabled: isReady, + select: ({ epochs }) => epochs, + staleTime: Infinity, + }, + }); + + const epochs = epochsQuery.data; + const updateEpochs = epochsQuery.refetch; + const isLoading = isFetching || epochsQuery.isFetching; + + return { epochs, updateEpochs, isLoading }; +}; diff --git a/examples/provide-liquidity/hooks/queries/usePoolsApr.ts b/examples/provide-liquidity/hooks/queries/usePoolsApr.ts new file mode 100644 index 000000000..b272c98e2 --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/usePoolsApr.ts @@ -0,0 +1,128 @@ +import { useMemo } from 'react'; +import { useQueries, UseQueryResult } from '@tanstack/react-query'; +import { SuperfluidAsset } from 'osmo-query/dist/codegen/osmosis/superfluid/superfluid'; +import { Duration } from 'osmo-query/dist/codegen/google/protobuf/duration'; +import { Gauge } from 'osmo-query/dist/codegen/osmosis/incentives/gauge'; + +import { defaultChainName } from '@/config'; +import { useQueryHooks } from './useQueryHooks'; +import { useRpcQueryClient } from './useRpcQueryClient'; +import { calcPoolAprs, osmosisAssets, ExtendedPool } from '@/utils'; +import { getPagination, usePrices, useSuperfluidApr } from '.'; + +export const durations = ['1', '7', '14'] as const; + +export type Durations = typeof durations[number]; + +export type PoolApr = { + [K in Durations]: ReturnType; +}; + +export type PoolsApr = { [key: string]: PoolApr }; + +export const usePoolsApr = (pools: ExtendedPool[]) => { + const { osmosisQuery, isReady, isFetching } = useQueryHooks(defaultChainName); + const { rpcQueryClient } = useRpcQueryClient(); + + const lockableDurationsQuery: UseQueryResult = + osmosisQuery.incentives.useLockableDurations({ + options: { + enabled: isReady, + select: ({ lockableDurations }) => lockableDurations || [], + staleTime: Infinity, + }, + }); + + const superfluidAssetsQuery: UseQueryResult = + osmosisQuery.superfluid.useAllAssets({ + options: { + enabled: isReady, + select: ({ assets }) => assets || [], + staleTime: Infinity, + }, + }); + + const gaugeQueries = useQueries({ + queries: pools.map(({ denom }) => { + return { + queryKey: ['gauges', denom], + queryFn: () => + rpcQueryClient?.osmosis.incentives + .activeGaugesPerDenom({ + denom, + pagination: getPagination(100n), + }) + .then(({ data }) => data), + enabled: !!rpcQueryClient, + staleTime: Infinity, + }; + }), + }); + + const pricesQuery = usePrices(); + const superfluidAprQuery = useSuperfluidApr(); + + const singleQueries = { + prices: pricesQuery, + superfluidApr: superfluidAprQuery, + superfluidAssets: superfluidAssetsQuery, + lockableDurations: lockableDurationsQuery, + }; + + const isSingleQueriesFetching = Object.values(singleQueries).some( + ({ isFetching }) => isFetching + ); + const isGaugesQueryFetching = gaugeQueries.some( + ({ isFetching }) => isFetching + ); + const isLoading = + isFetching || isSingleQueriesFetching || isGaugesQueryFetching; + + type SingleQueries = typeof singleQueries; + + type SingleQueriesData = { + [Key in keyof SingleQueries]: NonNullable; + }; + + const poolsApr = useMemo(() => { + if (isLoading || !isReady || pools.length === 0) return; + + const singleQueriesData = Object.fromEntries( + Object.entries(singleQueries).map(([key, query]) => [key, query.data]) + ) as SingleQueriesData; + + const { prices, superfluidAssets, lockableDurations, superfluidApr } = + singleQueriesData; + + const gaugesArray = gaugeQueries + .map(({ data }) => data) + .filter(Boolean) as Gauge[][]; + + const allApr = gaugesArray.map((gauges) => { + const poolDenom = gauges[0].distributeTo.denom; + const pool = pools.find(({ denom }) => denom === poolDenom)!; + const poolApr = durations.map((duration) => { + const apr = calcPoolAprs({ + pool, + prices, + lockup: duration, + assets: osmosisAssets, + volume7d: pool.volume7d, + activeGauges: gauges, + swapFee: pool.poolParams.swapFee, + aprSuperfluid: superfluidApr, + lockupDurations: lockableDurations, + superfluidPools: superfluidAssets, + }); + return [duration, apr]; + }); + + return [poolDenom, Object.fromEntries(poolApr)]; + }); + + return Object.fromEntries(allApr) as PoolsApr; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading, isReady, pools]); + + return { poolsApr, isFetchingApr: isLoading }; +}; diff --git a/examples/provide-liquidity/hooks/queries/usePoolsData.ts b/examples/provide-liquidity/hooks/queries/usePoolsData.ts new file mode 100644 index 000000000..e65cf5461 --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/usePoolsData.ts @@ -0,0 +1,190 @@ +import { useMemo } from 'react'; +import { Coin } from '@cosmjs/stargate'; +import { useChain } from '@cosmos-kit/react'; +import { UseQueryResult } from '@tanstack/react-query'; +import { Pool } from 'osmo-query/dist/codegen/osmosis/gamm/pool-models/balancer/balancerPool'; + +import { defaultChainName } from '@/config'; +import { useQueryHooks } from './useQueryHooks'; +import { usePrices } from './external/usePrices'; +import { + descByLiquidity, + descByMyLiquidity, + extendPool, + filterPools, + getPoolsByDenom, +} from '@/utils'; +import { SuperfluidAsset } from 'osmo-query/dist/codegen/osmosis/superfluid/superfluid'; +import { PeriodLock } from 'osmo-query/dist/codegen/osmosis/lockup/lock'; +import { useFees, useRewards } from './external'; + +(BigInt.prototype as any).toJSON = function () { + return this.toString(); +}; + +export const getPagination = (limit: bigint) => ({ + limit, + key: new Uint8Array(), + offset: 0n, + countTotal: true, + reverse: false, +}); + +export const usePoolsData = () => { + const { address } = useChain(defaultChainName); + + const { cosmosQuery, osmosisQuery, isReady, isFetching } = + useQueryHooks(defaultChainName); + + const balancesQuery: UseQueryResult = + cosmosQuery.bank.v1beta1.useAllBalances({ + request: { + address: address || '', + pagination: getPagination(100n), + }, + options: { + enabled: isReady, + select: ({ balances }) => balances || [], + }, + }); + + const superfluidAssetsQuery: UseQueryResult = + osmosisQuery.superfluid.useAllAssets({ + options: { + enabled: isReady, + select: ({ assets }) => assets || [], + staleTime: Infinity, + }, + }); + + const lockedCoinsQuery: UseQueryResult = + osmosisQuery.lockup.useAccountLockedCoins({ + request: { + owner: address || '', + }, + options: { + enabled: isReady, + select: ({ coins }) => coins || [], + }, + }); + + const locksQuery: UseQueryResult = + osmosisQuery.lockup.useAccountLockedLongerDuration({ + request: { + owner: address || '', + duration: { + nanos: 0, + seconds: 0n, + }, + }, + options: { + enabled: isReady, + select: ({ locks }) => locks || [], + }, + }); + + const superfluidDelegationsQuery: UseQueryResult = + osmosisQuery.superfluid.useSuperfluidDelegationsByDelegator({ + request: { + delegatorAddress: address || '', + }, + options: { + enabled: isReady, + select: ({ totalDelegatedCoins }) => totalDelegatedCoins || [], + staleTime: Infinity, + }, + }); + + const poolsQuery: UseQueryResult = osmosisQuery.gamm.v1beta1.usePools( + { + request: { + pagination: getPagination(5000n), + }, + options: { + enabled: isReady, + select: ({ pools }) => pools || [], + staleTime: Infinity, + }, + } + ); + + const feesQuery = useFees(); + const pricesQuery = usePrices(); + const rewardsQuery = useRewards(); + + const dataQueries = { + locks: locksQuery, + pools: poolsQuery, + balances: balancesQuery, + lockedCoins: lockedCoinsQuery, + superfluidAssets: superfluidAssetsQuery, + superfluidDelegations: superfluidDelegationsQuery, + fees: feesQuery, + prices: pricesQuery, + rewards: rewardsQuery, + }; + + const queriesToRefetch = [ + dataQueries.locks, + dataQueries.balances, + dataQueries.lockedCoins, + ]; + + const queries = Object.values(dataQueries); + const isInitialFetching = queries.some(({ isFetching }) => isFetching); + const isRefetching = queries.some(({ isRefetching }) => isRefetching); + const isLoading = isFetching || isInitialFetching || isRefetching; + + type AllQueries = typeof dataQueries; + + type QueriesData = { + [Key in keyof AllQueries]: NonNullable; + }; + + const data = useMemo(() => { + if (isLoading || !isReady) return; + + const queriesData = Object.fromEntries( + Object.entries(dataQueries).map(([key, query]) => [key, query.data]) + ) as QueriesData; + + const { prices, fees, pools, balances, lockedCoins, superfluidAssets } = + queriesData; + + const filteredPools = filterPools(pools, prices); + + const extendedPools = filteredPools.map((pool) => + extendPool({ pool, fees, balances, lockedCoins, prices }) + ); + + const allPools = extendedPools.sort(descByLiquidity).slice(0, 40); + + const highlightedPools = getPoolsByDenom(extendedPools, superfluidAssets) + .sort(descByLiquidity) + .slice(0, 3); + + const myPools = getPoolsByDenom(extendedPools, balances).sort( + descByMyLiquidity + ); + + const displayedPools = [allPools, highlightedPools, myPools].flat(); + const uniquePools = displayedPools.filter((pool, i) => { + return i === displayedPools.findIndex((p) => pool.id === p.id); + }); + + return { + ...queriesData, + allPools, + highlightedPools, + myPools, + uniquePools, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]); + + const refetch = () => { + queriesToRefetch.forEach((query) => query.refetch()); + }; + + return { data, isLoading, refetch }; +}; diff --git a/examples/provide-liquidity/hooks/queries/useQueryHooks.ts b/examples/provide-liquidity/hooks/queries/useQueryHooks.ts new file mode 100644 index 000000000..7d6ef13ea --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/useQueryHooks.ts @@ -0,0 +1,47 @@ +import { HttpEndpoint } from '@cosmjs/stargate'; +import { useChain } from '@cosmos-kit/react'; +import { UseQueryResult } from '@tanstack/react-query'; +import { useRpcEndpoint, useRpcClient, createRpcQueryHooks } from 'osmo-query'; + +export const useQueryHooks = (chainName: string, extraKey?: string) => { + const { address, getRpcEndpoint } = useChain(chainName); + + const rpcEndpointQuery: UseQueryResult = + useRpcEndpoint({ + getter: getRpcEndpoint, + options: { + enabled: !!address, + staleTime: Infinity, + queryKeyHashFn: (queryKey) => { + const key = [...queryKey, chainName]; + return JSON.stringify(extraKey ? [...key, extraKey] : key); + }, + }, + }); + + const rpcClientQuery = useRpcClient({ + rpcEndpoint: rpcEndpointQuery.data || '', + options: { + enabled: !!address && !!rpcEndpointQuery.data, + staleTime: Infinity, + queryKeyHashFn: (queryKey) => { + return JSON.stringify(extraKey ? [...queryKey, extraKey] : queryKey); + }, + }, + }); + + const { cosmos: cosmosQuery, osmosis: osmosisQuery } = createRpcQueryHooks({ + rpc: rpcClientQuery.data, + }); + + const isReady = !!address && !!rpcClientQuery.data; + const isFetching = rpcEndpointQuery.isFetching || rpcClientQuery.isFetching; + + return { + cosmosQuery, + osmosisQuery, + isReady, + isFetching, + rpcEndpoint: rpcEndpointQuery.data, + }; +}; diff --git a/examples/provide-liquidity/hooks/queries/useRpcQueryClient.ts b/examples/provide-liquidity/hooks/queries/useRpcQueryClient.ts new file mode 100644 index 000000000..b064a381c --- /dev/null +++ b/examples/provide-liquidity/hooks/queries/useRpcQueryClient.ts @@ -0,0 +1,19 @@ +import { osmosis } from 'osmo-query'; +import { useQuery } from '@tanstack/react-query'; +import { defaultChainName } from '@/config'; +import { useQueryHooks } from './useQueryHooks'; + +const createRPCQueryClient = osmosis.ClientFactory.createRPCQueryClient; + +export const useRpcQueryClient = () => { + const { rpcEndpoint } = useQueryHooks(defaultChainName); + + const rpcQueryClientQuery = useQuery({ + queryKey: ['rpcQueryClient', rpcEndpoint], + queryFn: () => createRPCQueryClient({ rpcEndpoint: rpcEndpoint || '' }), + enabled: !!rpcEndpoint, + staleTime: Infinity, + }); + + return { rpcQueryClient: rpcQueryClientQuery.data }; +}; diff --git a/examples/provide-liquidity/hooks/useClient.ts b/examples/provide-liquidity/hooks/useClient.ts deleted file mode 100644 index e0429d57a..000000000 --- a/examples/provide-liquidity/hooks/useClient.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { osmosis } from 'osmojs'; -import { useChain } from '@cosmos-kit/react'; - -export const useClient = (chainName: string) => { - const { getRpcEndpoint } = useChain(chainName); - - const getClient = async () => { - let rpcEndpoint = await getRpcEndpoint(); - - if (!rpcEndpoint) { - console.log('no rpc endpoint — using a fallback'); - rpcEndpoint = `https://rpc.cosmos.directory/${chainName}`; - } - - const client = await osmosis.ClientFactory.createRPCQueryClient({ - rpcEndpoint, - }); - - return client; - }; - - return { getClient }; -}; diff --git a/examples/provide-liquidity/hooks/useOsmosisRequests.ts b/examples/provide-liquidity/hooks/useOsmosisRequests.ts deleted file mode 100644 index 2534a4f09..000000000 --- a/examples/provide-liquidity/hooks/useOsmosisRequests.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { useRequest } from './useRequest'; -import { osmosis } from 'osmojs'; -import { QueryAllBalancesRequest } from 'osmojs/dist/codegen/cosmos/bank/v1beta1/query'; -import { Pool } from 'osmojs/dist/codegen/osmosis/gamm/pool-models/balancer/balancerPool'; -import { useRef, useState } from 'react'; -import { useClient } from './useClient'; -import { - AccountLockedCoinsRequest, - AccountLockedLongerDurationRequest, -} from 'osmojs/dist/codegen/osmosis/lockup/query'; -import { QueryDelegatorDelegationsRequest } from 'osmojs/dist/codegen/cosmos/staking/v1beta1/query'; -import { PriceHash } from '../utils/types'; -import { SuperfluidDelegationsByDelegatorRequest } from 'osmojs/dist/codegen/osmosis/superfluid/query'; -import { ActiveGaugesPerDenomRequest } from 'osmojs/dist/codegen/osmosis/incentives/query'; -import { - assets as nativeAssets, - asset_list as osmosisIbcAssets, -} from '@chain-registry/osmosis'; - -type Client = Awaited< - ReturnType ->; - -const osmosisAssets = [...nativeAssets.assets, ...osmosisIbcAssets.assets]; - -const removeUnsupportedPools = ({ poolAssets }: Pool, prices: PriceHash) => { - return poolAssets.every( - ({ token }) => - prices[token!.denom] && - !token!.denom.startsWith('gamm/pool') && - osmosisAssets.find((asset) => asset.base === token?.denom) - ); -}; - -export const useOsmosisRequests = (chainName: string) => { - const { getClient: getClientRequest } = useClient(chainName); - const [osmosisClient, setOsmosisClient] = useState(null); - const prevChainName = useRef(chainName); - - const getClient = async () => { - if (chainName === prevChainName.current && osmosisClient) - return osmosisClient; - - prevChainName.current = chainName; - const newClient = await getClientRequest(); - setOsmosisClient(newClient); - return newClient; - }; - - const getAllBalances = async (arg: QueryAllBalancesRequest) => { - const client = await getClient(); - const { balances } = await client.cosmos.bank.v1beta1.allBalances(arg); - return balances; - }; - - const getDelegations = async (arg: QueryDelegatorDelegationsRequest) => { - const client = await getClient(); - const { delegationResponses } = - await client.cosmos.staking.v1beta1.delegatorDelegations(arg); - return delegationResponses; - }; - - const getSuperfluidAssets = async () => { - const client = await getClient(); - const { assets: superfluidAssets } = - await client.osmosis.superfluid.allAssets(); - return superfluidAssets; - }; - - const getLockedCoins = async (arg: AccountLockedCoinsRequest) => { - const client = await getClient(); - const { coins: lockedCoins } = - await client.osmosis.lockup.accountLockedCoins(arg); - return lockedCoins; - }; - - const getLockableDurations = async () => { - const client = await getClient(); - const { lockableDurations } = - await client.osmosis.incentives.lockableDurations(); - return lockableDurations; - }; - - const getLocks = async (arg: AccountLockedLongerDurationRequest) => { - const client = await getClient(); - const { locks } = await client.osmosis.lockup.accountLockedLongerDuration( - arg - ); - return locks; - }; - - const getSuperfluidDelegations = async ( - arg: SuperfluidDelegationsByDelegatorRequest - ) => { - const client = await getClient(); - const { totalDelegatedCoins } = - await client.osmosis.superfluid.superfluidDelegationsByDelegator(arg); - return totalDelegatedCoins; - }; - - const getAllPools = async (prices: PriceHash) => { - const client = await getClient(); - const { pools } = (await client.osmosis.gamm.v1beta1.pools({ - pagination: { - key: new Uint8Array(), - offset: BigInt(0), - limit: BigInt(2000), - countTotal: false, - reverse: false, - }, - })) as { pools: Pool[] }; - const formattedPools = pools - .filter(({ $typeUrl }) => !$typeUrl?.includes('stableswap')) - .filter((pool) => removeUnsupportedPools(pool, prices)); - return formattedPools; - }; - - const getActiveGauges = async (arg: ActiveGaugesPerDenomRequest) => { - const client = await getClient(); - const res = await client.osmosis.incentives.activeGaugesPerDenom(arg); - return res; - }; - - return { - getAllPools: useRequest(getAllPools), - getLockedCoins: useRequest(getLockedCoins), - getDelegations: useRequest(getDelegations), - getAllBalances: useRequest(getAllBalances), - getActiveGauges, - getSuperfluidAssets: - useRequest(getSuperfluidAssets), - getLockableDurations: - useRequest(getLockableDurations), - getLocks: useRequest(getLocks), - getSuperfluidDelegations: useRequest( - getSuperfluidDelegations - ), - }; -}; diff --git a/examples/provide-liquidity/hooks/useQueuedRequests.ts b/examples/provide-liquidity/hooks/useQueuedRequests.ts deleted file mode 100644 index bfd819dd7..000000000 --- a/examples/provide-liquidity/hooks/useQueuedRequests.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useState } from 'react'; - -const splitIntoChunks = (arr: T[], chunkSize: number): T[][] => { - const res = []; - for (let i = 0; i < arr.length; i += chunkSize) { - const chunk = arr.slice(i, i + chunkSize); - res.push(chunk); - } - return res; -}; - -export const useQueuedRequests = ({ - queueLength, - queueGap, -}: { - queueLength: number; - queueGap?: number; -}) => { - const [data, setData] = useState([]); - const [error, setError] = useState(); - const [loading, setLoading] = useState(false); - - const sendQueuedRequests = async (requests: Promise[]) => { - setLoading(true); - let results: T[] = []; - - const requestQueues = splitIntoChunks>(requests, queueLength); - - for (const [index, requestQueue] of Object.entries(requestQueues)) { - let queueResult: T[] = []; - - try { - queueResult = await Promise.all(requestQueue); - results = [...results, ...queueResult]; - } catch (error) { - console.error(error); - setError(error || 'Unexpected Error!'); - break; - } - - if (Number(index) !== requestQueues.length - 1 && queueGap) { - await new Promise((resolve) => setTimeout(resolve, queueGap)); - } - } - - setData(results); - setLoading(false); - }; - - return { - data, - error, - loading, - sendQueuedRequests, - }; -}; diff --git a/examples/provide-liquidity/hooks/useRequest.ts b/examples/provide-liquidity/hooks/useRequest.ts deleted file mode 100644 index 21a93ec61..000000000 --- a/examples/provide-liquidity/hooks/useRequest.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useState } from 'react'; - -export const useRequest = any>( - requestFunc: (...args: Parameters) => ReturnType -) => { - const [data, setData] = useState>>(); - const [error, setError] = useState(); - const [loading, setLoading] = useState(false); - - const request = async (...args: Parameters) => { - setLoading(true); - try { - const result = await requestFunc(...args); - setData(result); - } catch (err) { - setError(err || 'Unexpected Error!'); - } finally { - setLoading(false); - } - }; - - const cleanUpData = () => { - setData(undefined); - }; - - return { - data, - error, - loading, - request, - cleanUpData, - }; -}; diff --git a/examples/provide-liquidity/hooks/useToaster.tsx b/examples/provide-liquidity/hooks/useToaster.tsx new file mode 100644 index 000000000..711d8a74f --- /dev/null +++ b/examples/provide-liquidity/hooks/useToaster.tsx @@ -0,0 +1,49 @@ +import { useToast, Text, Box } from '@chakra-ui/react'; + +export enum ToastType { + Info = 'info', + Error = 'error', + Success = 'success', + Loading = 'loading' +} + +export type CustomToast = { + title: string; + type: ToastType; + message?: string | JSX.Element; + closable?: boolean; + duration?: number; +}; + +export const useToaster = () => { + const toast = useToast({ + position: 'top-right', + containerStyle: { + maxWidth: '150px' + } + }); + + const customToast = ({ + type, + title, + message, + closable = true, + duration = 5000 + }: CustomToast) => { + return toast({ + title, + duration, + status: type, + isClosable: closable, + description: ( + + + {message} + + + ) + }); + }; + + return { ...toast, toast: customToast }; +}; diff --git a/examples/provide-liquidity/hooks/useTransactionToast.tsx b/examples/provide-liquidity/hooks/useTransactionToast.tsx deleted file mode 100644 index 621be8020..000000000 --- a/examples/provide-liquidity/hooks/useTransactionToast.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useToast, Text, Box } from '@chakra-ui/react'; -import { TransactionResult } from '../components/types'; - -export const useTransactionToast = () => { - const toast = useToast({ - position: 'top-right', - containerStyle: { - maxWidth: '150px', - }, - }); - - const showToast = (code: number, res?: any) => { - toast({ - title: `Transaction ${ - code === TransactionResult.Success ? 'successful' : 'failed' - }`, - status: code === TransactionResult.Success ? 'success' : 'error', - duration: code === TransactionResult.Success ? 5000 : 20000, - isClosable: true, - description: ( - - {res?.message} - {res?.rawLog} - - ), - }); - }; - - return { showToast }; -}; diff --git a/examples/provide-liquidity/hooks/useTx.ts b/examples/provide-liquidity/hooks/useTx.ts new file mode 100644 index 000000000..92f26228b --- /dev/null +++ b/examples/provide-liquidity/hooks/useTx.ts @@ -0,0 +1,115 @@ +import { cosmos } from 'osmo-query'; +import { isDeliverTxSuccess, StdFee } from '@cosmjs/stargate'; +import { useToaster, ToastType, type CustomToast } from './useToaster'; +import { useChain } from '@cosmos-kit/react'; +import { ToastId } from '@chakra-ui/react'; +import { TxRaw } from 'osmo-query/dist/codegen/cosmos/tx/v1beta1/tx'; + +interface Msg { + typeUrl: string; + value: any; +} + +interface TxOptions { + fee?: StdFee | null; + toast?: Partial; + onSuccess?: () => void; +} + +export enum TxStatus { + Failed = 'Transaction Failed', + Successful = 'Transaction Successful', + Broadcasting = 'Transaction Broadcasting', +} + +const txRaw = cosmos.tx.v1beta1.TxRaw; + +export const useTx = (chainName: string) => { + const { address, getSigningStargateClient, estimateFee } = + useChain(chainName); + + const toaster = useToaster(); + + const tx = async (msgs: Msg[], options: TxOptions) => { + if (!address) { + toaster.toast({ + type: ToastType.Error, + title: 'Wallet not connected', + message: 'Please connect the wallet', + }); + return; + } + + let signed: TxRaw; + let client: Awaited>; + + try { + let fee: StdFee; + if (options?.fee) { + fee = options.fee; + client = await getSigningStargateClient(); + } else { + const [_fee, _client] = await Promise.all([ + estimateFee(msgs), + getSigningStargateClient(), + ]); + fee = _fee; + client = _client; + } + signed = await client.sign(address, msgs, fee, ''); + } catch (e: any) { + console.error(e); + toaster.toast({ + title: TxStatus.Failed, + message: e?.message || 'An unexpected error has occured', + type: ToastType.Error, + }); + return; + } + + let broadcastToastId: ToastId; + + broadcastToastId = toaster.toast({ + title: TxStatus.Broadcasting, + message: 'Waiting for transaction to be included in the block', + type: ToastType.Loading, + duration: 999999, + }); + + if (client && signed) { + await client + .broadcastTx(Uint8Array.from(txRaw.encode(signed).finish())) + .then((res) => { + if (isDeliverTxSuccess(res)) { + if (options.onSuccess) options.onSuccess(); + + toaster.toast({ + title: options.toast?.title || TxStatus.Successful, + type: options.toast?.type || ToastType.Success, + message: options.toast?.message, + }); + } else { + toaster.toast({ + title: TxStatus.Failed, + message: res?.rawLog, + type: ToastType.Error, + duration: 10000, + }); + } + }) + .catch((err) => { + toaster.toast({ + title: TxStatus.Failed, + message: err?.message, + type: ToastType.Error, + duration: 10000, + }); + }) + .finally(() => toaster.close(broadcastToastId)); + } else { + toaster.close(broadcastToastId); + } + }; + + return { tx }; +}; diff --git a/examples/provide-liquidity/package.json b/examples/provide-liquidity/package.json index ac9ff0c79..6b071ff3a 100644 --- a/examples/provide-liquidity/package.json +++ b/examples/provide-liquidity/package.json @@ -28,18 +28,19 @@ "@emotion/react": "11.10.6", "@emotion/styled": "11.10.6", "@osmonauts/math": "1.7.0", + "@tanstack/react-query": "4.32.0", "bignumber.js": "9.1.0", "chain-registry": "1.17.1", "dayjs": "^1.11.7", "framer-motion": "9.0.7", - "long": "^5.2.1", "next": "12.2.5", - "osmojs": "16.5.1", + "osmo-query": "16.5.1", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "4.6.0" }, "devDependencies": { + "@tanstack/react-query-devtools": "4.32.0", "@types/node": "18.11.9", "@types/react": "18.0.25", "@types/react-dom": "18.0.9", diff --git a/examples/provide-liquidity/pages/_app.tsx b/examples/provide-liquidity/pages/_app.tsx index e38c174a5..196a123af 100644 --- a/examples/provide-liquidity/pages/_app.tsx +++ b/examples/provide-liquidity/pages/_app.tsx @@ -1,4 +1,3 @@ -import '../styles/globals.css'; import type { AppProps } from 'next/app'; import { ChainProvider } from '@cosmos-kit/react'; import { ChakraProvider } from '@chakra-ui/react'; @@ -6,12 +5,24 @@ import { aminoTypes, registry } from '../config/defaults'; import { wallets as keplrWallets } from '@cosmos-kit/keplr'; import { wallets as cosmostationWallets } from '@cosmos-kit/cosmostation'; import { wallets as leapWallets } from '@cosmos-kit/leap'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +// import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { assets, chains } from 'chain-registry'; import { GasPrice } from '@cosmjs/stargate'; import { SignerOptions } from '@cosmos-kit/core'; -import '@interchain-ui/react/styles'; import { defaultTheme } from '../config'; +import '@interchain-ui/react/styles'; +import '../styles/globals.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 2, + refetchOnWindowFocus: false, + }, + }, +}); function CreateCosmosApp({ Component, pageProps }: AppProps) { const signerOptions: SignerOptions = { @@ -52,7 +63,9 @@ function CreateCosmosApp({ Component, pageProps }: AppProps) { }, }} > - + + + ); diff --git a/examples/provide-liquidity/pages/index.tsx b/examples/provide-liquidity/pages/index.tsx index 8eb6a638c..93f31daef 100644 --- a/examples/provide-liquidity/pages/index.tsx +++ b/examples/provide-liquidity/pages/index.tsx @@ -1,10 +1,17 @@ +import { useEffect, useState } from 'react'; import { WalletSection, ProvideLiquidity, Layout } from '@/components'; export default function Home() { + const [content, setContent] = useState(); + + useEffect(() => { + setContent(); + }, []); + return ( - + {content} ); } diff --git a/examples/provide-liquidity/utils/apr.ts b/examples/provide-liquidity/utils/apr.ts index db53ec67c..5b109d0de 100644 --- a/examples/provide-liquidity/utils/apr.ts +++ b/examples/provide-liquidity/utils/apr.ts @@ -1,14 +1,6 @@ -import { Asset } from '@chain-registry/types'; -import { asset_list, assets } from '@chain-registry/osmosis'; import { calcPoolAprs as _calcPoolAprs } from '@osmonauts/math'; - import { CalcPoolAprsParams } from './types'; -const osmosisAssets: Asset[] = [ - ...assets.assets, - ...asset_list.assets, -]; - export const calcPoolAprs = ({ activeGauges, pool, @@ -18,13 +10,14 @@ export const calcPoolAprs = ({ lockupDurations, volume7d, swapFee, + assets, lockup = '14', includeNonPerpetual = true, }: CalcPoolAprsParams) => { return _calcPoolAprs({ activeGauges, pool, - assets: osmosisAssets, + assets, prices, superfluidPools, aprSuperfluid, diff --git a/examples/provide-liquidity/utils/pool.ts b/examples/provide-liquidity/utils/pool.ts index fa6a7027e..52720c16e 100644 --- a/examples/provide-liquidity/utils/pool.ts +++ b/examples/provide-liquidity/utils/pool.ts @@ -13,8 +13,8 @@ import { convertDollarValueToDenomUnits, convertBaseUnitsToDisplayUnits, } from '@chain-registry/utils'; -import { Pool } from 'osmojs/dist/codegen/osmosis/gamm/pool-models/balancer/balancerPool'; -import { Coin } from 'osmojs/dist/codegen/cosmos/base/v1beta1/coin'; +import { Pool } from 'osmo-query/dist/codegen/osmosis/gamm/pool-models/balancer/balancerPool'; +import { Coin } from 'osmo-query/dist/codegen/cosmos/base/v1beta1/coin'; import { calcPoolLiquidity as _calcPoolLiquidity, getPoolByGammName as _getPoolByGammName, @@ -40,18 +40,20 @@ import { CoinBalance, PoolAssetPretty, } from './types'; -import { Fee } from '../components'; +import { Fee } from '@/hooks'; export const osmosisAssets: Asset[] = [ ...assets.assets, ...asset_list.assets, -]; +].filter(({ type_asset }) => type_asset !== 'ics20'); export const getOsmoAssetByDenom = (denom: CoinDenom): Asset => { return getAssetByDenom(osmosisAssets, denom); }; -export const getDenomForCoinGeckoId = (coinGeckoId: CoinGeckoToken): CoinDenom => { +export const getDenomForCoinGeckoId = ( + coinGeckoId: CoinGeckoToken +): CoinDenom => { return getDenomByCoinGeckoId(osmosisAssets, coinGeckoId); }; @@ -63,7 +65,9 @@ export const getExponentByDenom = (denom: CoinDenom): Exponent => { return _getExponentByDenom(osmosisAssets, denom); }; -export const convertGeckoPricesToDenomPriceHash = (prices: CoinGeckoUSDResponse): PriceHash => { +export const convertGeckoPricesToDenomPriceHash = ( + prices: CoinGeckoUSDResponse +): PriceHash => { return convertCoinGeckoPricesToDenomPriceMap(osmosisAssets, prices); }; @@ -99,7 +103,10 @@ export const convertDollarValueToShares = ( return _convertDollarValueToShares(osmosisAssets, value, pool, prices); }; -export const prettyPool = (pool: Pool, { includeDetails = false } = {}): PoolPretty => { +export const prettyPool = ( + pool: Pool, + { includeDetails = false } = {} +): PoolPretty => { return _prettyPool(osmosisAssets, pool, { includeDetails }); }; @@ -127,7 +134,10 @@ export const dollarValueToDenomUnits = ( return convertDollarValueToDenomUnits(osmosisAssets, prices, symbol, value); }; -export const baseUnitsToDisplayUnits = (symbol: string, amount: string | number) => { +export const baseUnitsToDisplayUnits = ( + symbol: string, + amount: string | number +) => { return convertBaseUnitsToDisplayUnits(osmosisAssets, symbol, amount); }; @@ -155,17 +165,41 @@ export const calcMaxCoinsForPool = ( return _calcMaxCoinsForPool(osmosisAssets, prices, poolInfo, balances); }; -export const calcShareOutAmount = (poolInfo: Pool, coinsNeeded: Coin[]): string => { +export const calcShareOutAmount = ( + poolInfo: Pool, + coinsNeeded: Coin[] +): string => { return _calcShareOutAmount(poolInfo, coinsNeeded); }; -export const addPropertiesToPool = ( - pool: Pool, - fees: Fee[], - balances: Coin[], - lockedCoins: Coin[], - prices: PriceHash -) => { +export const filterPools = (pools: Pool[], prices: PriceHash) => { + return pools + .filter(({ $typeUrl }) => !$typeUrl?.includes('stableswap')) + .filter(({ poolAssets }) => + poolAssets.every( + ({ token }) => + prices[token.denom] && + !token.denom.startsWith('gamm/pool') && + osmosisAssets.find(({ base }) => base === token.denom) + ) + ); +}; + +interface ExtendPoolProps { + pool: Pool; + fees: Fee[]; + balances: Coin[]; + lockedCoins: Coin[]; + prices: PriceHash; +} + +export const extendPool = ({ + pool, + fees, + balances, + lockedCoins, + prices, +}: ExtendPoolProps) => { const liquidity = new BigNumber(calcPoolLiquidity(pool, prices)) .decimalPlaces(0) .toNumber(); @@ -175,34 +209,49 @@ export const addPropertiesToPool = ( const volume7d = Math.round(Number(feeData?.volume_7d || 0)); const fees7D = Math.round(Number(feeData?.fees_spent_7d || 0)); - const balanceCoin = balances.find( - ({ denom }) => denom === pool.totalShares?.denom - ); + const poolDenom = pool.totalShares?.denom; + + const balanceCoin = balances.find(({ denom }) => denom === poolDenom); const myLiquidity = balanceCoin ? convertGammTokenToDollarValue(balanceCoin, pool, prices) : 0; - const lockedCoin = lockedCoins.find( - ({ denom }) => denom === pool.totalShares?.denom - ); + const lockedCoin = lockedCoins.find(({ denom }) => denom === poolDenom); const bonded = lockedCoin ? convertGammTokenToDollarValue(lockedCoin, pool, prices) : 0; - const apr = { - 1: { totalApr: '0' }, - 7: { totalApr: '0' }, - 14: { totalApr: '0' }, - }; - return { ...pool, liquidity, volume24H, fees7D, volume7d, - apr, myLiquidity, bonded, + denom: poolDenom, }; }; + +export type ExtendedPool = ReturnType; + +export const descByLiquidity = (pool1: ExtendedPool, pool2: ExtendedPool) => { + return new BigNumber(pool1.liquidity).lt(pool2.liquidity) ? 1 : -1; +}; + +export const descByMyLiquidity = (pool1: ExtendedPool, pool2: ExtendedPool) => { + return new BigNumber(pool1.myLiquidity).lt(pool2.myLiquidity) ? 1 : -1; +}; + +type Item = { + denom: string; + [key: string]: any; +}; + +export const getPoolsByDenom = (allPools: ExtendedPool[], items: Item[]) => { + return items + .filter(({ denom }) => allPools.find(({ denom: d }) => d === denom)) + .map(({ denom }) => { + return allPools.find(({ denom: d }) => d === denom) as ExtendedPool; + }); +}; diff --git a/yarn.lock b/yarn.lock index 689309d27..14042f93f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13703,7 +13703,7 @@ osmojs@15.5.0: long "^5.2.0" protobufjs "^6.11.3" -osmojs@16.5.1, osmojs@^16.5.1: +osmojs@^16.5.1: version "16.5.1" resolved "https://registry.npmmirror.com/osmojs/-/osmojs-16.5.1.tgz#38f2fe3cd65dbd4e4b415e9f22f387a24b634155" integrity sha512-V2Q2yMt7Paax6i+S5Q1l29Km0As/waXKmSVRe8gtd9he42kcbkpwwk/QUCvgS98XsL7Qz+vas2Tca016uBQHTQ==