diff --git a/.husky/pre-commit b/.husky/pre-commit index 3ef8c522..f94cc390 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ -yarn run lint:fix && yarn run format:fix && git add . \ No newline at end of file +STAGED_FILES=$(git diff --cached --name-only) +yarn run lint:fix && yarn run format:fix && git add $STAGED_FILES \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index eb27bfc3..a925e19b 100755 --- a/next.config.mjs +++ b/next.config.mjs @@ -88,6 +88,11 @@ const nextConfig = { }, webpack(config, options) { if (options.isServer) config.devtool = 'source-map'; + config.module.rules.push({ + test: /\.svg$/, + issuer: /\.[jt]sx?$/, + use: ['@svgr/webpack'], + }); return config; }, async headers() { diff --git a/package.json b/package.json index b1bc4ece..d3d451c2 100755 --- a/package.json +++ b/package.json @@ -31,10 +31,11 @@ "@emotion/styled": "11.11.0", "@next/third-parties": "14.1.3", "@nikolovlazar/chakra-ui-prose": "1.2.1", + "@noble/curves": "^2.0.0", "@prisma/client": "5.18.0", - "@starknet-react/chains": "3.0.0", - "@starknet-react/core": "3.0.1", - "@strkfarm/sdk": "^1.0.51", + "@starknet-react/chains": "^5.0.1", + "@starknet-react/core": "^5.0.1", + "@strkfarm/sdk": "1.1.3", "@tanstack/query-core": "5.28.0", "@types/mixpanel-browser": "2.49.0", "@types/mustache": "4.2.5", @@ -51,8 +52,8 @@ "combined-stream": "^1.0.8", "ethers": "6.11.1", "framer-motion": "11.0.5", - "get-starknet": "3.3.3", - "get-starknet-core": "3.3.3", + "get-starknet": "^3.3.3", + "get-starknet-core": "^3.3.5", "graphql": "16.9.0", "jotai": "2.6.4", "jotai-tanstack-query": "0.8.5", @@ -72,13 +73,15 @@ "react-responsive-carousel": "3.2.23", "react-select": "5.8.0", "react-share": "5.1.0", + "recharts": "^3.1.0", "sharp": "0.33.4", - "starknet": "6.11.0", - "starknetkit": "2.4.0", + "starknet": "8.5.2", + "starknetkit": "^3.0.3", "swr": "2.2.5", "wonka": "6.3.4" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", "@types/lodash.debounce": "^4.0.9", "@types/node": "20", "@types/react": "18", diff --git a/public/banners/troves_starktember.svg b/public/banners/troves_starktember.svg new file mode 100644 index 00000000..0eda3953 --- /dev/null +++ b/public/banners/troves_starktember.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/banners/troves_starktember_mobile.svg b/public/banners/troves_starktember_mobile.svg new file mode 100644 index 00000000..3eb2e607 --- /dev/null +++ b/public/banners/troves_starktember_mobile.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fallback-profile-icon.jpeg b/public/fallback-profile-icon.jpeg new file mode 100644 index 00000000..037e43b3 Binary files /dev/null and b/public/fallback-profile-icon.jpeg differ diff --git a/public/fulllogo.svg b/public/fulllogo.svg index 3507ad8b..e8e7e286 100644 --- a/public/fulllogo.svg +++ b/public/fulllogo.svg @@ -1,4 +1,4 @@ - + diff --git a/src/app/api/lib.ts b/src/app/api/lib.ts index 6b8dd214..e1ef9198 100644 --- a/src/app/api/lib.ts +++ b/src/app/api/lib.ts @@ -1,6 +1,7 @@ import { TrovesStrategyAPIResult } from '@/store/troves.atoms'; +import { UniversalStrategies } from '@strkfarm/sdk'; import { Redis } from '@upstash/redis'; -import { Contract, RpcProvider, uint256 } from 'starknet'; +import { Contract, RpcProvider } from 'starknet'; const kvRedis = new Redis({ url: process.env.VK_REDIS_KV_REST_API_URL, @@ -47,19 +48,53 @@ export const getRewardsInfo = async ( strategies: Pick[], ) => { const funder = - '0x02D6cf6182259ee62A001EfC67e62C1fbc0dF109D2AA4163EB70D6d1074F0173'; - const allowedStrats = [ + '0x0291816c46b4fb9db4d544bac7feb651ab1ae2de3b52e133c0ad23a87dd7c17d'; + + const tokenWiseConfig = [ { - id: 'vesu_fusion_eth', - // ! consider exchange rate of vToken - maxRewardsPerDay: 0.047, // in token units - maxAPY: 200, // in percent - underlyingTokenName: 'ETH', - decimals: 18, - rewardToken: - '0x021fe2ca1b7e731e4a5ef7df2881356070c5d72db4b2d19f9195f6b641f75df0', + token: 'ETH', + maxAPY: 100, // in % + maxRewardsPerDay: 0.538755 / 15, // tokem amount / days + }, + { + token: 'WBTC', + maxAPY: 100, // in % + maxRewardsPerDay: 0.02191182 / 15, // tokem amount / days + }, + { + token: 'USDC', + maxAPY: 100, // in % + maxRewardsPerDay: 2306 / 15, // tokem amount / days + }, + { + token: 'USDT', + maxAPY: 75, // in % + maxRewardsPerDay: 1349 / 15, // tokem amount / days + }, + { + token: 'STRK', + maxAPY: 75, // in % + maxRewardsPerDay: 10500 / 15, // tokem amount / days }, ]; + const allowedStrats = UniversalStrategies.map((u) => { + const tokenWiseInfo = tokenWiseConfig.find( + (t) => t.token === u.depositTokens[0].symbol, + ); + if (!tokenWiseInfo) { + throw new Error(`No token config found for ${u.depositTokens[0].symbol}`); + } + return { + id: `evergreen_${u.depositTokens[0].symbol.toLowerCase()}`, + // ! consider exchange rate of vToken + maxRewardsPerDay: tokenWiseInfo.maxRewardsPerDay || 0, + maxAPY: tokenWiseInfo.maxAPY || 0, + underlyingTokenName: u.depositTokens[0].symbol, + decimals: u.depositTokens[0].decimals, + rewardToken: u.depositTokens[0].address.address, + rewardReceiver: u.additionalInfo.vaultAllocator.address, + }; + }); const provider = new RpcProvider({ nodeUrl: process.env.RPC_URL!, @@ -88,23 +123,25 @@ export const getRewardsInfo = async ( ); const priceData = await priceResponse.json(); // consider token price of vToken - const clsVToken = await provider.getClassAt(stratAllowed.rewardToken); - const tokenContractVToken = new Contract( - clsVToken.abi, - stratAllowed.rewardToken, - provider, - ); - const shareValue = await tokenContractVToken.call('convert_to_assets', [ - uint256.bnToUint256((1e18).toString()), - ]); - console.log(`shareValue::${stratId}::${shareValue}`); - const tokenPrice = - (priceData.price * - Number( - (BigInt(shareValue.toString()) * BigInt(10000)) / - BigInt((1e18).toString()), - )) / - 10000; + // const clsVToken = await provider.getClassAt(stratAllowed.rewardToken); + + // useful math when reward token is a ERC4626 token + // const tokenContractVToken = new Contract( + // clsVToken.abi, + // stratAllowed.rewardToken, + // provider, + // ); + // const shareValue = await tokenContractVToken.call('convert_to_assets', [ + // uint256.bnToUint256((1e18).toString()), + // ]); + // console.log(`shareValue::${stratId}::${shareValue}`); + const tokenPrice = priceData.price; + // (priceData.price * + // Number( + // (BigInt(shareValue.toString()) * BigInt(10000)) / + // BigInt((1e18).toString()), + // )) / + // 10000; console.log( `RewardCalc::${stratId}::tokenPrice::${tokenPrice}, underlyingTokenPrice::${priceData.price}`, ); @@ -130,7 +167,11 @@ export const getRewardsInfo = async ( // if less bal available, use the available balance const rewardToken = stratAllowed.rewardToken; const cls = await provider.getClassAt(rewardToken); - const tokenContract = new Contract(cls.abi, rewardToken, provider); + const tokenContract = new Contract({ + abi: cls.abi, + address: rewardToken, + providerOrAccount: provider, + }); const available = await tokenContract.balanceOf(funder); const availableBal = Number( @@ -154,7 +195,7 @@ export const getRewardsInfo = async ( maxRewardsPerDay: stratAllowed.maxRewardsPerDay, rewardToken: stratAllowed.rewardToken, funder, - receiver: strat.contract[0].address, + receiver: stratAllowed.rewardReceiver, }); } } diff --git a/src/app/api/price/[name]/route.ts b/src/app/api/price/[name]/route.ts index 0daaf8d7..5cc6894d 100644 --- a/src/app/api/price/[name]/route.ts +++ b/src/app/api/price/[name]/route.ts @@ -9,7 +9,7 @@ async function initRedis() { try { console.log('initRedis server'); // eslint-disable-next-line - const config = getMainnetConfig(); + const config = getMainnetConfig(process.env.RPC_URL!, 'latest'); const pricer = new PricerRedis(config, []); if (!process.env.REDIS_URL) { console.warn('REDIS_URL not set'); diff --git a/src/app/api/strategies/apyHistory/[strategyId]/route.ts b/src/app/api/strategies/apyHistory/[strategyId]/route.ts new file mode 100644 index 00000000..a46c1cae --- /dev/null +++ b/src/app/api/strategies/apyHistory/[strategyId]/route.ts @@ -0,0 +1,39 @@ +import { NextRequest } from 'next/server'; +import { getStrategies } from '@/store/strategies.atoms'; + +export async function GET(req: NextRequest, context: any) { + const { params } = context; + const { searchParams } = new URL(req.url); + const strategyId = params.strategyId; + + const duration = parseInt(searchParams.get('duration') || '7', 10); + + const strategies = getStrategies(); + const strategy = strategies.find((s) => s.id === strategyId); + + if (!strategy) { + return new Response(JSON.stringify({ error: 'Strategy not found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const result = await fetch(`https://app.endur.fi/api/blocks/${duration}`); + const response = await result.json(); + + const blockInfo = response.blocks.map( + (block: { block: number; timestamp: number }) => { + return { block: block.block, timestamp: block.timestamp }; + }, + ); + + const apyHistory = { + strategy: strategy.name, + history: await strategy.getAPYHistory(blockInfo), + }; + + return new Response(JSON.stringify({ apyHistory }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +} diff --git a/src/app/api/strategies/route.ts b/src/app/api/strategies/route.ts index c7ea9c53..c9b91e18 100755 --- a/src/app/api/strategies/route.ts +++ b/src/app/api/strategies/route.ts @@ -10,6 +10,7 @@ import { MY_STORE } from '@/store'; import VesuAtoms, { vesu } from '@/store/vesu.store'; import EndurAtoms, { endur } from '@/store/endur.store'; import { setDataToRedis, getDataFromRedis, getRewardsInfo } from '../lib'; +import { DEFAULT_APY_METHODLOGY } from '@/constants'; export const revalidate = 1800; // 30 minutes export const dynamic = 'force-dynamic'; @@ -59,7 +60,8 @@ async function getStrategyInfo( ): Promise { const tvl = await strategy.getTVL(); - const data = { + const defaultAPYMethodology = DEFAULT_APY_METHODLOGY; + const data: TrovesStrategyAPIResult = { name: strategy.name, id: strategy.id, apy: strategy.netYield, @@ -67,6 +69,7 @@ async function getStrategyInfo( baseApy: strategy.netYield, rewardsApy: 0, }, + apyMethodology: strategy.metadata.apyMethodology || defaultAPYMethodology, depositToken: ( await strategy.depositMethods({ amount: MyNumber.fromZero(), diff --git a/src/app/api/tnc/signUser/route.ts b/src/app/api/tnc/signUser/route.ts index c127acaf..f613cedd 100644 --- a/src/app/api/tnc/signUser/route.ts +++ b/src/app/api/tnc/signUser/route.ts @@ -11,6 +11,15 @@ const mixpanel = Mixpanel.init('118f29da6a372f0ccb6f541079cad56b'); export async function POST(req: Request) { const { address, signature } = await req.json(); + if (process.env.NEXT_PUBLIC_IGNORE_SIGNING === 'true') { + console.warn('Signing is ignored in this environment'); + return NextResponse.json({ + success: true, + message: 'Signing is ignored in this environment', + user: null, + }); + } + console.debug('address', address, 'signature', signature); if (!address || !signature) { return NextResponse.json({ @@ -43,7 +52,7 @@ export async function POST(req: Request) { nodeUrl: process.env.NEXT_PUBLIC_RPC_URL!, }); - const myAccount = new Account(provider, address, ''); + const myAccount = new Account({ provider, address, signer: '' }); let isValid = false; @@ -94,7 +103,7 @@ export async function POST(req: Request) { if (!isValid) { try { - const cls = await provider.getClassAt(address, 'pending'); + const cls = await provider.getClassAt(address, 'latest'); // means account is deployed return NextResponse.json({ success: false, diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx index c63b5526..dfcc7f2d 100644 --- a/src/app/community/page.tsx +++ b/src/app/community/page.tsx @@ -81,11 +81,11 @@ const CommunityPage = () => { isOGNFTEligible.isError, ]); - const ogNFTContract = new Contract( - NFTAbi, - process.env.NEXT_PUBLIC_OG_NFT_CONTRACT || '', - provider, - ); + const ogNFTContract = new Contract({ + abi: NFTAbi, + address: process.env.NEXT_PUBLIC_OG_NFT_CONTRACT || '', + providerOrAccount: provider, + }); const { sendAsync: claimOGNFT, @@ -106,7 +106,7 @@ const CommunityPage = () => { const ogNFTAddr: `0x${string}` = (process.env.NEXT_PUBLIC_OG_NFT_CONTRACT || '0x0') as `0x${string}`; const { data: ogNFTBalance, status: balanceQueryStatus } = useReadContract({ - abi: NFTAbi, + abi: NFTAbi as any, address: ogNFTAddr, functionName: 'balanceOf', args: [address || '0x0', 1], diff --git a/src/app/globals.css b/src/app/globals.css index 8a563360..e95f26a1 100755 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -160,6 +160,57 @@ body { ); } +.strategy-page-gradient { + background: linear-gradient( + 0deg, + rgba(33, 33, 33, 0.35), + rgba(33, 33, 33, 0.35) + ), + linear-gradient( + 288.17deg, + rgba(144, 105, 240, 0.2) -123.96%, + rgba(33, 33, 33, 0.2) 101.38% + ); +} + +.apy-gradient { + background: linear-gradient( + 0deg, + rgba(33, 33, 33, 0.35), + rgba(33, 33, 33, 0.35) + ), + linear-gradient( + 326.73deg, + rgba(144, 105, 240, 0.5) -541.23%, + rgba(33, 33, 33, 0.4) 92.85% + ); +} + +.holdings-gradient { + background: linear-gradient( + 0deg, + rgba(33, 33, 33, 0.35), + rgba(33, 33, 33, 0.35) + ), + linear-gradient( + 326.73deg, + rgba(144, 105, 240, 0.5) -541.23%, + rgba(33, 33, 33, 0.4) 92.85% + ); +} + +.faded-purple-gradient { + background: linear-gradient( + 0deg, + rgba(33, 33, 33, 0.35), + rgba(33, 33, 33, 0.35) + ), + linear-gradient( + 326.73deg, + rgba(144, 105, 240, 0.5) -541.23%, + rgba(33, 33, 33, 0.4) 92.85% + ); +} .connect-button-gradient { background: linear-gradient(93.94deg, #9069f0 3.22%, #4a14cd 101.67%); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 5288fd4f..6c84efd4 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,9 @@ 'use client'; import { useDotButton } from '@/components/EmblaCarouselDotButton'; -import Pools from '@/components/Pools'; import Strategies from '@/components/Strategies'; import TVL from '@/components/TVL'; +import { useIsMobile } from '@/hooks/use-mobile'; import { useWindowSize } from '@/utils/useWindowSize'; import { @@ -14,8 +14,10 @@ import { TabPanel, TabPanels, Tabs, + Image as ChakraImage, TabIndicator, Text, + Link, } from '@chakra-ui/react'; import { useAccount } from '@starknet-react/core'; import Autoplay from 'embla-carousel-autoplay'; @@ -30,15 +32,15 @@ const banner_images = [ // mobile: '/banners/strkfarm_braavos_mobile.svg', // link: 'https://starknet.quest/quest/235', // }, + // { + // desktop: '/banners/endur.svg', + // mobile: '/banners/endur_mobile.svg', + // link: 'https://endur.fi/r/troves', + // }, { - desktop: '/banners/endur.svg', - mobile: '/banners/endur_mobile.svg', - link: 'https://endur.fi/r/troves', - }, - { - desktop: '/banners/seed_grant.svg', - mobile: '/banners/seed_grant_small.jpg', - link: 'https://x.com/troves/status/1787783906982260881', + desktop: '/banners/troves_starktember.svg', + mobile: '/banners/troves_starktember_mobile.svg', + link: 'https://x.com/trovesfi', }, ]; @@ -65,11 +67,11 @@ export default function Home() { } function handleTabsChange(index: number) { - if (index === 1) { - setRoute('pools'); - } else { - setRoute('strategies'); - } + // if (index === 1) { + // setRoute('pools'); + // } else { + setRoute('strategies'); + // } } useEffect(() => { @@ -80,13 +82,15 @@ export default function Home() { (async () => { const tab = searchParams.get('tab'); if (tab === 'pools') { - setTabIndex(1); + setTabIndex(0); // actually 1, but put 0 since yields are commented } else { setTabIndex(0); } })(); }, [searchParams]); + const isMobile = useIsMobile(); + return ( + + + {banner_images.map((banner, index) => ( + + + 450) || size.width == 0 + ? banner.desktop + : banner.mobile + } + height={'auto'} + boxShadow={'none'} + width="100%" + alt="Banner" + style={{ objectFit: 'cover', borderRadius: '10px' }} + /> + + + ))} + + + - Strategies✨ + Yield Strategies✨ - { @@ -151,7 +185,7 @@ export default function Home() { }} > Find yields - + */} - - + */} {/*
*/} diff --git a/src/app/recovery/_components/zkLendRecoveryComp.tsx b/src/app/recovery/_components/zkLendRecoveryComp.tsx index c2ce52b1..aabbd64a 100644 --- a/src/app/recovery/_components/zkLendRecoveryComp.tsx +++ b/src/app/recovery/_components/zkLendRecoveryComp.tsx @@ -74,16 +74,16 @@ const AUTO_COMPOUNDING = { }, }; -const BATCH_ID = 1; - -export default function ZklendRecoveryComp() { +export default function ZklendRecoveryComp(props: { BATCH_ID: number }) { const _address = useAtomValue(addressAtom); const address = useMemo(() => { return _address || ''; }, [_address]); const ALL_STRATS: Record = { - ...AUTO_COMPOUNDING, + ...(props.BATCH_ID == 1 + ? AUTO_COMPOUNDING + : ({} as Record)), ...STRATEGY_ADDRESSES, }; @@ -105,14 +105,14 @@ export default function ZklendRecoveryComp() { const contractCalls = Object.entries(ALL_STRATS).map( async ([key, strategyInfo]) => { - const contract = new Contract( - strategyAbi, - strategyInfo.address, - provider, - ); + const contract = new Contract({ + abi: strategyAbi, + address: strategyInfo.address, + providerOrAccount: provider, + }); const res: any = await contract.call('zklend_position', [ address, - uint256.bnToUint256(BATCH_ID), + uint256.bnToUint256(props.BATCH_ID), ]); const token = num.getHexString(res[0].toString()); const tokenInfo = TOKENS.find( @@ -143,7 +143,7 @@ export default function ZklendRecoveryComp() { }), { ...balances }, ); - console.log('revoery2', updatedBalances); + console.log('revoery2', updatedBalances, props); setBalances(updatedBalances); } catch (error) { setIsLoading(false); @@ -161,7 +161,7 @@ export default function ZklendRecoveryComp() { (acc, [key, value]) => { const tokenName: 'ETH' | 'USDC' | 'STRK' = value.token as any; if (!['ETH', 'USDC', 'STRK'].includes(tokenName)) { - console.error('Invalid token name:', tokenName); + console.error('Invalid token name:', tokenName, balances); throw new Error('Invalid token name'); } acc[tokenName] += Number(value.balance); @@ -174,11 +174,11 @@ export default function ZklendRecoveryComp() { const calls = useMemo(() => { const contracts = Object.entries(ALL_STRATS).map(([key, strategyInfo]) => { - const contract = new Contract( - strategyAbi, - strategyInfo.address, - provider, - ); + const contract = new Contract({ + abi: strategyAbi, + address: strategyInfo.address, + providerOrAccount: provider, + }); return contract; }); const calls = contracts @@ -193,7 +193,7 @@ export default function ZklendRecoveryComp() { const amount = balances[strategy_key].balance; if (amount && Number(amount) > 0) return contract.populate('withdraw_zklend', [ - uint256.bnToUint256(BATCH_ID), + uint256.bnToUint256(props.BATCH_ID), address, ]); return null; @@ -241,7 +241,7 @@ export default function ZklendRecoveryComp() { marginBottom={{ base: '20px', md: '0' }} > - Recovery from zkLend: + Recovery from zkLend: Batch {props.BATCH_ID} - 1. Check your eligible claims by connecting your wallet. Please note - that approximately 1-5% of your original funds are expected to be - available. Please let us know of any descrepresies before 28th March - on our{' '} - - Telegram - - . -
- 2. Any newly recovered funds from{' '} + 1. Any newly recovered funds from{' '} { - const contract = new Contract( - strategyAbi, - strategyInfo.address, - provider, - ); + const contract = new Contract({ + abi: strategyAbi, + address: strategyInfo.address, + providerOrAccount: provider, + }); const res = await contract.call('nostra_position', [address]); console.log(`revoery`, strategyInfo.address, address, res); return { @@ -107,11 +107,11 @@ export default function Recovery() { const calls = useMemo(() => { const contracts = Object.entries(STRATEGY_ADDRESSES).map( ([key, strategyInfo]) => { - const contract = new Contract( - strategyAbi, - strategyInfo.address, - provider, - ); + const contract = new Contract({ + abi: strategyAbi, + address: strategyInfo.address, + providerOrAccount: provider, + }); return contract; }, ); @@ -159,11 +159,11 @@ export default function Recovery() { const contracts = Object.entries(STRATEGY_ADDRESSES).map( ([key, strategyInfo]) => { - const contract = new Contract( - strategyAbi, - strategyInfo.address, - provider, - ); + const contract = new Contract({ + abi: strategyAbi, + address: strategyInfo.address, + providerOrAccount: provider, + }); return contract; }, ); @@ -403,7 +403,9 @@ export default function Recovery() {
- + +
+ ); } diff --git a/src/app/strategy/[strategyId]/_components/APYHistory.tsx b/src/app/strategy/[strategyId]/_components/APYHistory.tsx new file mode 100644 index 00000000..a8013b72 --- /dev/null +++ b/src/app/strategy/[strategyId]/_components/APYHistory.tsx @@ -0,0 +1,200 @@ +import { Box, Flex, Text } from '@chakra-ui/react'; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + ResponsiveContainer, +} from 'recharts'; +import TimeRangeSelector from '@/components/TimeRangeSelector'; +import { useState, useMemo } from 'react'; + +type TimeRange = '1d' | '7d' | '30d' | 'all'; + +interface APYHistoryData { + month: string; + apy: number; +} + +const dummyAPYHistory: APYHistoryData[] = (() => { + const days = 60; + const today = new Date(); + const data: APYHistoryData[] = []; + const baseAPY = 4.0; + for (let i = days - 1; i >= 0; i--) { + const date = new Date(today); + date.setDate(today.getDate() - i); + const dayStr = date.toISOString().slice(0, 10); + + const apy = + baseAPY + + Math.sin(i / 7) * 0.2 + + Math.random() * 0.1 + + (i > 30 ? 0.5 : 0); + data.push({ + month: dayStr, + apy: Number(apy.toFixed(2)), + }); + } + return data; +})(); + +function getMinMax(arr: T[], key: keyof T & string): [number, number] { + const values = arr.map((item) => item[key] as unknown as number); + const min = Math.min(...values); + const max = Math.max(...values); + if (min === max) { + return [min - 1, max + 1]; + } + return [min, max]; +} + +function generateTicks([min, max]: [number, number]): number[] { + const step = Math.max(0.1, (max - min) / 4); + return [min, min + step, min + 2 * step, min + 3 * step, max]; +} + +function formatYAxis(value: number): string { + return `${value.toFixed(1)}%`; +} + +const timeRangeToDays: Record = { + '1d': 10, + '7d': 7, + '30d': 30, + all: dummyAPYHistory.length, +}; + +const renderAPYHistoryChart = ( + selectedRange: TimeRange, + onRangeChange: (range: TimeRange) => void, + filteredData: APYHistoryData[], +) => { + const yAxisDomain: [number, number] = getMinMax(filteredData, 'apy'); + + return ( + + + + APY History + + + + + + + + + + + + + + + { + const date = new Date(value); + return date.toLocaleString('default', { + month: 'short', + day: 'numeric', + }); + }} + type="category" + /> + + + + + + + + ); +}; + +function APYHistory() { + const [selectedRange, setSelectedRange] = useState('all'); + const days = timeRangeToDays[selectedRange]; + const filteredData = useMemo( + () => dummyAPYHistory.slice(-days), + [selectedRange], + ); + return ( + <>{renderAPYHistoryChart(selectedRange, setSelectedRange, filteredData)} + ); +} + +export function APYHistoryTab() { + return ( + + + + + + ); +} diff --git a/src/app/strategy/[strategyId]/_components/DetailsTab.tsx b/src/app/strategy/[strategyId]/_components/DetailsTab.tsx index 585ed04c..0101c665 100644 --- a/src/app/strategy/[strategyId]/_components/DetailsTab.tsx +++ b/src/app/strategy/[strategyId]/_components/DetailsTab.tsx @@ -1,9 +1,9 @@ -import { Box, Center, Spinner, Flex, Text, Image } from '@chakra-ui/react'; +import { Box, Center, Spinner, Flex, Text } from '@chakra-ui/react'; import { TrovesStrategyAPIResult } from '@/store/troves.atoms'; import FlowChart from './FlowChart'; -import depositAction from '@/assets/depositAction.svg'; -import withdrawAction from '@/assets/withdrawAction.svg'; -import loopAction from '@/assets/loopAction.svg'; +import DepositActionIcon from '@/assets/depositAction.svg'; +import WithdrawActionIcon from '@/assets/withdrawAction.svg'; +import LoopActionIcon from '@/assets/loopAction.svg'; import { useMemo } from 'react'; import { StrategyInfo } from '@/store/strategies.atoms'; @@ -29,72 +29,89 @@ export function DetailsTab(props: DetailsTabProps) { return []; }, [strategyAPIResult.actions, strategy.metadata.investmentSteps]); + function isDeposit(action: string) { + return ( + action.toLowerCase().includes('stake') || + action.toLowerCase().includes('supply') || + action.toLowerCase().includes('deposit') || + action.toLowerCase().includes('invest') || + action.toLowerCase().includes('buy') + ); + } + return ( - - - {steps.length > 0 && ( - - <> - - Steps performed by the strategy - - {steps.map((action, index) => ( - - {action.toLowerCase().includes('stake') || - action.toLowerCase().includes('supply') ? ( - - ) : action.toLowerCase().includes('borrow') ? ( - - ) : ( - - )} - - {action} - - - ))} - {steps.length == 0 && ( -
- -
- )} - -
- )} + + + + {steps.length > 0 && ( + + <> + + Steps performed by the strategy + + {steps.map((action, index) => ( + + {action.toLowerCase().includes('stake') || + action.toLowerCase().includes('supply') ? ( + + ) : action.toLowerCase().includes('borrow') ? ( + + ) : ( + + )} + + {action} + + + ))} + {steps.length == 0 && ( +
+ +
+ )} + +
+ )} - {strategyAPIResult.investmentFlows.length > 0 && ( - - 0 && ( + - Configuration - - - - )} + + Configuration + + +
+ )} +
- +
); } diff --git a/src/app/strategy/[strategyId]/_components/FAQTab.tsx b/src/app/strategy/[strategyId]/_components/FAQTab.tsx index f252ef89..baab888f 100644 --- a/src/app/strategy/[strategyId]/_components/FAQTab.tsx +++ b/src/app/strategy/[strategyId]/_components/FAQTab.tsx @@ -22,118 +22,130 @@ export function FAQTab(props: FAQTabProps) { const { strategy } = props; return ( - - + - Get your questions answered - - - - {!strategy.metadata.faqs || - (strategy.metadata.faqs.length == 0 && ( - - No FAQs at the moment - - ))} - - {strategy.metadata.faqs && - strategy.metadata.faqs.length > 0 && - strategy.metadata.faqs.map((faq, index) => ( - - - - - {faq.question} - - - - - - {faq.answer} - - + + Get your questions answered + + + + {!strategy.metadata.faqs || + (strategy.metadata.faqs.length == 0 && ( + + No FAQs at the moment + ))} - - + + {strategy.metadata.faqs && + strategy.metadata.faqs.length > 0 && + strategy.metadata.faqs.map((faq, index) => ( + + + + + {faq.question} + + + + + + {faq.answer} + + + ))} + + - - - - For more queries reach out to us on Telegram - - - Our team will respond to you soon! - - + + + For more queries reach out to us on Telegram + + + Our team will respond to you soon! + + - - - + + + + + - + ); } diff --git a/src/app/strategy/[strategyId]/_components/ManageTab.tsx b/src/app/strategy/[strategyId]/_components/ManageTab.tsx index ff40fb83..f244ab9c 100644 --- a/src/app/strategy/[strategyId]/_components/ManageTab.tsx +++ b/src/app/strategy/[strategyId]/_components/ManageTab.tsx @@ -14,23 +14,26 @@ export function ManageTab(props: ManageTabProps) { const { strategy, isMobile } = props; return ( - + - - How does it work? - - {/* + + How does it work? + + {/* Withdraw anytime by redeeming your NFT for USDC. */} - - {strategy.description} - + + {strategy.description} + - - {/* + + {/* Risks @@ -74,23 +77,24 @@ export function ManageTab(props: ManageTabProps) { ))} */} - + - - {!strategy || - (strategy.isSingleTokenDepositView && ( - - ))} - {strategy && !strategy.isSingleTokenDepositView && ( - - )} + + {!strategy || + (strategy.isSingleTokenDepositView && ( + + ))} + {strategy && !strategy.isSingleTokenDepositView && ( + + )} + - + ); } diff --git a/src/app/strategy/[strategyId]/_components/RiskTab.tsx b/src/app/strategy/[strategyId]/_components/RiskTab.tsx index 59e391c2..109474db 100644 --- a/src/app/strategy/[strategyId]/_components/RiskTab.tsx +++ b/src/app/strategy/[strategyId]/_components/RiskTab.tsx @@ -58,116 +58,124 @@ export function RiskTab(props: RiskTabProps) { }, [strategy.metadata.risk.riskFactor, strategy.metadata.risk.notARisks]); return ( - - {risks.length > 0 && ( + + + {risks.length > 0 && ( + + + Risk Assessment + + + {risks.map((risk, index) => ( + + + {risk.type}: {getRiskString(risk.value)} + + + ))} + + + )} - Risk Assessment + Risk details - - {risks.map((risk, index) => ( - - - {risk.type}: {getRiskString(risk.value)} - - - ))} - - - )} - - - Risk details - - - - {strategy.risks.map((r, index) => ( - - {r} - {index === 0 && ( - - {getRiskString(strategy.riskFactor)} - {' risk'} - - )} - - ))} - - - - + + {strategy.risks.map((r, index) => ( + + {r} + {index === 0 && ( + + {getRiskString(strategy.riskFactor)} + {' risk'} + + )} + + ))} + + + + + ); } diff --git a/src/app/strategy/[strategyId]/_components/Strategy.tsx b/src/app/strategy/[strategyId]/_components/Strategy.tsx index bd13ffc8..a0fc528f 100755 --- a/src/app/strategy/[strategyId]/_components/Strategy.tsx +++ b/src/app/strategy/[strategyId]/_components/Strategy.tsx @@ -50,6 +50,14 @@ import { ManageTab } from './ManageTab'; import { RiskTab } from './RiskTab'; import { StrategyInfoComponent } from './StrategyInfo'; import { TransactionsTab } from './TransactionsTab'; +import { APYHistoryTab } from './APYHistory'; + +import ManageIcon from '@/assets/manage.svg'; +import APYHistoryIcon from '@/assets/apy-history.svg'; +import RiskIcon from '@/assets/risk.svg'; +import DetailsIcon from '@/assets/details.svg'; +import FaqIcon from '@/assets/faq.svg'; +import TransactionsIcon from '@/assets/transactions.svg'; function HoldingsText({ strategy, @@ -62,6 +70,9 @@ function HoldingsText({ }) { if (strategy.settings.isInMaintenance) return Maintenance Mode; + + if (strategy?.isRetired()) return '-'; + if (!address) return You will see your holdings here; if (balData.isLoading || !balData.data?.tokenInfo) { @@ -80,7 +91,7 @@ function HoldingsText({ balData.data.tokenInfo?.displayDecimals || 2, ), ); - if (value === 0 || strategy?.isRetired()) return '-'; + if (value === 0) return '-'; return `${balData.data.amount.toEtherToFixedDecimals( balData.data.tokenInfo?.displayDecimals || 2, )} ${balData.data.tokenInfo?.name}`; @@ -122,7 +133,12 @@ function HoldingsAndEarnings({ }) { return ( - + Your Holdings @@ -139,7 +155,12 @@ function HoldingsAndEarnings({ label={!strategy?.isRetired() && 'Life time earnings'} {...MYSTYLES.TOOLTIP.STANDARD} > - + { setRoute('manage'); break; case 1: - setRoute('details'); + setRoute('apys'); break; case 2: setRoute('risks'); break; case 3: - setRoute('faq'); + setRoute('details'); break; case 4: + setRoute('faq'); + break; + case 5: setRoute('transactions'); break; default: @@ -219,18 +243,21 @@ const Strategy = ({ params }: StrategyParams) => { case 'manage': setTabIndex(0); break; - case 'details': + case 'apys': setTabIndex(1); break; case 'risks': setTabIndex(2); break; - case 'faq': + case 'details': setTabIndex(3); break; - case 'transactions': + case 'faq': setTabIndex(4); break; + case 'transactions': + setTabIndex(5); + break; default: setTabIndex(0); break; @@ -303,13 +330,13 @@ const Strategy = ({ params }: StrategyParams) => { sign * Number( new MyNumber(tx.amount, tokenInfo.decimals).toEtherToFixedDecimals( - 4, + 6, ), ) ); }, 0); const currentValue = Number( - balData.data?.amount.toEtherToFixedDecimals(4) || '0', + balData.data?.amount.toEtherToFixedDecimals(6) || '0', ); if (currentValue === 0) return 0; @@ -361,251 +388,313 @@ const Strategy = ({ params }: StrategyParams) => { display={{ base: 'block' }} justifyContent={'center'} width={'100%'} - margin={'0 auto'} - padding={'10px'} - maxWidth={'1152px'} + padding={0} > - - - - - - - + - {strategy && ( - - - {!strategy?.isRetired() && strategyCached && ( - - - - )} - - )} - - {strategy && ( - - - - - )} - - - {!isMobile && ( - - - { - mixpanel.track('Manage clicked'); + + + + + + {strategy && ( + + + {!strategy?.isRetired() && strategyCached && ( + + + + )} + + )} + + {strategy && ( + + + + + )} + + + {!isMobile && ( + + - FAQs - - { - mixpanel.track('Transactions clicked'); + { + mixpanel.track('Manage clicked'); + }} + > + + Manage + + { + mixpanel.track('APY History clicked'); + }} + > + + APY History + + { + mixpanel.track('Risk clicked'); + }} + > + + Risks + + { + mixpanel.track('Details clicked'); + }} + > + + Details + + { + mixpanel.track('FAQs clicked'); + }} + > + + FAQs + + { + mixpanel.track('Transactions clicked'); + }} + > + + Transactions + + + + + + {strategy && } + + + {strategyCached && strategy && } + + + {strategy && } + + + {strategyCached && strategy && ( + + )} + + + {strategy && } + + + + {strategy && ( + + )} + + + + )} + + {/* MOBILE VIEW */} + {isMobile && ( + + { + if (Array.isArray(expandedIndex)) { + setAccordionIndex(expandedIndex[0] ?? 0); + } else { + setAccordionIndex(expandedIndex); + } }} + allowToggle + width="100%" + display="flex" + flexDirection="column" + gap="10px" + borderRadius={'lg'} > - Transactions - - - - - - {strategy && } - - - {strategyCached && strategy && ( - - )} - - - {strategy && } - - - - {strategy && } - - - - {strategy && ( - - )} - - - - )} - - {/* MOBILE VIEW */} - {isMobile && ( - - { - if (Array.isArray(expandedIndex)) { - setAccordionIndex(expandedIndex[0] ?? 0); - } else { - setAccordionIndex(expandedIndex); - } - }} - allowToggle - width="100%" - display="flex" - flexDirection="column" - gap="10px" - borderRadius={'lg'} - > - {strategy && - [ - { - label: 'Manage', - content: , - }, - { - label: 'Details', - content: strategyCached && ( - - ), - }, - { - label: 'Risks', - content: , - }, - { - label: 'FAQs', - content: , - }, - { - label: 'Transactions', - content: ( - - ), - }, - ].map((item, index) => ( - - , + }, + { + label: 'Details', + content: strategyCached && ( + + ), + }, + { + label: 'Risks', + content: , + }, + { + label: 'FAQs', + content: , + }, + { + label: 'Transactions', + content: ( + + ), + }, + ].map((item, index) => ( + - - {item.label} - - - - - {item.content} - - - ))} - - - )} + + {item.label} + + + + + {item.content} + + + ))} + + + )} + ); diff --git a/src/app/strategy/[strategyId]/_components/StrategyInfo.tsx b/src/app/strategy/[strategyId]/_components/StrategyInfo.tsx index bdf5f3de..2fde3d13 100644 --- a/src/app/strategy/[strategyId]/_components/StrategyInfo.tsx +++ b/src/app/strategy/[strategyId]/_components/StrategyInfo.tsx @@ -3,12 +3,11 @@ import { AvatarGroup, Box, Flex, - Image, Link, Text, Tooltip, } from '@chakra-ui/react'; -import shield from '@/assets/shield.svg'; +import ShieldIcon from '@/assets/shield.svg'; import { StrategyInfo } from '@/store/strategies.atoms'; export function StrategyInfoComponent(props: { strategy: StrategyInfo }) { @@ -36,35 +35,33 @@ export function StrategyInfoComponent(props: { strategy: StrategyInfo }) { /> )} - - {strategy ? strategy.name : 'Strategy Not found'} - - {strategy.metadata.auditUrl && ( - Audited. Click to view report.}> - - - badge - - - - )} + + + {strategy ? strategy.name : 'Strategy Not found'} + + {strategy.metadata.auditUrl && ( + Audited. Click to view report.}> + + + + + + + )} + ); } diff --git a/src/app/strategy/[strategyId]/_components/TokenDeposit.tsx b/src/app/strategy/[strategyId]/_components/TokenDeposit.tsx index 4f7af8e6..40bba48d 100644 --- a/src/app/strategy/[strategyId]/_components/TokenDeposit.tsx +++ b/src/app/strategy/[strategyId]/_components/TokenDeposit.tsx @@ -22,6 +22,7 @@ export function TokenDeposit(props: TokenDepositProps) { const { strategy } = props; return ( { // mixpanel.track('All pools clicked') @@ -46,11 +47,11 @@ export function TokenDeposit(props: TokenDepositProps) { { // mixpanel.track('Strategies opened') @@ -64,6 +65,7 @@ export function TokenDeposit(props: TokenDepositProps) { width={'100%'} padding={'20px 16px'} borderBottomLeftRadius={'8px'} + borderBottomRightRadius={'8px'} > {tabIndex == 0 && ( <> @@ -99,6 +101,7 @@ export function TokenDeposit(props: TokenDepositProps) { {tabIndex == 1 && ( diff --git a/src/app/strategy/[strategyId]/_components/TransactionsTab.tsx b/src/app/strategy/[strategyId]/_components/TransactionsTab.tsx index e73ff21f..595e26d8 100644 --- a/src/app/strategy/[strategyId]/_components/TransactionsTab.tsx +++ b/src/app/strategy/[strategyId]/_components/TransactionsTab.tsx @@ -41,6 +41,55 @@ interface TransactionsTabProps { isMobile?: boolean; } +function getTransactionIcon(type: string) { + if (type == 'deposit') { + return ( + + + + + {capitalize(type)} + + ); + } + + const bgColor = type == 'withdraw' || type == 'claim' ? 'red_2' : 'yellow_2'; + let text = 'Withdrawn'; + if (type == 'redeem') { + text = 'Withdraw in progress'; + } else if (type == 'claim' || type == 'withdraw') { + text = 'Withdrawn'; + } else { + throw new Error(`Unknown transaction type: ${type}`); + } + return ( + + + + + {text} + + ); +} + function DesktopTransactionHistory(props: { transactions: ITransaction[] }) { const { transactions } = props; return ( @@ -52,12 +101,12 @@ function DesktopTransactionHistory(props: { transactions: ITransaction[] }) { sx={{ overflow: 'hidden', 'border-collapse': 'separate', - 'border-spacing': '0px 3px', + 'border-spacing': '0px 5px', }} > # @@ -75,6 +127,8 @@ function DesktopTransactionHistory(props: { transactions: ITransaction[] }) { fontSize={'14px'} fontWeight={'600'} textTransform={'capitalize'} + borderRightWidth={'1px'} + borderColor={'mybg'} > Amount @@ -83,6 +137,8 @@ function DesktopTransactionHistory(props: { transactions: ITransaction[] }) { fontSize={'14px'} fontWeight={'600'} textTransform={'capitalize'} + borderRightWidth={'1px'} + borderColor={'mybg'} > Transaction type @@ -91,6 +147,8 @@ function DesktopTransactionHistory(props: { transactions: ITransaction[] }) { fontSize={'14px'} fontWeight={'600'} textTransform={'capitalize'} + borderRightWidth={'1px'} + borderColor={'mybg'} > Transaction hash @@ -100,6 +158,7 @@ function DesktopTransactionHistory(props: { transactions: ITransaction[] }) { fontWeight={'600'} textTransform={'capitalize'} borderTopRightRadius={'lg'} + borderBottomRightRadius={'lg'} > Time @@ -111,51 +170,28 @@ function DesktopTransactionHistory(props: { transactions: ITransaction[] }) { const decimals = token?.decimals; return ( - - + + {index + 1}. - {Number( - new MyNumber( - tx.amount, - decimals!, - ).toEtherToFixedDecimals(token.displayDecimals), - ).toLocaleString()}{' '} + {new MyNumber( + tx.amount, + decimals!, + ).toEtherToFixedDecimals(token.displayDecimals)}{' '} {token?.name} - - {tx.type === 'deposit' ? ( - - - - ) : ( - - - - )} - - {capitalize(tx.type)} - + {getTransactionIcon(tx.type)} - + {timeAgo(new Date(tx.timestamp * 1000))} @@ -196,6 +237,27 @@ function MobileTransactionHistory(props: { transactions: ITransaction[] }) { const token = getTokenInfoFromAddr(tx.asset); const decimals = token?.decimals; const isDeposit = tx.type === 'deposit'; + let displayText = 'Deposit'; + let iconColor = 'light_green'; + switch (tx.type) { + case 'deposit': + displayText = 'Deposited'; + break; + case 'withdraw': + displayText = 'Withdrawn'; + iconColor = 'red_2'; + break; + case 'redeem': + iconColor = 'yellow_2'; + displayText = 'Withdraw in progress'; + break; + case 'claim': + iconColor = 'red_2'; + displayText = 'Withdrawn'; + break; + default: + throw new Error(`Unknown transaction type: ${tx.type}`); + } return ( )} - - {isDeposit ? 'Deposited' : 'Withdrawn'} + + {displayText} @@ -266,59 +324,68 @@ export function TransactionsTab(props: TransactionsTabProps) { const { strategy, txHistory, isMobile } = props; return ( - - - - Transaction history - - {!strategy.settings.isTransactionHistDisabled && ( - - There may be delays in fetching data. If your transaction isn't - found, try again later. + + + + + Transaction history - )} - - {address ? ( - strategy.settings.isTransactionHistDisabled ? ( + {!strategy.settings.isTransactionHistDisabled && ( + + There may be delays in fetching data. If your transaction + isn't found, try again later. + + )} + + {address ? ( + strategy.settings.isTransactionHistDisabled ? ( + + Transaction history is not available for this strategy yet. If + enabled in future, will include the entire history. + + ) : txHistory.findManyInvestment_flows.length !== 0 ? ( + isMobile ? ( + + ) : ( + + ) + ) : ( + + No transactions found + + ) + ) : ( - Transaction history is not available for this strategy yet. If - enabled in future, will include the entire history. + Connect your wallet to view transaction history - ) : txHistory.findManyInvestment_flows.length !== 0 ? ( - isMobile ? ( - - ) : ( - - ) - ) : ( - - No transactions found - - ) - ) : ( - - Connect your wallet to view transaction history - - )} - + )} + + ); } diff --git a/src/app/strategy/[strategyId]/loading.tsx b/src/app/strategy/[strategyId]/loading.tsx new file mode 100644 index 00000000..806514d7 --- /dev/null +++ b/src/app/strategy/[strategyId]/loading.tsx @@ -0,0 +1,21 @@ +import { Spinner, Flex } from '@chakra-ui/react'; + +export default function Loading() { + return ( + + + + ); +} diff --git a/src/app/template.tsx b/src/app/template.tsx index d604e6e9..a1e3ce4c 100755 --- a/src/app/template.tsx +++ b/src/app/template.tsx @@ -7,17 +7,17 @@ import { ChakraBaseProvider, Container, Flex, + Spinner, extendTheme, } from '@chakra-ui/react'; import { mainnet } from '@starknet-react/chains'; -import { StarknetConfig, jsonRpcProvider } from '@starknet-react/core'; +import { StarknetConfig, jsonRpcProvider, voyager } from '@starknet-react/core'; import { Provider as JotaiProvider } from 'jotai'; import mixpanel from 'mixpanel-browser'; import Image from 'next/image'; import { usePathname } from 'next/navigation'; import * as React from 'react'; import { Toaster } from 'react-hot-toast'; -import { RpcProviderOptions, constants } from 'starknet'; import Footer from '@/components/Footer'; import { useIsMobile } from '@/hooks/use-mobile'; @@ -35,6 +35,7 @@ const theme = extendTheme({ highlight: '#303136', purple: '#9069F0', + purple_30p: '#CFACFA4D', purple_60p: '#6F5CA599', purple_hover: '#4C2CD7', purple_hover_2: '#C5A6FF', @@ -44,22 +45,33 @@ const theme = extendTheme({ header: '#1d1531', + table_header_bg: '#9069F033', + badge_blue: '#002F6A', badge_green: '#016131', mybg: 'black', // dark blue bg_2: '#111113', bg_3: '#090910', + bg_4: '#010101', mycard: '#19191b', mycard_light: '#212121', mycard_light_2x: '#303136', mycard_dark: '#121212', + input_light: '#37373780', + list_item_bg: '#37373766', + + text_grey: '#868898', + text_grey_60p: '#86889899', + text_grey_90p: '#868898E5', grey_text: '#B6B6B6', + grey_text_2: '#909090', text_primary: 'white', text_secondary: '#b2b3bd', text_secondary_2: '#D3D3D3', + text_black_70p: '#010101B2', yellow: '#EFDB72', yellow_2: '#FFA500', @@ -73,6 +85,7 @@ const theme = extendTheme({ light_green_30p: '#3EE5C24D', border_light: '#CFCFEA', + border_light_2: '#2D2D3D', border_light_3p: '#CFCFEA0D', border_light_30p: '#CFCFEA4D', @@ -82,6 +95,7 @@ const theme = extendTheme({ dark_bg: '#111119', purple_tint: '#CFCFEA', lavender_gray: '#B4B1BD', + border_grey: '#B3B3B326', text_subtle: '#a0a2b0', text_subtle_50p: '#a0a2b080', @@ -124,17 +138,12 @@ export const CONNECTOR_NAMES = ['Braavos', 'Argent X', 'Argent (mobile)']; // 'A export default function Template({ children }: { children: React.ReactNode }) { const chains = [mainnet]; + const pathname = usePathname(); const provider = jsonRpcProvider({ rpc: (chain) => { - const args: RpcProviderOptions = { - nodeUrl: - 'https://rpc.nethermind.io/mainnet-juno?apikey=t1HPjhplOyEQpxqVMhpwLGuwmOlbXN0XivWUiPAxIBs0kHVK', - chainId: constants.StarknetChainId.SN_MAIN, - }; - return args; + return { nodeUrl: process.env.NEXT_PUBLIC_RPC_URL! }; }, }); - const pathname = usePathname(); function getIconNode(icon: typeof import('*.svg'), alt: string) { return ( @@ -146,16 +155,38 @@ export default function Template({ children }: { children: React.ReactNode }) { const isMobile = useIsMobile(); + function Loading() { + return ( + + + + ); + } + return ( - + }> + + + + + diff --git a/src/assets/details.svg b/src/assets/details.svg new file mode 100644 index 00000000..a0c5566a --- /dev/null +++ b/src/assets/details.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/faq.svg b/src/assets/faq.svg new file mode 100644 index 00000000..200b5804 --- /dev/null +++ b/src/assets/faq.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/manage.svg b/src/assets/manage.svg new file mode 100644 index 00000000..bf2c850f --- /dev/null +++ b/src/assets/manage.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/risk.svg b/src/assets/risk.svg new file mode 100644 index 00000000..5a39f5ef --- /dev/null +++ b/src/assets/risk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/tg.svg b/src/assets/tg.svg index 01c6b5ff..4cb99463 100644 --- a/src/assets/tg.svg +++ b/src/assets/tg.svg @@ -1,4 +1,4 @@ - + diff --git a/src/assets/transactions.svg b/src/assets/transactions.svg new file mode 100644 index 00000000..d1225753 --- /dev/null +++ b/src/assets/transactions.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/APYInfo.tsx b/src/components/APYInfo.tsx index 67fcaf99..3b1ab9c7 100644 --- a/src/components/APYInfo.tsx +++ b/src/components/APYInfo.tsx @@ -1,11 +1,10 @@ +import CONSTANTS, { DEFAULT_APY_METHODLOGY } from '@/constants'; import { StrategyInfo } from '@/store/strategies.atoms'; import { TrovesStrategyAPIResult } from '@/store/troves.atoms'; import { MYSTYLES } from '@/style'; import { Flex, Tooltip, - Box, - Text, Stat, StatLabel, StatNumber, @@ -13,6 +12,8 @@ import { Spinner, } from '@chakra-ui/react'; import { useMemo } from 'react'; +import { APYToolTip } from './YieldCard'; +import { APRSplit } from '@/store/pools'; export function APYInfo(props: { strategy: StrategyInfo; @@ -20,65 +21,44 @@ export function APYInfo(props: { }) { const { strategy, strategyAPIResult } = props; - const defaultAPYTooltip = - 'Current APY including any fees. Net returns subject to change based on market conditions.'; - const leverage = useMemo(() => { if (!strategyAPIResult) return 0; return strategyAPIResult.leverage || 0; }, [strategyAPIResult]); + const apySplits: APRSplit[] = [ + { + apr: strategyAPIResult.apySplit.baseApy, + title: 'Strategy APY', + description: 'Includes fees & Defi spring rewards', + }, + ]; + + if (strategyAPIResult.apySplit.rewardsApy > 0) { + apySplits.push({ + apr: strategyAPIResult.apySplit.rewardsApy, + title: 'Boosted APY', + description: CONSTANTS.BOOSTED_YIELD_TOOLTIP_TEXT, + }); + } + return ( - {strategy.metadata.apyMethodology || defaultAPYTooltip} - {strategyAPIResult && ( - - - Strategy APY: - - Including fees and Defi spring rewards - - - - {(strategyAPIResult.apySplit.baseApy * 100).toFixed(2)}% - - - )} - {strategyAPIResult && strategyAPIResult.apySplit.rewardsApy > 0 && ( - - - Rewards APY: - - Incentives by Troves - - - - {(strategyAPIResult.apySplit.rewardsApy * 100).toFixed(2)}% - - - )} - - } + label={APYToolTip({ + apyMethodology: + strategy.metadata.apyMethodology || DEFAULT_APY_METHODLOGY, + apySplits, + })} {...MYSTYLES.TOOLTIP.STANDARD} > 1 && ( - {balance.toEtherToFixedDecimals(4)} + {balance.toEtherToFixedDecimals( + selectedMarket.displayDecimals || 4, + )} , label: 'Telegram', href: CONSTANTS.COMMUNITY_TG, gradient: 'mycard_light', }, { - icon: x.src, + icon: , label: 'Twitter', href: 'https://troves.fi/twitter', gradient: 'mycard_light', @@ -81,7 +80,7 @@ const Footer: React.FC = () => { const { isOpen, onOpen, onClose } = useDisclosure(); return ( - + { align={{ base: 'center', md: 'flex-start' }} minW="220px" > - + { key={s.label} href={s.href} aria-label={s.label} - icon={{s.label}} + icon={s.icon} target="_blank" rel="noopener noreferrer" borderRadius="full" diff --git a/src/components/HarvestTime.tsx b/src/components/HarvestTime.tsx index 6da1ee86..db8b3699 100644 --- a/src/components/HarvestTime.tsx +++ b/src/components/HarvestTime.tsx @@ -64,8 +64,8 @@ const HarvestTime: React.FC = ({ strategy, balData }) => { {!strategy.settings.hideHarvestInfo && ( @@ -176,15 +176,17 @@ const HarvestTime: React.FC = ({ strategy, balData }) => { fontSize={'12px'} fontWeight={'400'} lineHeight={'100%'} - bg={'mycard'} width={'100%'} borderRadius={'lg'} > Total rewards harvested:{' '} - {getDisplayCurrencyAmount( - harvestTime?.data?.totalStrkHarvestedByContract.STRKAmount || 0, - 2, - )}{' '} + + {getDisplayCurrencyAmount( + harvestTime?.data?.totalStrkHarvestedByContract.STRKAmount || + 0, + 2, + )}{' '} + STRK @@ -193,12 +195,13 @@ const HarvestTime: React.FC = ({ strategy, balData }) => { fontSize={'12px'} fontWeight={'400'} lineHeight={'100%'} - bg={'mycard'} width={'100%'} borderRadius={'lg'} > Total number of times harvested:{' '} - {harvestTime?.data?.totalHarvestsByContract || '-'} + + {harvestTime?.data?.totalHarvestsByContract || '-'} + )} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index fe507ffe..52ee0a39 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,6 +1,5 @@ import { ChevronDownIcon, EmailIcon } from '@chakra-ui/icons'; import { - Avatar, Box, Button, Center, @@ -16,20 +15,21 @@ import { Text, useDisclosure, } from '@chakra-ui/react'; -import { useAtom, useSetAtom } from 'jotai'; +import { useSetAtom } from 'jotai'; import { - connect, ConnectOptionsWithConnectors, StarknetkitConnector, + useStarknetkitConnectModal, + disconnect as starknetKitDisconnect, + connect, } from 'starknetkit'; import argentMobile from '@/assets/argentMobile.svg'; -import tg from '@/assets/tg.svg'; +import TgIcon from '@/assets/tg.svg'; import CONSTANTS from '@/constants'; import { useIsMobile } from '@/hooks/use-mobile'; import { getERC20Balance } from '@/store/balance.atoms'; import { addressAtom } from '@/store/claims.atoms'; -import { lastWalletAtom } from '@/store/utils.atoms'; import { getEndpoint, getTokenInfoFromName, @@ -39,7 +39,7 @@ import { standariseAddress, truncate, } from '@/utils'; -import fulllogo from '@public/fulllogo.svg'; +import FullLogoIcon from '@public/fulllogo.svg'; import { InjectedConnector, useAccount, @@ -69,11 +69,11 @@ export function getConnectors(isMobile: boolean) { chainId: constants.NetworkName.SN_MAIN, }, inAppBrowserOptions: {}, - }) as StarknetkitConnector; + }); const mobileBraavosConnector = BraavosMobileConnector.init({ inAppBrowserOptions: {}, - }) as StarknetkitConnector; + }); const argentXConnector = new InjectedConnector({ options: { @@ -169,7 +169,7 @@ interface NavbarProps { } export default function Navbar(props: NavbarProps) { - const { address, connector, account } = useAccount(); + const { address, connector } = useAccount(); const { disconnectAsync } = useDisconnect(); const setAddress = useSetAtom(addressAtom); const { data: starkProfile } = useStarkProfile({ @@ -177,8 +177,12 @@ export default function Navbar(props: NavbarProps) { useDefaultPfp: true, }); const { connect: connectSnReact } = useConnect(); + const isMobile = useIsMobile(); + const { starknetkitConnectModal } = useStarknetkitConnectModal({ + connectors: getConnectors(isMobile) as StarknetkitConnector[], + }); - const [lastWallet, setLastWallet] = useAtom(lastWalletAtom); + // const [lastWallet, setLastWallet] = useAtom(lastWalletAtom); const getTokenBalance = async (token: string, address: string) => { const tokenInfo = getTokenInfoFromName(token); @@ -187,10 +191,6 @@ export default function Navbar(props: NavbarProps) { return balance.amount.toEtherToFixedDecimals(6); }; - const isMobile = useIsMobile(); - - console.log(account, 'account'); - const connectorConfig: ConnectOptionsWithConnectors = useMemo(() => { return { modalMode: 'alwaysAsk', @@ -208,23 +208,31 @@ export default function Navbar(props: NavbarProps) { async function connectWallet(config = connectorConfig) { try { + // const { connector } = await starknetkitConnectModal(); + // if (!connector) { + // return; + // } + + // await connectSnReact({ connector: connector as any }); + + console.log(`connectWallet`, config); const { connector } = await connect(config); console.log(connector, 'connector'); if (connector) { connectSnReact({ connector: connector as any }); } + return true; } catch (error) { console.error('connectWallet error', error); + return false; } } useEffect(() => { const config = connectorConfig; - connectWallet({ - ...config, - modalMode: 'neverAsk', - }); + console.log('connecting wallet'); + connectWallet({ ...config, modalMode: 'neverAsk' }); }, []); useEffect(() => { @@ -245,13 +253,13 @@ export default function Navbar(props: NavbarProps) { }, [address]); // Set last wallet when a new wallet is connected - useEffect(() => { - console.log('lastWallet connector', connector?.name); - if (connector) { - const name: string = connector.name; - setLastWallet(name); - } - }, [connector]); + // useEffect(() => { + // console.log('lastWallet connector', connector?.name); + // if (connector) { + // const name: string = connector.name; + // setLastWallet(name); + // } + // }, [connector]); // set address atom useEffect(() => { @@ -270,7 +278,7 @@ export default function Navbar(props: NavbarProps) { zIndex={999} top="0" > - + {process.env.NEXT_PUBLIC_IGNORE_SIGNING != 'true' && }
- logo + {/* Troves */} @@ -407,18 +411,7 @@ export default function Navbar(props: NavbarProps) { variant={'ghost'} borderColor={'color2'} display={{ base: 'block', md: 'none' }} - icon={ - - } + icon={} /> + ))} + + ); +} diff --git a/src/components/TncModal.tsx b/src/components/TncModal.tsx index 0a8dc28e..ad71ebd7 100644 --- a/src/components/TncModal.tsx +++ b/src/components/TncModal.tsx @@ -47,7 +47,7 @@ export const UserTnCAtom = atomWithQuery((get) => { }); const TncModal: React.FC = (props) => { - const { address, account } = useAccount(); + const { address } = useAccount(); const [refCode, setReferralCode] = useAtom(referralCodeAtom); const searchParams = useSearchParams(); const userTncInfoRes = useAtomValue(UserTnCAtom); @@ -117,7 +117,7 @@ const TncModal: React.FC = (props) => { }); setIsSigningPending(false); } - if (!address || !account || !sigData) { + if (!address || !sigData) { return; } @@ -150,13 +150,22 @@ const TncModal: React.FC = (props) => { }, [sigData, signingError]); const handleSign = async () => { - if (!address || !account) { - return; - } - mixpanel.track('TnC agreed', { address }); + try { + if (!address) { + if (!address) { + toast.error('No Address to sign TnC'); + } - setIsSigningPending(true); - signTypedData(); + return; + } + mixpanel.track('TnC agreed', { address }); + + setIsSigningPending(true); + signTypedData(); + } catch (error) { + toast.error('TnC Signing Error'); + console.log('TnC Signing Error', error); + } }; return ( diff --git a/src/components/TxButton.tsx b/src/components/TxButton.tsx index 803650e5..660665c9 100755 --- a/src/components/TxButton.tsx +++ b/src/components/TxButton.tsx @@ -23,9 +23,11 @@ import { import { useAccount, useSendTransaction } from '@starknet-react/core'; import { useAtomValue, useSetAtom } from 'jotai'; import mixpanel from 'mixpanel-browser'; -import { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { TwitterShareButton } from 'react-share'; import { Call } from 'starknet'; +import ConfirmationDialog from './ConfirmationDialog'; +import MyNumber from '@/utils/MyNumber'; interface TxButtonProps { txInfo: StrategyTxProps; @@ -37,6 +39,9 @@ interface TxButtonProps { selectedMarket?: TokenInfo; strategy?: IStrategyProps; resetDepositForm: () => void; + onClickConfirmationPopup?: ( + amount: MyNumber, + ) => Promise; } export default function TxButton(props: TxButtonProps) { @@ -123,7 +128,36 @@ export default function TxButton(props: TxButtonProps) { return strategiesList.find((s: any) => s.id === props.strategy?.id); }, [strategiesInfo, props.strategy?.id]); + // Type the ref to match ConfirmationDialog's handleTrigger method + type ConfirmationDialogRef = { handleTrigger: () => void }; + const buttonRef = useRef(null); + const loading = ( + + Loading... + + ); + const [popupContent, setPopupContent] = useState( + loading, + ); + + async function preHandleButton() { + if (props.onClickConfirmationPopup && buttonRef.current) { + buttonRef.current.handleTrigger(); + try { + const res = await props.onClickConfirmationPopup(props.txInfo.amount); + setPopupContent(res); + } catch (error) { + setPopupContent( + Something went wrong. Please try again later., + ); + } + } else { + handleButton(); + } + } + async function handleButton() { + setPopupContent(loading); writeAsync().then((tx) => { if (props.buttonText === 'Deposit') onOpen(); mixpanel.track('Submitted tx', { @@ -229,6 +263,14 @@ export default function TxButton(props: TxButtonProps) { + { + setPopupContent(loading); + }} + />