diff --git a/package.json b/package.json index d49baa4..c57a342 100755 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "@nikolovlazar/chakra-ui-prose": "1.2.1", "@prisma/client": "5.18.0", "@starknet-react/chains": "0.1.7", + "@apollo/client": "^3.11.8", + "graphql": "^16.9.0", "@starknet-react/core": "2.8.0", "@tanstack/query-core": "5.28.0", "@types/mixpanel-browser": "2.49.0", diff --git a/src/app/strategy/[strategyId]/_components/Strategy.tsx b/src/app/strategy/[strategyId]/_components/Strategy.tsx index 521ef73..7de0ce1 100755 --- a/src/app/strategy/[strategyId]/_components/Strategy.tsx +++ b/src/app/strategy/[strategyId]/_components/Strategy.tsx @@ -4,18 +4,12 @@ import { Avatar, Box, Card, - Center, Flex, Grid, GridItem, Link, ListItem, OrderedList, - Spinner, - Stat, - StatHelpText, - StatLabel, - StatNumber, Tab, TabIndicator, TabList, @@ -23,26 +17,21 @@ import { TabPanels, Tabs, Text, - Tooltip, VStack, - Wrap, - WrapItem, } from '@chakra-ui/react'; 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, - getUniqueById, shortAddress, timeAgo, } from '@/utils'; @@ -177,139 +166,14 @@ const Strategy = ({ params }: StrategyParams) => { {strategy ? strategy.name : 'Strategy Not found'} + {strategy && ( - - - - - How does it work? - - - {strategy.description} - - - {getUniqueById( - strategy.actions.map((p) => ({ - id: p.pool.protocol.name, - logo: p.pool.protocol.logo, - })), - ).map((p) => ( - -
- - {p.id} -
-
- ))} -
-
- - - - APY - - - {(strategy.netYield * 100).toFixed(2)}% - - - {strategy.leverage.toFixed(2)}x boosted - - - -
- - {!balData.isLoading && - !balData.isError && - !balData.isPending && - balData.data && - balData.data.tokenInfo && ( - - - - Your Holdings - - - {address - ? Number( - balData.data.amount.toEtherToFixedDecimals( - balData.data.tokenInfo?.displayDecimals || - 2, - ), - ) == 0 - ? '-' - : `${balData.data.amount.toEtherToFixedDecimals(balData.data.tokenInfo?.displayDecimals || 2)} ${balData.data.tokenInfo?.name}` - : isMobile - ? CONSTANTS.MOBILE_MSG - : 'Connect wallet'} - - - - - - Net earnings - - = 0 ? 'cyan' : 'red'} - > - {address && profit != 0 - ? `${profit?.toFixed(balData.data.tokenInfo?.displayDecimals || 2)} ${balData.data.tokenInfo?.name}` - : '-'} - - - - - )} - {(balData.isLoading || - balData.isPending || - !balData.data?.tokenInfo) && ( - - Your Holdings: - {address ? ( - - ) : isMobile ? ( - CONSTANTS.MOBILE_MSG - ) : ( - 'Connect wallet' - )} - - )} - {balData.isError && ( - - Your Holdings: Error - - )} - -
+
+ @@ -370,6 +234,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..e735860 --- /dev/null +++ b/src/components/HarvestTime.tsx @@ -0,0 +1,248 @@ +import React, { useMemo } from 'react'; +import { + Avatar, + Box, + Card, + Center, + Spinner, + Stat, + StatHelpText, + StatLabel, + StatNumber, + Text, + Wrap, + WrapItem, +} 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 { formatTimestamp, getUniqueById } from '@/utils'; +import CONSTANTS from '@/constants'; +import { isMobile } from 'react-device-detect'; + +interface HarvestTimeProps { + strategy: StrategyInfo; + balData: any; +} + +const HarvestTime: React.FC = ({ strategy, balData }) => { + const { address } = useAccount(); + const contractAddress = strategy.holdingTokens[0].address ?? ''; + + const harvestTimeAtom = useMemo( + () => HarvestTimeAtom(contractAddress), + [address], + ); + + const harvestTime = useAtomValue(harvestTimeAtom); + + const data = harvestTime.data?.findManyHarvests[0]; + + const harvestTimestamp = useMemo(() => { + if (!data?.timestamp) return null; + return formatTimestamp(data.timestamp); + }, [data?.timestamp]); + + return ( + + + + + + APY + + + {(strategy.netYield * 100).toFixed(2)}% + + + {strategy.leverage.toFixed(2)}x boosted + + + + + + + + Next Harvest in: + + + + + Mon + + + {harvestTimestamp?.month ?? 0} + + + + + + Days + + + {harvestTimestamp?.day ?? 0} + + + + + + Hour + + + {harvestTimestamp?.hour ?? 0} + + + + + + Mins + + + {harvestTimestamp?.minute ?? 0} + + + + + + + + + Total rewards harvested:{' '} + {harvestTime?.data?.totalStrkHarvestedByContract.STRKAmount} + + {} | Total number of times harvested:{' '} + {harvestTime?.data?.totalHarvestsByContract} + + + + + + + + How does it work? + + + {strategy.description} + + + {getUniqueById( + strategy.actions.map((p) => ({ + id: p.pool.protocol.name, + logo: p.pool.protocol.logo, + })), + ).map((p) => ( + +
+ + {p.id} +
+
+ ))} +
+
+
+ + + {!balData.isLoading && + !balData.isError && + !balData.isPending && + balData.data && + balData.data.tokenInfo && ( + + Your Holdings: + {address + ? `${balData.data.amount.toEtherToFixedDecimals(4)} ${balData.data.tokenInfo?.name}` + : isMobile + ? CONSTANTS.MOBILE_MSG + : 'Connect wallet'} + + )} + {(balData.isLoading || + balData.isPending || + !balData.data?.tokenInfo) && ( + + Your Holdings: + {address ? ( + + ) : isMobile ? ( + CONSTANTS.MOBILE_MSG + ) : ( + 'Connect wallet' + )} + + )} + {balData.isError && ( + + Your Holdings: Error + + )} + +
+
+ ); +}; + +export default HarvestTime; diff --git a/src/store/harvest.atom.ts b/src/store/harvest.atom.ts new file mode 100644 index 0000000..60b89c7 --- /dev/null +++ b/src/store/harvest.atom.ts @@ -0,0 +1,79 @@ +import { gql } from '@apollo/client'; +import apolloClient from '@/utils/apolloClient'; +import { atomWithQuery } from 'jotai-tanstack-query'; + +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: contract, + }, + }, + take, + orderBy, + 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/strategies/IStrategy.ts b/src/strategies/IStrategy.ts index 67265f2..d17e3e8 100755 --- a/src/strategies/IStrategy.ts +++ b/src/strategies/IStrategy.ts @@ -25,6 +25,7 @@ export interface TokenInfo { token: string; decimals: number; displayDecimals: number; + address?: string; name: string; logo: any; minAmount: MyNumber; diff --git a/src/utils.ts b/src/utils.ts index c4211f2..85c7102 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -108,6 +108,17 @@ export function getDisplayCurrencyAmount( return Number(Number(amount).toFixed(decimals)).toLocaleString(); } +export function formatTimestamp(timestamp: string) { + const date = new Date(parseInt(timestamp, 10) * 1000); + return { + day: date.getUTCDate(), + month: date.getUTCMonth() + 1, + hour: date.getUTCHours(), + minute: date.getUTCMinutes(), + second: date.getUTCSeconds(), + }; +} + export function copyReferralLink(refCode: string) { navigator.clipboard.writeText(getReferralUrl(refCode)); diff --git a/yarn.lock b/yarn.lock index 24bb649..e908588 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,7 +15,11 @@ framer-motion "^6.3.11" lodash.union "^4.6.0" +<<<<<<< HEAD +"@apollo/client@^3.11.8": +======= "@apollo/client@3.11.8": +>>>>>>> e9f234a0c9a1a6d378aa3ac9ba7310f3486707ef version "3.11.8" resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.11.8.tgz#f6bacdc7e1b243807c1387113e1d445a53471a9c" integrity sha512-CgG1wbtMjsV2pRGe/eYITmV5B8lXUCYljB2gB/6jWTFQcrvirUVvKg7qtFdjYkQSFbIffU1IDyxgeaN81eTjbA== @@ -4652,7 +4656,11 @@ graphql-tag@^2.12.6: dependencies: tslib "^2.1.0" +<<<<<<< HEAD +graphql@^16.9.0: +======= graphql@16.9.0: +>>>>>>> e9f234a0c9a1a6d378aa3ac9ba7310f3486707ef version "16.9.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.9.0.tgz#1c310e63f16a49ce1fbb230bd0a000e99f6f115f" integrity sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw== @@ -6494,6 +6502,8 @@ rehackt@^0.1.0: resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.1.0.tgz#a7c5e289c87345f70da8728a7eb878e5d03c696b" integrity sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw== +<<<<<<< HEAD +======= request-promise-core@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" @@ -6501,6 +6511,7 @@ request-promise-core@1.1.3: dependencies: lodash "^4.17.15" +>>>>>>> e9f234a0c9a1a6d378aa3ac9ba7310f3486707ef require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -7321,6 +7332,8 @@ tslib@^2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== +<<<<<<< HEAD +======= tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -7333,6 +7346,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== +>>>>>>> e9f234a0c9a1a6d378aa3ac9ba7310f3486707ef type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -7799,11 +7813,14 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== +<<<<<<< HEAD +======= yoctocolors-cjs@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== +>>>>>>> e9f234a0c9a1a6d378aa3ac9ba7310f3486707ef zen-observable-ts@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58"