diff --git a/src/app/api/stats/[address]/route.ts b/src/app/api/stats/[address]/route.ts index e1661eb2..c7369b10 100755 --- a/src/app/api/stats/[address]/route.ts +++ b/src/app/api/stats/[address]/route.ts @@ -22,6 +22,13 @@ export async function GET(_req: Request, context: any) { return { id: strategy.id, usdValue: balanceInfo.usdValue, + tokenInfo: { + name: balanceInfo.tokenInfo.name, + symbol: balanceInfo.tokenInfo.name, + logo: balanceInfo.tokenInfo.logo, + decimals: balanceInfo.tokenInfo.decimals, + displayDecimals: balanceInfo.tokenInfo.displayDecimals, + }, amount: balanceInfo.amount.toEtherStr(), }; }); diff --git a/src/app/api/strategies/route.ts b/src/app/api/strategies/route.ts index 29a3de66..6fb479fe 100755 --- a/src/app/api/strategies/route.ts +++ b/src/app/api/strategies/route.ts @@ -1,13 +1,14 @@ import { NextResponse } from 'next/server'; import { atom } from 'jotai'; -import ZkLendAtoms from '@/store/zklend.store'; -import { PoolInfo } from '@/store/pools'; -import NostraLendingAtoms from '@/store/nostralending.store'; +import ZkLendAtoms, { zkLend } from '@/store/zklend.store'; +import { PoolInfo, PoolType } from '@/store/pools'; +import NostraLendingAtoms, { nostraLending } from '@/store/nostralending.store'; import { RpcProvider } from 'starknet'; import { getLiveStatusNumber, getStrategies } from '@/store/strategies.atoms'; import { MY_STORE } from '@/store'; import MyNumber from '@/utils/MyNumber'; import { IStrategy, NFTInfo, TokenInfo } from '@/strategies/IStrategy'; +import { STRKFarmStrategyAPIResult } from '@/store/strkfarm.atoms'; export const revalidate = 3600; // 1 hr @@ -15,18 +16,25 @@ const allPoolsAtom = atom((get) => { const pools: PoolInfo[] = []; const poolAtoms = [ZkLendAtoms, NostraLendingAtoms]; return poolAtoms.reduce((_pools, p) => _pools.concat(get(p.pools)), pools); - return []; }); async function getPools(store: any, retry = 0) { - const allPools = store.get(allPoolsAtom); - if (!allPools.length && retry < 10) { + const allPools: PoolInfo[] | undefined = store.get(allPoolsAtom); + + const minProtocolsRequired = [zkLend.name, nostraLending.name]; + const hasRequiredPools = minProtocolsRequired.every((p) => { + if (!allPools) return false; + return allPools.some( + (pool) => pool.protocol.name === p && pool.type == PoolType.Lending, + ); + }); + const MAX_RETRIES = 120; + if (retry >= MAX_RETRIES) { + throw new Error('Failed to fetch pools'); + } else if (!allPools || !hasRequiredPools) { await new Promise((resolve) => setTimeout(resolve, 1000)); return getPools(store, retry + 1); } - if (retry >= 10) { - throw new Error('Failed to fetch pools'); - } return allPools; } @@ -34,7 +42,9 @@ const provider = new RpcProvider({ nodeUrl: process.env.RPC_URL || 'https://starknet-mainnet.public.blastapi.io', }); -async function getStrategyInfo(strategy: IStrategy) { +async function getStrategyInfo( + strategy: IStrategy, +): Promise { const tvl = await strategy.getTVL(); return { @@ -67,8 +77,13 @@ async function getStrategyInfo(strategy: IStrategy) { export async function GET(req: Request) { const allPools = await getPools(MY_STORE); const strategies = getStrategies(); + strategies.forEach((strategy) => { - strategy.solve(allPools, '1000'); + try { + strategy.solve(allPools, '1000'); + } catch (err) { + console.error('Error solving strategy', strategy.name, err); + } }); const stratsDataProms: any[] = []; diff --git a/src/components/Strategies.tsx b/src/components/Strategies.tsx index 235a91ac..91aefdf9 100755 --- a/src/components/Strategies.tsx +++ b/src/components/Strategies.tsx @@ -1,7 +1,5 @@ import CONSTANTS from '@/constants'; -import { strategiesAtom } from '@/store/strategies.atoms'; import { - Box, Container, Link, Skeleton, @@ -15,16 +13,20 @@ import { } from '@chakra-ui/react'; import { useAtomValue } from 'jotai'; import React, { useMemo } from 'react'; -import { userStatsAtom } from '@/store/utils.atoms'; -import { allPoolsAtomUnSorted, filteredPools } from '@/store/protocols'; -import { addressAtom } from '@/store/claims.atoms'; +import { filteredPools } from '@/store/protocols'; import { usePagination } from '@ajna/pagination'; import { YieldStrategyCard } from './YieldCard'; +import { + STRKFarmBaseAPYsAtom, + STRKFarmStrategyAPIResult, +} from '@/store/strkfarm.atoms'; export default function Strategies() { - const allPools = useAtomValue(allPoolsAtomUnSorted); - const strategies = useAtomValue(strategiesAtom); - const { data: userData } = useAtomValue(userStatsAtom); - const address = useAtomValue(addressAtom); + const strkFarmPoolsRes = useAtomValue(STRKFarmBaseAPYsAtom); + const strkFarmPools = useMemo(() => { + if (!strkFarmPoolsRes || !strkFarmPoolsRes.data) + return [] as STRKFarmStrategyAPIResult[]; + return strkFarmPoolsRes.data.strategies.sort((a, b) => b.apy - a.apy); + }, [strkFarmPoolsRes]); const _filteredPools = useAtomValue(filteredPools); const ITEMS_PER_PAGE = 15; @@ -57,29 +59,18 @@ export default function Strategies() { - {allPools.length > 0 && strategies.length > 0 && ( + {strkFarmPools.length > 0 && ( <> - {strategies.map((strat, index) => { + {strkFarmPools.map((pool, index) => { return ( - + ); })} )} - {allPools.length > 0 && strategies.length === 0 && ( - - - No strategies. Check back soon. - - - )} - {allPools.length === 0 && ( + {strkFarmPools.length === 0 && ( diff --git a/src/components/TncModal.tsx b/src/components/TncModal.tsx index 49f10847..302e67a2 100644 --- a/src/components/TncModal.tsx +++ b/src/components/TncModal.tsx @@ -18,7 +18,7 @@ import axios from 'axios'; import { atomWithQuery } from 'jotai-tanstack-query'; import React, { useEffect, useMemo, useState } from 'react'; import { UserTncInfo } from '@/app/api/interfaces'; -import { useAtomValue, useSetAtom } from 'jotai'; +import { useAtom, useAtomValue } from 'jotai'; import { referralCodeAtom } from '@/store/referral.store'; import { useSearchParams } from 'next/navigation'; import { generateReferralCode } from '@/utils'; @@ -44,10 +44,13 @@ export const UserTnCAtom = atomWithQuery((get) => { const TncModal: React.FC = (props) => { const { address, account } = useAccount(); - const setReferralCode = useSetAtom(referralCodeAtom); + const [refCode, setReferralCode] = useAtom(referralCodeAtom); const searchParams = useSearchParams(); const userTncInfoRes = useAtomValue(UserTnCAtom); - const userTncInfo = useMemo(() => userTncInfoRes.data, [userTncInfoRes]); + const userTncInfo = useMemo( + () => userTncInfoRes.data, + [userTncInfoRes, refCode], + ); const { isOpen, onOpen, onClose } = useDisclosure(); const [isSigningPending, setIsSigningPending] = useState(false); const { disconnectAsync } = useDisconnect(); @@ -64,6 +67,8 @@ const TncModal: React.FC = (props) => { !userTncInfo.user.isTncSigned ) { onOpen(); + } else { + onClose(); } return; } diff --git a/src/components/YieldCard.tsx b/src/components/YieldCard.tsx index dc8a2f57..8e774e64 100644 --- a/src/components/YieldCard.tsx +++ b/src/components/YieldCard.tsx @@ -22,7 +22,7 @@ import { VStack, } from '@chakra-ui/react'; import shield from '@/assets/shield.svg'; -import { IStrategyProps, StrategyLiveStatus } from '@/strategies/IStrategy'; +import { StrategyLiveStatus } from '@/strategies/IStrategy'; import { useAtomValue } from 'jotai'; import { getDisplayCurrencyAmount } from '@/utils'; import { addressAtom } from '@/store/claims.atoms'; @@ -32,6 +32,7 @@ import { getPoolInfoFromStrategy } from '@/store/protocols'; import { TriangleDownIcon, TriangleUpIcon } from '@chakra-ui/icons'; import { useState } from 'react'; import mixpanel from 'mixpanel-browser'; +import { STRKFarmStrategyAPIResult } from '@/store/strkfarm.atoms'; interface YieldCardProps { pool: PoolInfo; @@ -206,12 +207,29 @@ function isLive(status: StrategyLiveStatus) { ); } -function getStrategyWiseInfo( +function getStrategyWiseHoldingsInfo( userData: UserStats | null | undefined, id: string, ) { const amount = userData?.strategyWise.find((item) => item.id === id); - return amount?.usdValue ? amount?.usdValue : 0; + if (!amount) { + return { + usdValue: 0, + amount: 0, + tokenInfo: { + symbol: '', + decimals: 0, + displayDecimals: 0, + logo: '', + name: '', + }, + }; + } + return { + usdValue: amount.usdValue, + amount: Number(amount.amount), + tokenInfo: amount.tokenInfo, + }; } function StrategyTVL(props: YieldCardProps) { @@ -219,6 +237,8 @@ function StrategyTVL(props: YieldCardProps) { const address = useAtomValue(addressAtom); const { data: userData } = useAtomValue(userStatsAtom); + const holdingsInfo = getStrategyWiseHoldingsInfo(userData, pool.pool.id); + const isPoolLive = pool.additional && pool.additional.tags[0] && @@ -247,17 +267,44 @@ function StrategyTVL(props: YieldCardProps) { borderRadius={'20px'} color="grey_text" fontSize={'12px'} + width={'100%'} + mt="5px" > - <> - - - $ - {Math.round( - getStrategyWiseInfo(userData, pool.pool.id), - ).toLocaleString()} - - - + + + + + ${getDisplayCurrencyAmount(holdingsInfo.usdValue, 0)} + + + + + + {holdingsInfo.amount != 0 && ( + + {/* */} + + {getDisplayCurrencyAmount( + holdingsInfo.amount, + holdingsInfo.tokenInfo.displayDecimals, + ).toLocaleString()} + + + + )} + + )} @@ -470,16 +517,11 @@ export default function YieldCard(props: YieldCardProps) { } export function YieldStrategyCard(props: { - strat: IStrategyProps; + strat: STRKFarmStrategyAPIResult; index: number; }) { - const tvlInfo = useAtomValue(props.strat.tvlAtom); - const pool = getPoolInfoFromStrategy( - props.strat, - tvlInfo.data?.usdValue || 0, - ); - - return ; + const strat = getPoolInfoFromStrategy(props.strat); + return ; } export function HeaderSorter(props: { diff --git a/src/store/protocols.ts b/src/store/protocols.ts index abb187b3..81eeaa16 100644 --- a/src/store/protocols.ts +++ b/src/store/protocols.ts @@ -14,8 +14,11 @@ import CarmineAtoms, { carmine } from './carmine.store'; import { atom } from 'jotai'; import { Category, PoolInfo, PoolType } from './pools'; import strkfarmLogo from '@public/logo.png'; -import { IStrategyProps } from '@/strategies/IStrategy'; -import STRKFarmAtoms, { strkfarm } from './strkfarm.atoms'; +import STRKFarmAtoms, { + strkfarm, + STRKFarmStrategyAPIResult, +} from './strkfarm.atoms'; +import { getLiveStatusEnum } from './strategies.atoms'; export const PROTOCOLS = [ { @@ -151,8 +154,7 @@ export const allPoolsAtomUnSorted = atom((get) => { }); export function getPoolInfoFromStrategy( - strat: IStrategyProps, - tvlInfo: number, + strat: STRKFarmStrategyAPIResult, ): PoolInfo { let category = Category.Others; if (strat.name.includes('STRK')) { @@ -164,18 +166,18 @@ export function getPoolInfoFromStrategy( pool: { id: strat.id, name: strat.name, - logos: [strat.holdingTokens[0].logo], + logos: [strat.logo], }, protocol: { name: 'STRKFarm', link: `/strategy/${strat.id}`, logo: strkfarmLogo.src, }, - tvl: tvlInfo, - apr: strat.netYield, + tvl: strat.tvlUsd, + apr: strat.apy, aprSplits: [ { - apr: strat.netYield, + apr: strat.apy, title: 'Net Yield', description: 'Includes fees & Defi spring rewards', }, @@ -191,7 +193,7 @@ export function getPoolInfoFromStrategy( }, additional: { riskFactor: strat.riskFactor, - tags: [strat.liveStatus], + tags: [getLiveStatusEnum(strat.status.number)], isAudited: true, leverage: strat.leverage, }, diff --git a/src/store/strkfarm.atoms.ts b/src/store/strkfarm.atoms.ts index c6cd91af..d3ceaa37 100644 --- a/src/store/strkfarm.atoms.ts +++ b/src/store/strkfarm.atoms.ts @@ -103,20 +103,22 @@ export class STRKFarm extends IDapp { } } +export const STRKFarmBaseAPYsAtom = atomWithQuery((get) => ({ + queryKey: ['strkfarm_base_aprs'], + queryFn: async ({ + queryKey, + }): Promise<{ + strategies: STRKFarmStrategyAPIResult[]; + }> => { + const response = await fetch(`${CONSTANTS.STRKFarm.BASE_APR_API}`); + const data = await response.json(); + return data; + }, +})); + export const strkfarm = new STRKFarm(); const STRKFarmAtoms: ProtocolAtoms = { - baseAPRs: atomWithQuery((get) => ({ - queryKey: ['strkfarm_base_aprs'], - queryFn: async ({ - queryKey, - }): Promise<{ - strategies: STRKFarmStrategyAPIResult[]; - }> => { - const response = await fetch(`${CONSTANTS.STRKFarm.BASE_APR_API}`); - const data = await response.json(); - return data; - }, - })), + baseAPRs: STRKFarmBaseAPYsAtom, pools: atom((get) => { const empty: PoolInfo[] = []; if (!STRKFarmAtoms.baseAPRs) return empty; diff --git a/src/store/utils.atoms.ts b/src/store/utils.atoms.ts index 7c78f458..23ada1da 100755 --- a/src/store/utils.atoms.ts +++ b/src/store/utils.atoms.ts @@ -80,6 +80,13 @@ interface StrategyWise { id: string; usdValue: number; amount: string; + tokenInfo: { + name: string; + symbol: string; + logo: string; + decimals: number; + displayDecimals: number; + }; } export interface UserStats { diff --git a/src/strategies/IStrategy.ts b/src/strategies/IStrategy.ts index db40a895..02dd22d8 100755 --- a/src/strategies/IStrategy.ts +++ b/src/strategies/IStrategy.ts @@ -252,14 +252,16 @@ export class IStrategy extends IStrategyProps { return eligiblePools; } - filterStrkzkLend( - pools: PoolInfo[], - amount: string, - prevActions: StrategyAction[], - ) { - return pools.filter( - (p) => p.pool.name == 'STRK' && p.protocol.name == zkLend.name, - ); + filterZkLend(tokenName: string) { + return ( + pools: PoolInfo[], + amount: string, + prevActions: StrategyAction[], + ) => { + return pools.filter( + (p) => p.pool.name == tokenName && p.protocol.name == zkLend.name, + ); + }; } optimizerDeposit( @@ -285,14 +287,24 @@ export class IStrategy extends IStrategyProps { for (let i = 0; i < this.steps.length; ++i) { const step = this.steps[i]; let _pools = [...pools]; + console.debug('checking solve'); for (let j = 0; j < step.filter.length; ++j) { const filter = step.filter[j]; _pools = filter.bind(this)(_pools, amount, this.actions); } - console.log('solve', i, _pools, pools.length, this.actions, _amount); + console.debug( + 'solve', + { + i, + poolsLen: pools.length, + _amount, + }, + this.actions, + ); if (_pools.length > 0) { + console.debug('solving', step.name); this.actions = step.optimizer.bind(this)( _pools, _amount, @@ -309,18 +321,19 @@ export class IStrategy extends IStrategyProps { } } } catch (err) { - console.warn(`${this.tag} - unsolved`, err); + console.error(`${this.tag} - unsolved`, err); return; } + console.debug('Completed solving actions'); this.actions.forEach((action) => { const sign = action.isDeposit ? 1 : -1; const apr = action.isDeposit ? action.pool.apr : action.pool.borrow.apr; netYield += sign * apr * Number(action.amount); - console.log('netYield1', sign, apr, action.amount, netYield); + console.debug('netYield1', sign, apr, action.amount, netYield); }); this.netYield = netYield / Number(amount); - console.log('netYield', netYield, this.netYield); + console.debug('netYield', netYield, this.netYield); this.leverage = this.netYield / this.actions[0].pool.apr; this.postSolve(); diff --git a/src/strategies/auto_strk.strat.ts b/src/strategies/auto_strk.strat.ts index be091ade..f4aa5ebb 100755 --- a/src/strategies/auto_strk.strat.ts +++ b/src/strategies/auto_strk.strat.ts @@ -73,12 +73,12 @@ export class AutoTokenStrategy extends IStrategy { { name: `Supplies your ${token} to zkLend`, optimizer: this.optimizer, - filter: [this.filterStrkzkLend], + filter: [this.filterZkLend(this.token.name)], }, { name: `Re-invest your STRK Rewards every 7 days`, optimizer: this.compounder, - filter: [this.filterStrkzkLend], + filter: [this.filterZkLend('STRK')], }, ]; const _risks = [...this.risks]; diff --git a/src/strategies/delta_neutral_mm.ts b/src/strategies/delta_neutral_mm.ts index 90a61887..afbfbeab 100755 --- a/src/strategies/delta_neutral_mm.ts +++ b/src/strategies/delta_neutral_mm.ts @@ -93,7 +93,7 @@ export class DeltaNeutralMM extends IStrategy { { name: `Re-invest your STRK Rewards every 7 days (Compound)`, optimizer: this.compounder, - filter: [this.filterStrkzkLend], + filter: [this.filterZkLend('STRK')], }, ]; diff --git a/src/utils.ts b/src/utils.ts index cccf8f9b..0d667a4f 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -105,7 +105,9 @@ export function getDisplayCurrencyAmount( amount: string | number, decimals: number, ) { - return Number(Number(amount).toFixed(decimals)).toLocaleString(); + return Number(Number(amount).toFixed(decimals)).toLocaleString(undefined, { + minimumFractionDigits: decimals, + }); } // returns time to endtime in days, hours, minutes @@ -149,7 +151,7 @@ export async function getPrice(tokenInfo: TokenInfo) { try { return await getPriceFromMyAPI(tokenInfo); } catch (e) { - console.error('getPriceFromMyAPI error', e); + console.warn('getPriceFromMyAPI error', e); } console.log('getPrice coinbase', tokenInfo.name); const priceInfo = await axios.get( @@ -171,6 +173,9 @@ export async function getPriceFromMyAPI(tokenInfo: TokenInfo) { console.log('getPrice from redis', tokenInfo.name); const endpoint = getEndpoint(); + if (endpoint.includes('localhost')) { + throw new Error('getEndpoint: skip redis'); + } const priceInfo = await axios.get(`${endpoint}/api/price/${tokenInfo.name}`); const now = new Date(); const priceTime = new Date(priceInfo.data.timestamp);