diff --git a/src/app/strategy/[strategyId]/_components/Strategy.tsx b/src/app/strategy/[strategyId]/_components/Strategy.tsx index 521ef73..d986687 100755 --- a/src/app/strategy/[strategyId]/_components/Strategy.tsx +++ b/src/app/strategy/[strategyId]/_components/Strategy.tsx @@ -12,10 +12,6 @@ import { ListItem, OrderedList, Spinner, - Stat, - StatHelpText, - StatLabel, - StatNumber, Tab, TabIndicator, TabList, @@ -32,13 +28,12 @@ import { useAccount } from '@starknet-react/core'; import { atom, useAtomValue, useSetAtom } from 'jotai'; import mixpanel from 'mixpanel-browser'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { isMobile } from 'react-device-detect'; import Deposit from '@/components/Deposit'; -import CONSTANTS from '@/constants'; import { DUMMY_BAL_ATOM } from '@/store/balance.atoms'; import { StrategyInfo, strategiesAtom } from '@/store/strategies.atoms'; import { transactionsAtom, TxHistoryAtom } from '@/store/transactions.atom'; +import HarvestTime from '@/components/HarvestTime'; import { capitalize, getTokenInfoFromAddr, @@ -49,6 +44,8 @@ import { import { StrategyParams } from '../page'; import MyNumber from '@/utils/MyNumber'; import { ExternalLinkIcon } from '@chakra-ui/icons'; +import { isMobile } from 'react-device-detect'; +import CONSTANTS from '@/constants'; const Strategy = ({ params }: StrategyParams) => { const { address } = useAccount(); @@ -177,13 +174,15 @@ const Strategy = ({ params }: StrategyParams) => { {strategy ? strategy.name : 'Strategy Not found'} + {strategy && ( - - + + + { ))} - - - - APY - - - {(strategy.netYield * 100).toFixed(2)}% - - - {strategy.leverage.toFixed(2)}x boosted - - - { + @@ -370,6 +350,7 @@ const Strategy = ({ params }: StrategyParams) => { + Behind the scenes diff --git a/src/components/HarvestTime.tsx b/src/components/HarvestTime.tsx new file mode 100644 index 0000000..63adf6c --- /dev/null +++ b/src/components/HarvestTime.tsx @@ -0,0 +1,224 @@ +import React, { useMemo } from 'react'; +import { + Box, + Flex, + Stat, + StatLabel, + StatNumber, + Tag, + Text, + Tooltip, +} from '@chakra-ui/react'; +import { useAccount } from '@starknet-react/core'; +import { StrategyInfo } from '@/store/strategies.atoms'; +import { HarvestTimeAtom } from '@/store/harvest.atom'; +import { useAtomValue } from 'jotai'; +import { formatTimediff, getDisplayCurrencyAmount, timeAgo } from '@/utils'; +import { isMobile } from 'react-device-detect'; + +interface HarvestTimeProps { + strategy: StrategyInfo; + balData: any; +} + +const HarvestTime: React.FC = ({ strategy, balData }) => { + const { address } = useAccount(); + const holdingToken: any = strategy.holdingTokens[0]; + const contractAddress = holdingToken.address || holdingToken.token || ''; + + const harvestTimeAtom = useMemo( + () => HarvestTimeAtom(contractAddress), + [address], + ); + + const harvestTime = useAtomValue(harvestTimeAtom); + + const data = harvestTime.data?.findManyHarvests[0]; + + const lastHarvest = useMemo(() => { + if (!data || !data.timestamp) return null; + return new Date(Number(data.timestamp) * 1000); + }, [data?.timestamp]); + + const harvestTimestamp = useMemo(() => { + const DAYMS = 86400 * 1000; + // Base date is last harvest time + 2 days or now (for no harvest strats) + const baseDate = lastHarvest + ? new Date(lastHarvest.getTime() + 2 * DAYMS) + : new Date(); + + // With base date, get next sunday 12am UTC + // set date to coming sunday in UTC + const nextHarvest = baseDate; + nextHarvest.setUTCDate( + nextHarvest.getUTCDate() + (7 - nextHarvest.getUTCDay()), + ); + nextHarvest.setUTCHours(0); + nextHarvest.setUTCMinutes(0); + nextHarvest.setUTCSeconds(0); + + // if nextHarvest is within 24hrs of last harvest, + // increase it by 7 days + // This is needed as harvest can happen anytime near deadline + if ( + lastHarvest && + nextHarvest.getTime() - lastHarvest.getTime() < 86400 * 1000 + ) { + nextHarvest.setUTCDate(nextHarvest.getUTCDate() + 7); + } + + return formatTimediff(nextHarvest); + }, [data?.timestamp]); + + return ( + + + + + APY + + {(strategy.netYield * 100).toFixed(2)}% + + + + + + 🔥{strategy.leverage.toFixed(2)}x boosted + + + + + + {!isMobile && ( + + + + Next Harvest in:{' '} + {harvestTimestamp.isZero && ( + + Anytime now + + )} + + + + + Days + + + {harvestTimestamp.days ?? 0} + + + + + + Hour + + + {harvestTimestamp.hours ?? 0} + + + + + + Mins + + + {harvestTimestamp.minutes ?? 0} + + + + + + )} + + + + + Harvested{' '} + + {getDisplayCurrencyAmount( + harvestTime?.data?.totalStrkHarvestedByContract.STRKAmount || 0, + 2, + )}{' '} + STRK + {' '} + over {harvestTime?.data?.totalHarvestsByContract} claims.{' '} + {lastHarvest && ( + + Last harvested {timeAgo(lastHarvest)}. + + )} + + + + ); +}; + +export default HarvestTime; diff --git a/src/store/harvest.atom.ts b/src/store/harvest.atom.ts new file mode 100644 index 0000000..73413d0 --- /dev/null +++ b/src/store/harvest.atom.ts @@ -0,0 +1,80 @@ +import { gql } from '@apollo/client'; +import apolloClient from '@/utils/apolloClient'; +import { atomWithQuery } from 'jotai-tanstack-query'; +import { standariseAddress } from '@/utils'; + +interface HarvestTime { + contract: string; + amount: string; + timestamp: string; +} + +interface QueryResponse { + findManyHarvests: HarvestTime[]; + totalHarvestsByContract: number; + totalStrkHarvestedByContract: { + STRKAmount: number; + USDValue: number; + rawSTRKAmount: string; + }; +} + +// GraphQL query +const GET_HARVESTS_QUERY = gql` + query Query( + $where: HarvestsWhereInput + $take: Int + $orderBy: [HarvestsOrderByWithRelationInput!] + $contract: String! + ) { + findManyHarvests(where: $where, take: $take, orderBy: $orderBy) { + contract + amount + timestamp + } + totalHarvestsByContract(contract: $contract) + totalStrkHarvestedByContract(contract: $contract) { + STRKAmount + USDValue + rawSTRKAmount + } + } +`; + +// Function to execute the query +async function getHarvestData( + contract: string, + take: number = 1, + orderBy: any = [{ timestamp: 'desc' }], +): Promise { + try { + const { data } = await apolloClient.query({ + query: GET_HARVESTS_QUERY, + variables: { + where: { + contract: { + equals: standariseAddress(contract), + }, + }, + take, + orderBy, + contract: standariseAddress(contract), + }, + }); + + // Return the data + return data; + } catch (error) { + console.error('Error fetching harvest data:', error); + return null; + } +} + +export const HarvestTimeAtom = (contract: string) => + atomWithQuery((get) => ({ + queryKey: ['harvest_data', contract], + queryFn: async () => { + const result = await getHarvestData(contract); + return result; + }, + })); diff --git a/src/utils.ts b/src/utils.ts index c4211f2..3f50a65 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -108,6 +108,35 @@ export function getDisplayCurrencyAmount( return Number(Number(amount).toFixed(decimals)).toLocaleString(); } +// returns time to endtime in days, hours, minutes +export function formatTimediff(endTime: Date) { + const now = new Date(); + if (now.getTime() >= endTime.getTime()) { + return { + days: 0, + hours: 0, + minutes: 0, + isZero: true, + }; + } + + // else return number of days, months, weeks, hours, minutrs, seconds to endtime + const diff = endTime.getTime() - now.getTime(); + // get days floor + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + // after accounting days, get remaining hours + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + // after accounting days and hours, get remaining minutes + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + + return { + days, + hours, + minutes, + isZero: false, + }; +} + export function copyReferralLink(refCode: string) { navigator.clipboard.writeText(getReferralUrl(refCode)); @@ -134,9 +163,9 @@ export async function getPriceFromMyAPI(tokenInfo: TokenInfo) { console.log('getPrice from redis', tokenInfo.name); const endpoint = - typeof window === 'undefined' + (typeof window === 'undefined' ? process.env.HOSTNAME - : window.location.origin; + : window.location.origin) || 'https://app.strkfarm.xyz'; const priceInfo = await axios.get(`${endpoint}/api/price/${tokenInfo.name}`); const now = new Date(); const priceTime = new Date(priceInfo.data.timestamp);