improve add boost ux

This commit is contained in:
saml33 2024-06-24 16:29:28 +10:00
parent 5deb0649b7
commit 88d016aa9d
11 changed files with 733 additions and 366 deletions

View File

@ -1,39 +1,42 @@
import Image from 'next/image' import Image from 'next/image'
import { formatTokenSymbol } from 'utils/tokens' import { formatTokenSymbol } from 'utils/tokens'
import useBankRates from 'hooks/useBankRates'
import useLeverageMax from 'hooks/useLeverageMax'
import mangoStore from '@store/mangoStore' import mangoStore from '@store/mangoStore'
import SheenLoader from './shared/SheenLoader' import SheenLoader from './shared/SheenLoader'
import { SOL_YIELD } from './Stake'
import Tooltip from './shared/Tooltip' import Tooltip from './shared/Tooltip'
import Link from 'next/link' import Link from 'next/link'
import { StakeableToken } from 'hooks/useStakeableTokens'
export const HERO_TOKEN_BUTTON_CLASSES =
'inner-shadow-bottom default-transition relative w-full rounded-xl border border-th-bkg-3 bg-th-bkg-1 px-6 py-4 text-th-fgd-1 focus:outline-none focus-visible:border-th-fgd-4 md:hover:bg-th-bkg-2 md:hover:focus-visible:border-th-fgd-4'
export const HERO_TOKEN_IMAGE_WRAPPER_CLASSES =
'inner-shadow-bottom-sm mb-2 flex h-14 w-14 items-center justify-center rounded-full border border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2 shrink-0'
const HeroTokenButton = ({ const HeroTokenButton = ({
onClick, onClick,
tokenName, tokenInfo,
}: { }: {
tokenName: string tokenInfo: StakeableToken
onClick: () => void onClick: () => void
}) => { }) => {
const leverage = useLeverageMax(tokenName) const { symbol, name } = tokenInfo.token
const { APY } = tokenInfo.financialMetrics
// const leverage = useLeverageMax(symbol)
const groupLoaded = mangoStore((s) => s.groupLoaded) const groupLoaded = mangoStore((s) => s.groupLoaded)
const { stakeBankDepositRate, financialMetrics } = useBankRates( // const { stakeBankDepositRate, financialMetrics } = useBankRates(
tokenName, // symbol,
leverage, // leverage,
) // )
const { financialMetrics: estimatedNetAPYFor1xLev } = useBankRates( // const { financialMetrics: estimatedNetAPYFor1xLev } = useBankRates(symbol, 1)
tokenName,
1,
)
const APY_Daily_Compound = // const APY_Daily_Compound =
Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1 // Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1
const UiRate = // const UiRate =
tokenName === 'USDC' // symbol === 'USDC'
? APY_Daily_Compound * 100 // ? APY_Daily_Compound * 100
: Math.max(estimatedNetAPYFor1xLev.APY, financialMetrics.APY) // : Math.max(estimatedNetAPYFor1xLev.APY, financialMetrics.APY)
const renderRateEmoji = (token: string, rate: number) => { const renderRateEmoji = (token: string, rate: number) => {
if (token.toLowerCase().includes('sol')) { if (token.toLowerCase().includes('sol')) {
@ -53,41 +56,64 @@ const HeroTokenButton = ({
} }
} }
const emoji = renderRateEmoji(tokenName, UiRate) const emoji = renderRateEmoji(symbol, APY)
return ( return (
<button <button className={HERO_TOKEN_BUTTON_CLASSES} onClick={onClick}>
className={`inner-shadow-bottom default-transition relative w-full rounded-xl border border-th-bkg-3 bg-th-bkg-1 p-6 text-th-fgd-1 focus:outline-none focus-visible:border-th-fgd-4 md:hover:bg-th-bkg-2 md:hover:focus-visible:border-th-fgd-4`}
onClick={onClick}
>
<div> <div>
<div className="flex flex-col items-center"> <div className="flex items-center space-x-2.5">
<div <div className={HERO_TOKEN_IMAGE_WRAPPER_CLASSES}>
className={`inner-shadow-bottom-sm mb-2 flex h-14 w-14 items-center justify-center rounded-full border border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2`}
>
<Image <Image
src={`/icons/${tokenName.toLowerCase()}.svg`} src={`/icons/${symbol.toLowerCase()}.svg`}
width={32} width={32}
height={32} height={32}
alt="Select a token" alt="Select a token"
/> />
</div> </div>
<div className="flex flex-col items-center"> <div className="flex w-full justify-between">
<p className={`text-th-fgd-1`}>{formatTokenSymbol(tokenName)}</p> <div className="text-left">
<span className={`text-2xl font-bold`}> <span className="text-xl font-bold">
{!groupLoaded ? ( {formatTokenSymbol(symbol)}
<SheenLoader> </span>
<div className={`h-6 w-10 bg-th-bkg-2`} /> <p className={`text-xs text-th-fgd-4`}>{name}</p>
</SheenLoader> </div>
) : !UiRate || isNaN(UiRate) ? ( <div className="text-right">
<span className="text-base font-normal text-th-fgd-4"> <p className={`text-xs text-th-fgd-4`}>Max APY</p>
Rate Unavailable <div className="flex items-center">
{emoji ? (
<Tooltip
content={
<>
<p className="mb-2">
The max APY is favorable right now. Rates can change
very quickly. Make sure you understand the risks
before boosting.
</p>
<Link href="/risks" shallow>
Risks
</Link>
</>
}
>
<span className="mr-2 text-lg">{emoji}</span>
</Tooltip>
) : null}
<span className={`text-xl font-bold`}>
{!groupLoaded ? (
<SheenLoader>
<div className={`h-6 w-10 bg-th-bkg-2`} />
</SheenLoader>
) : !APY || isNaN(APY) ? (
<span className="text-base font-normal text-th-fgd-4">
Rate Unavailable
</span>
) : (
`${APY.toFixed(2)}%`
)}
</span> </span>
) : ( </div>
`${UiRate.toFixed(2)}%` </div>
)} {/* {groupLoaded ? (
</span>
{groupLoaded ? (
<div className="mt-1 flex items-center"> <div className="mt-1 flex items-center">
{SOL_YIELD.includes(tokenName) ? ( {SOL_YIELD.includes(tokenName) ? (
<> <>
@ -113,37 +139,10 @@ const HeroTokenButton = ({
</> </>
)} )}
</div> </div>
) : null} ) : null} */}
</div> </div>
</div> </div>
</div> </div>
{emoji ? (
<div
className="absolute left-0 top-0 h-0 w-0 rounded-tl-xl"
style={{
borderTopWidth: '100px',
borderRightWidth: '100px',
borderTopColor: 'var(--bkg-2)',
borderRightColor: 'transparent',
}}
>
<Tooltip
content={
<>
<p className="mb-2">
The max APY is favorable right now. Rates can change very
quickly. Make sure you understand the risks before boosting.
</p>
<Link href="/risks" shallow>
Risks
</Link>
</>
}
>
<span className="absolute bottom-12 left-4 text-2xl">{emoji}</span>
</Tooltip>
</div>
) : null}
</button> </button>
) )
} }

View File

@ -2,8 +2,11 @@ import useMangoGroup from 'hooks/useMangoGroup'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { SHOW_INACTIVE_POSITIONS_KEY } from 'utils/constants' import { SHOW_INACTIVE_POSITIONS_KEY } from 'utils/constants'
import TokenLogo from './shared/TokenLogo' import TokenLogo from './shared/TokenLogo'
import Button from './shared/Button' import Button, { IconButton } from './shared/Button'
import { formatTokenSymbol } from 'utils/tokens' import {
formatTokenSymbol,
getStakableTokensDataForTokenName,
} from 'utils/tokens'
import mangoStore, { ActiveTab } from '@store/mangoStore' import mangoStore, { ActiveTab } from '@store/mangoStore'
import Switch from './forms/Switch' import Switch from './forms/Switch'
import useLocalStorageState from 'hooks/useLocalStorageState' import useLocalStorageState from 'hooks/useLocalStorageState'
@ -15,10 +18,16 @@ import {
} from '@blockworks-foundation/mango-v4' } from '@blockworks-foundation/mango-v4'
import useBankRates from 'hooks/useBankRates' import useBankRates from 'hooks/useBankRates'
import usePositions from 'hooks/usePositions' import usePositions from 'hooks/usePositions'
import { AdjustmentsHorizontalIcon } from '@heroicons/react/20/solid' import {
AdjustmentsHorizontalIcon,
ArrowLeftIcon,
} from '@heroicons/react/20/solid'
import EditLeverageModal from './modals/EditLeverageModal' import EditLeverageModal from './modals/EditLeverageModal'
import Tooltip from './shared/Tooltip' import Tooltip from './shared/Tooltip'
import { useWallet } from '@solana/wallet-adapter-react' import { useWallet } from '@solana/wallet-adapter-react'
import UnstakeForm from './UnstakeForm'
import StakeForm from './StakeForm'
import DespositForm from './DepositForm'
const set = mangoStore.getState().set const set = mangoStore.getState().set
@ -35,18 +44,20 @@ const Positions = ({
}: { }: {
setActiveTab: (tab: ActiveTab) => void setActiveTab: (tab: ActiveTab) => void
}) => { }) => {
const selectedToken = mangoStore((s) => s.selectedToken)
const [showInactivePositions, setShowInactivePositions] = const [showInactivePositions, setShowInactivePositions] =
useLocalStorageState(SHOW_INACTIVE_POSITIONS_KEY, true) useLocalStorageState(SHOW_INACTIVE_POSITIONS_KEY, false)
const { positions, jlpBorrowBank, lstBorrowBank } = usePositions( const { positions, jlpBorrowBank, lstBorrowBank } = usePositions(
showInactivePositions, showInactivePositions,
) )
const [showAddRemove, setShowAddRemove] = useState('')
const numberOfPositions = useMemo(() => { const numberOfPositions = useMemo(() => {
if (!positions.length) return 0 if (!positions.length) return 0
return positions.filter((pos) => pos.stakeBalance > 0).length return positions.filter((pos) => pos.stakeBalance > 0).length
}, [positions]) }, [positions])
return ( return !showAddRemove ? (
<> <>
<div className="mb-2 flex items-center justify-between rounded-lg border-2 border-th-fgd-1 bg-th-bkg-1 px-6 py-3.5"> <div className="mb-2 flex items-center justify-between rounded-lg border-2 border-th-fgd-1 bg-th-bkg-1 px-6 py-3.5">
<p className="font-medium">{`You have ${numberOfPositions} active position${ <p className="font-medium">{`You have ${numberOfPositions} active position${
@ -69,6 +80,7 @@ const Positions = ({
key={bank.name} key={bank.name}
position={position} position={position}
setActiveTab={setActiveTab} setActiveTab={setActiveTab}
setShowAddRemove={setShowAddRemove}
borrowBank={isUsdcBorrow ? jlpBorrowBank : lstBorrowBank} borrowBank={isUsdcBorrow ? jlpBorrowBank : lstBorrowBank}
/> />
) : null ) : null
@ -80,29 +92,84 @@ const Positions = ({
)} )}
</div> </div>
</> </>
) : (
<div
className={`rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 p-6 text-th-fgd-1 md:p-8`}
>
<div className="mb-3 flex items-center space-x-3">
<IconButton
onClick={() => setShowAddRemove('')}
size="medium"
isPrimary
>
<ArrowLeftIcon className="h-5 w-5" />
</IconButton>
<h2>
{showAddRemove === 'add' ? 'Add' : 'Withdraw'} {selectedToken}
</h2>
</div>
{showAddRemove === 'add' ? (
selectedToken === 'USDC' ? (
<DespositForm
token="USDC"
clientContext={
getStakableTokensDataForTokenName('USDC').clientContext
}
/>
) : (
<StakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)?.clientContext
}
/>
)
) : (
<UnstakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)?.clientContext
}
/>
)}
</div>
) )
} }
const PositionItem = ({ const PositionItem = ({
position, position,
setActiveTab, setActiveTab,
setShowAddRemove,
borrowBank, borrowBank,
}: { }: {
position: Position position: Position
setActiveTab: (v: ActiveTab) => void setActiveTab: (v: ActiveTab) => void
setShowAddRemove: (v: 'add' | 'remove') => void
borrowBank: Bank | undefined borrowBank: Bank | undefined
}) => { }) => {
const { connected } = useWallet() const { connected } = useWallet()
const { jlpGroup, lstGroup } = useMangoGroup() const { jlpGroup, lstGroup } = useMangoGroup()
const { stakeBalance, bank, pnl, acct } = position const { stakeBalance, bank, pnl, acct } = position
const [showEditLeverageModal, setShowEditLeverageModal] = useState(false)
const handleAddOrManagePosition = (token: string) => { const handleAddNoPosition = (token: string) => {
setActiveTab('Boost!') setActiveTab('Boost!')
set((state) => { set((state) => {
state.selectedToken = token state.selectedToken = token
}) })
} }
const [showEditLeverageModal, setShowEditLeverageModal] = useState(false) const handleAddPosition = (token: string) => {
setShowAddRemove('add')
set((state) => {
state.selectedToken = token
})
}
const handleRemovePosition = (token: string) => {
setShowAddRemove('remove')
set((state) => {
state.selectedToken = token
})
}
const leverage = useMemo(() => { const leverage = useMemo(() => {
if (!acct || !bank) return 1 if (!acct || !bank) return 1
@ -162,11 +229,24 @@ const PositionItem = ({
<p>${bank.uiPrice.toFixed(2)}</p> <p>${bank.uiPrice.toFixed(2)}</p>
</div> </div>
</div> </div>
<Button onClick={() => handleAddOrManagePosition(bank.name)}> {stakeBalance ? (
<p className="mb-1 text-base tracking-wider text-th-bkg-1"> <div className="flex space-x-2">
{stakeBalance ? 'Add/Remove' : `Boost! ${bank.name}`} <Button onClick={() => handleAddPosition(bank.name)}>
</p> <p className="mb-1 text-base tracking-wider text-th-bkg-1">Add</p>
</Button> </Button>
<Button onClick={() => handleRemovePosition(bank.name)}>
<p className="mb-1 text-base tracking-wider text-th-bkg-1">
Withdraw
</p>
</Button>
</div>
) : (
<Button onClick={() => handleAddNoPosition(bank.name)}>
<p className="mb-1 text-base tracking-wider text-th-bkg-1">
{`Boost! ${bank.name}`}
</p>
</Button>
)}
</div> </div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div> <div>

View File

@ -1,10 +1,7 @@
import TokenButton from './TokenButton' import TokenButton from './TokenButton'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import TabUnderline from './shared/TabUnderline' import StakeForm from '@components/StakeForm'
import StakeForm, { walletBalanceForToken } from '@components/StakeForm'
import UnstakeForm from '@components/UnstakeForm'
import mangoStore from '@store/mangoStore' import mangoStore from '@store/mangoStore'
import { STAKEABLE_TOKENS } from 'utils/constants'
import { import {
formatTokenSymbol, formatTokenSymbol,
getStakableTokensDataForTokenName, getStakableTokensDataForTokenName,
@ -15,10 +12,13 @@ import DespositForm from './DepositForm'
import { EnterBottomExitBottom } from './shared/Transitions' import { EnterBottomExitBottom } from './shared/Transitions'
import TokenSelect from './TokenSelect' import TokenSelect from './TokenSelect'
import Label from './forms/Label' import Label from './forms/Label'
import usePositions from 'hooks/usePositions'
import { IconButton } from './shared/Button' import { IconButton } from './shared/Button'
import HeroTokenButton from './HeroTokenButton' import HeroTokenButton, {
import ButtonGroup from './forms/ButtonGroup' HERO_TOKEN_BUTTON_CLASSES,
HERO_TOKEN_IMAGE_WRAPPER_CLASSES,
} from './HeroTokenButton'
import Image from 'next/image'
import useStakeableTokens, { StakeableToken } from 'hooks/useStakeableTokens'
const set = mangoStore.getState().set const set = mangoStore.getState().set
@ -37,13 +37,12 @@ export const SOL_YIELD = [
const USDC_YIELD = ['JLP', 'USDC'] const USDC_YIELD = ['JLP', 'USDC']
const Stake = () => { const Stake = () => {
const [activeFormTab, setActiveFormTab] = useState('Add') const [tokensToShow, setTokensToShow] = useState('')
const [tokensToShow, setTokensToShow] = useState('All')
const [showTokenSelect, setShowTokenSelect] = useState(false) const [showTokenSelect, setShowTokenSelect] = useState(false)
const selectedToken = mangoStore((s) => s.selectedToken) const selectedToken = mangoStore((s) => s.selectedToken)
const walletTokens = mangoStore((s) => s.wallet.tokens) // const walletTokens = mangoStore((s) => s.wallet.tokens)
const { isDesktop } = useViewport() const { isDesktop } = useViewport()
const { positions } = usePositions() const { stakeableTokens } = useStakeableTokens()
const handleTokenSelect = useCallback((token: string) => { const handleTokenSelect = useCallback((token: string) => {
set((state) => { set((state) => {
@ -52,64 +51,38 @@ const Stake = () => {
setShowTokenSelect(false) setShowTokenSelect(false)
}, []) }, [])
const hasPosition = useMemo(() => {
if (!positions || !selectedToken) return false
return positions.find((position) => position.bank.name === selectedToken)
}, [positions, selectedToken])
const handleTabChange = useCallback(
(tab: string) => {
setActiveFormTab(tab)
if (tab === 'Remove' && positions?.length && !hasPosition) {
set((state) => {
state.selectedToken = positions[0].bank.name
})
}
if (tab === 'Add' && selectedToken) {
set((state) => {
state.selectedToken = ''
})
}
},
[hasPosition, positions, selectedToken],
)
const selectableTokens = useMemo(() => { const selectableTokens = useMemo(() => {
if (activeFormTab === 'Add') { return stakeableTokens.sort((a: StakeableToken, b: StakeableToken) => {
return STAKEABLE_TOKENS.sort((a: string, b: string) => { // const aClientContext = getStakableTokensDataForTokenName(
if (activeFormTab === 'Add') { // a.token.symbol,
const aClientContext = // ).clientContext
getStakableTokensDataForTokenName(a).clientContext // const aWalletBalance = walletBalanceForToken(
const aWalletBalance = walletBalanceForToken( // walletTokens,
walletTokens, // a.token.symbol,
a, // aClientContext,
aClientContext, // )
) // const bClientContext = getStakableTokensDataForTokenName(
const bClientContext = // b.token.symbol,
getStakableTokensDataForTokenName(b).clientContext // ).clientContext
const bWalletBalance = walletBalanceForToken( // const bWalletBalance = walletBalanceForToken(
walletTokens, // walletTokens,
b, // b.token.symbol,
bClientContext, // bClientContext,
) // )
return bWalletBalance.maxAmount - aWalletBalance.maxAmount
} else { // const aMaxAmount = aWalletBalance.maxAmount
const aHasPosition = positions.find((pos) => pos.bank.name === a) // const bMaxAmount = bWalletBalance.maxAmount
const bHasPosition = positions.find((pos) => pos.bank.name === b) const aApy = a.financialMetrics.APY
const aPositionValue = aHasPosition const bApy = b.financialMetrics.APY
? aHasPosition.stakeBalance * aHasPosition.bank.uiPrice
: 0 // if (bMaxAmount !== aMaxAmount) {
const bPositionValue = bHasPosition // return bMaxAmount - aMaxAmount
? bHasPosition.stakeBalance * bHasPosition.bank.uiPrice // } else {
: 0 // return bApy - aApy
return bPositionValue - aPositionValue // }
} return bApy - aApy
}) })
} else if (positions?.length) { }, [stakeableTokens])
const positionTokens = positions.map((position) => position.bank.name)
return positionTokens
} else return []
}, [activeFormTab, positions, walletTokens])
const swapUrl = `https://app.mango.markets/swap?in=USDC&out=${selectedToken}&walletSwap=true` const swapUrl = `https://app.mango.markets/swap?in=USDC&out=${selectedToken}&walletSwap=true`
@ -122,9 +95,7 @@ const Stake = () => {
> >
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="h-10 w-10" /> <div className="h-10 w-10" />
<h2> <h2>Select token to Boost!</h2>
Select token to {activeFormTab === 'Add' ? 'Boost!' : 'Unboost'}
</h2>
<IconButton <IconButton
onClick={() => setShowTokenSelect(false)} onClick={() => setShowTokenSelect(false)}
hideBg hideBg
@ -135,20 +106,18 @@ const Stake = () => {
</div> </div>
<div className="mb-2 flex justify-between px-3"> <div className="mb-2 flex justify-between px-3">
<p className="text-sm text-th-fgd-4">Token</p> <p className="text-sm text-th-fgd-4">Token</p>
<p className="text-sm text-th-fgd-4"> <p className="text-sm text-th-fgd-4">Wallet Balance</p>
{activeFormTab === 'Add' ? 'Wallet Balance' : 'Position Size'}
</p>
</div> </div>
<div className="h-full max-h-[500px] overflow-auto"> <div className="h-full max-h-[500px] overflow-auto pb-10">
{selectableTokens.map((token) => ( {selectableTokens.map((token) => (
<TokenSelect <TokenSelect
key={token} key={token.token.symbol}
onClick={() => handleTokenSelect(token)} onClick={() => handleTokenSelect(token.token.symbol)}
tokenName={token} tokenInfo={token}
clientContext={ clientContext={
getStakableTokensDataForTokenName(token).clientContext getStakableTokensDataForTokenName(token.token.symbol)
.clientContext
} }
showPositionSize={activeFormTab === 'Remove'}
/> />
))} ))}
</div> </div>
@ -156,114 +125,125 @@ const Stake = () => {
<div <div
className={`rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 text-th-fgd-1`} className={`rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 text-th-fgd-1`}
> >
<div className={`p-6 pt-4 md:p-8 md:pt-6`}> {selectableTokens.length ? (
<div className="pb-2"> !selectedToken ? (
<TabUnderline <>
activeValue={activeFormTab} <div className="flex flex-col items-center ">
values={['Add', 'Remove']} <div className="w-full border-b border-th-bkg-3 p-6 text-center md:p-8">
onChange={(v) => handleTabChange(v)} <h1 className="mb-1">Let&apos;s Boost!</h1>
/> <p>Leverage up your liquid staking yield.</p>
</div> </div>
{selectableTokens.length ? ( <div className="p-6 md:p-8">
!selectedToken ? ( <h2 className="mb-3 text-lg font-normal">
<> What do you want to earn?
<div className="mb-6 flex flex-col items-center"> </h2>
<h2 className="mb-1 text-lg font-normal">Earn yield in</h2> <div className="grid grid-cols-2 gap-4 text-lg font-bold">
<div className="w-full"> <button
<ButtonGroup className={`${HERO_TOKEN_BUTTON_CLASSES} ${
activeValue={tokensToShow} tokensToShow === 'SOL' ? 'bg-th-bkg-2' : ''
onChange={(p) => setTokensToShow(p)} }`}
values={['All', 'SOL', 'USDC']} onClick={() => setTokensToShow('SOL')}
/> >
<div className="flex flex-col items-center">
<div className={HERO_TOKEN_IMAGE_WRAPPER_CLASSES}>
<Image
src={`/icons/sol.svg`}
width={32}
height={32}
alt="Select a token"
/>
</div>
<span>SOL</span>
</div>
</button>
<button
className={`${HERO_TOKEN_BUTTON_CLASSES} ${
tokensToShow === 'USDC' ? 'bg-th-bkg-2' : ''
}`}
onClick={() => setTokensToShow('USDC')}
>
<div className="flex flex-col items-center">
<div className={HERO_TOKEN_IMAGE_WRAPPER_CLASSES}>
<Image
src={`/icons/usdc.svg`}
width={32}
height={32}
alt="Select a token"
/>
</div>
<span>USDC</span>
</div>
</button>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> </div>
{tokensToShow ? (
<div className="space-y-3 border-t border-th-bkg-3 p-6 md:p-8">
<h2 className="text-center text-lg font-normal">
Select a token to Boost!
</h2>
{selectableTokens {selectableTokens
.filter((t) => { .filter((t) => {
if (tokensToShow === 'SOL') { if (tokensToShow === 'SOL') {
return SOL_YIELD.includes(t) return SOL_YIELD.includes(t.token.symbol)
} else if (tokensToShow === 'USDC') { } else if (tokensToShow === 'USDC') {
return USDC_YIELD.includes(t) return USDC_YIELD.includes(t.token.symbol)
} else return t } else return
}) })
.map((token) => ( .map((token) => {
<HeroTokenButton const { symbol } = token.token
key={token} return (
onClick={() => <HeroTokenButton
set((state) => { key={symbol}
state.selectedToken = token onClick={() =>
}) set((state) => {
} state.selectedToken = symbol
tokenName={token} })
/> }
))} tokenInfo={token}
/>
)
})}
</div> </div>
</> ) : null}
) : ( </>
<>
<div className="pb-6">
<Label text="Token" />
<TokenButton
onClick={() => setShowTokenSelect(true)}
tokenName={selectedToken}
/>
</div>
{selectedToken == 'USDC' ? (
<>
{activeFormTab === 'Add' ? (
<DespositForm
token="USDC"
clientContext={
getStakableTokensDataForTokenName('USDC')
.clientContext
}
/>
) : null}
{activeFormTab === 'Remove' ? (
<UnstakeForm
token="USDC"
clientContext={
getStakableTokensDataForTokenName('USDC')
.clientContext
}
/>
) : null}
</>
) : (
<>
{activeFormTab === 'Add' ? (
<StakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)
?.clientContext
}
/>
) : null}
{activeFormTab === 'Remove' ? (
<UnstakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)
?.clientContext
}
/>
) : null}
</>
)}
</>
)
) : ( ) : (
<div className="p-10"> <div className="p-6 md:p-8">
<p className="text-center text-th-fgd-4"> <div className="pb-6">
No positions to remove <Label text="Token to Boost!" />
</p> <TokenButton
onClick={() => setShowTokenSelect(true)}
tokenName={selectedToken}
/>
</div>
{selectedToken === 'USDC' ? (
<DespositForm
token="USDC"
clientContext={
getStakableTokensDataForTokenName('USDC').clientContext
}
/>
) : (
<StakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)
?.clientContext
}
/>
)}
</div> </div>
)} )
</div> ) : (
<div className="p-10">
<p className="text-center text-th-fgd-4">
No positions to remove
</p>
</div>
)}
</div> </div>
</div> </div>
{activeFormTab === 'Add' && selectedToken ? ( {selectedToken ? (
<div className="fixed bottom-0 left-0 z-20 w-full lg:bottom-8 lg:left-8 lg:w-auto"> <div className="fixed bottom-0 left-0 z-20 w-full lg:bottom-8 lg:left-8 lg:w-auto">
{isDesktop ? ( {isDesktop ? (
<a <a

View File

@ -1,10 +1,11 @@
import Image from 'next/image' import Image from 'next/image'
import { formatTokenSymbol } from 'utils/tokens' import { formatTokenSymbol } from 'utils/tokens'
import useBankRates from 'hooks/useBankRates'
import useLeverageMax from 'hooks/useLeverageMax'
import mangoStore from '@store/mangoStore' import mangoStore from '@store/mangoStore'
import SheenLoader from './shared/SheenLoader' import SheenLoader from './shared/SheenLoader'
import { ChevronDownIcon } from '@heroicons/react/20/solid' import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { SOL_YIELD } from './Stake'
import useStakeableTokens, { StakeableToken } from 'hooks/useStakeableTokens'
import { useMemo } from 'react'
const TokenButton = ({ const TokenButton = ({
onClick, onClick,
@ -13,25 +14,15 @@ const TokenButton = ({
tokenName: string tokenName: string
onClick: () => void onClick: () => void
}) => { }) => {
const leverage = useLeverageMax(tokenName)
const groupLoaded = mangoStore((s) => s.groupLoaded) const groupLoaded = mangoStore((s) => s.groupLoaded)
const { stakeableTokens } = useStakeableTokens()
const { stakeBankDepositRate, financialMetrics } = useBankRates( const tokenInfo: StakeableToken | undefined = useMemo(() => {
tokenName, if (!tokenName || !stakeableTokens?.length) return
leverage, return stakeableTokens.find((token) => token.token.symbol === tokenName)
) }, [tokenName, stakeableTokens])
const { financialMetrics: estimatedNetAPYFor1xLev } = useBankRates( const apy = tokenInfo?.financialMetrics?.APY
tokenName,
1,
)
const APY_Daily_Compound =
Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1
const UiRate =
tokenName === 'USDC'
? APY_Daily_Compound * 100
: Math.max(estimatedNetAPYFor1xLev.APY, financialMetrics.APY)
return ( return (
<button <button
@ -59,21 +50,58 @@ const TokenButton = ({
<SheenLoader> <SheenLoader>
<div className={`h-5 w-10 bg-th-bkg-2`} /> <div className={`h-5 w-10 bg-th-bkg-2`} />
</SheenLoader> </SheenLoader>
) : !UiRate || isNaN(UiRate) ? ( ) : !apy || isNaN(apy) ? (
<span className="text-base font-normal text-th-fgd-4"> <span className="text-base font-normal text-th-fgd-4">
Rate Unavailable Rate Unavailable
</span> </span>
) : tokenName === 'USDC' ? ( ) : tokenName === 'USDC' ? (
<> <>
{`${UiRate.toFixed(2)}%`}{' '} {`${apy.toFixed(2)}%`}
<span className="text-sm font-normal text-th-fgd-4">APY</span> <div className="mt-1 flex items-center">
<Image
className="mr-1"
src={`/icons/usdc.svg`}
width={14}
height={14}
alt="USDC Logo"
/>
<span className="text-sm font-normal text-th-fgd-4">
Earn USDC
</span>
</div>
</> </>
) : ( ) : (
<> <>
{`${UiRate.toFixed(2)}%`}{' '} {`${apy.toFixed(2)}%`}
<span className="text-sm font-normal text-th-fgd-4"> <div className="mt-1">
Max APY {SOL_YIELD.includes(tokenName) ? (
</span> <div className="flex items-center">
<Image
className="mr-1"
src={`/icons/sol.svg`}
width={14}
height={14}
alt="SOL Logo"
/>
<span className="text-sm font-normal text-th-fgd-4">
Earn SOL
</span>
</div>
) : (
<div className="flex items-center">
<Image
className="mr-1"
src={`/icons/usdc.svg`}
width={14}
height={14}
alt="USDC Logo"
/>
<span className="text-sm font-normal text-th-fgd-4">
Earn USDC
</span>
</div>
)}
</div>
</> </>
)} )}
</span> </span>

View File

@ -1,7 +1,5 @@
import Image from 'next/image' import Image from 'next/image'
import { formatTokenSymbol } from 'utils/tokens' import { formatTokenSymbol } from 'utils/tokens'
import useBankRates from 'hooks/useBankRates'
import useLeverageMax from 'hooks/useLeverageMax'
import mangoStore from '@store/mangoStore' import mangoStore from '@store/mangoStore'
import SheenLoader from './shared/SheenLoader' import SheenLoader from './shared/SheenLoader'
import { useMemo } from 'react' import { useMemo } from 'react'
@ -9,48 +7,34 @@ import FormatNumericValue from './shared/FormatNumericValue'
import { walletBalanceForToken } from './StakeForm' import { walletBalanceForToken } from './StakeForm'
import usePositions from 'hooks/usePositions' import usePositions from 'hooks/usePositions'
import { ClientContextKeys } from 'utils/constants' import { ClientContextKeys } from 'utils/constants'
import { StakeableToken } from 'hooks/useStakeableTokens'
import { SOL_YIELD } from './Stake'
const TokenSelect = ({ const TokenSelect = ({
onClick, onClick,
tokenName, tokenInfo,
clientContext, clientContext,
showPositionSize, showPositionSize,
}: { }: {
tokenName: string tokenInfo: StakeableToken
onClick: () => void onClick: () => void
clientContext: ClientContextKeys clientContext: ClientContextKeys
showPositionSize?: boolean showPositionSize?: boolean
}) => { }) => {
const leverage = useLeverageMax(tokenName) const { symbol } = tokenInfo.token
const { APY } = tokenInfo.financialMetrics
const groupLoaded = mangoStore((s) => s.groupLoaded) const groupLoaded = mangoStore((s) => s.groupLoaded)
const walletTokens = mangoStore((s) => s.wallet.tokens) const walletTokens = mangoStore((s) => s.wallet.tokens)
const { positions } = usePositions() const { positions } = usePositions()
const { stakeBankDepositRate, financialMetrics } = useBankRates(
tokenName,
leverage,
)
const { financialMetrics: estimatedNetAPYFor1xLev } = useBankRates(
tokenName,
1,
)
const walletBalance = useMemo(() => { const walletBalance = useMemo(() => {
return walletBalanceForToken(walletTokens, tokenName, clientContext) return walletBalanceForToken(walletTokens, symbol, clientContext)
}, [walletTokens, tokenName, clientContext]) }, [walletTokens, symbol, clientContext])
const position = useMemo(() => { const position = useMemo(() => {
if (!positions || !positions?.length) return if (!positions || !positions?.length) return
return positions.find((position) => position.bank.name === tokenName) return positions.find((position) => position.bank.name === symbol)
}, [positions, tokenName]) }, [positions, symbol])
const APY_Daily_Compound =
Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1
const UiRate =
tokenName === 'USDC'
? APY_Daily_Compound * 100
: Math.max(estimatedNetAPYFor1xLev.APY, financialMetrics.APY)
return ( return (
<button <button
@ -63,7 +47,7 @@ const TokenSelect = ({
className={`inner-shadow-bottom-sm flex h-12 w-12 shrink-0 items-center justify-center rounded-full border border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2`} className={`inner-shadow-bottom-sm flex h-12 w-12 shrink-0 items-center justify-center rounded-full border border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2`}
> >
<Image <Image
src={`/icons/${tokenName.toLowerCase()}.svg`} src={`/icons/${symbol.toLowerCase()}.svg`}
width={24} width={24}
height={24} height={24}
alt="Select a token" alt="Select a token"
@ -71,7 +55,7 @@ const TokenSelect = ({
</div> </div>
<div className="text-left"> <div className="text-left">
<p className={`text-sm text-th-fgd-1 lg:text-base`}> <p className={`text-sm text-th-fgd-1 lg:text-base`}>
{formatTokenSymbol(tokenName)} {formatTokenSymbol(symbol)}
</p> </p>
<span <span
className={`text-sm font-bold leading-none text-th-fgd-1 sm:text-lg`} className={`text-sm font-bold leading-none text-th-fgd-1 sm:text-lg`}
@ -80,19 +64,56 @@ const TokenSelect = ({
<SheenLoader> <SheenLoader>
<div className={`h-5 w-10 bg-th-bkg-2`} /> <div className={`h-5 w-10 bg-th-bkg-2`} />
</SheenLoader> </SheenLoader>
) : !UiRate || isNaN(UiRate) ? ( ) : !APY || isNaN(APY) ? (
'Rate Unavailable' 'Rate Unavailable'
) : tokenName === 'USDC' ? ( ) : symbol === 'USDC' ? (
<> <>
{`${UiRate.toFixed(2)}%`}{' '} {`${APY.toFixed(2)}%`}
<span className="text-sm font-normal text-th-fgd-4">APY</span> <div className="mt-1 flex items-center">
<Image
className="mr-1"
src={`/icons/usdc.svg`}
width={14}
height={14}
alt="USDC Logo"
/>
<span className="text-sm font-normal text-th-fgd-4">
Earn USDC
</span>
</div>
</> </>
) : ( ) : (
<> <>
{`${UiRate.toFixed(2)}%`}{' '} {`${APY.toFixed(2)}%`}
<span className="text-sm font-normal text-th-fgd-4"> <div className="mt-1">
Max APY {SOL_YIELD.includes(symbol) ? (
</span> <div className="flex items-center">
<Image
className="mr-1"
src={`/icons/sol.svg`}
width={14}
height={14}
alt="SOL Logo"
/>
<span className="text-sm font-normal text-th-fgd-4">
Earn SOL
</span>
</div>
) : (
<div className="flex items-center">
<Image
className="mr-1"
src={`/icons/usdc.svg`}
width={14}
height={14}
alt="USDC Logo"
/>
<span className="text-sm font-normal text-th-fgd-4">
Earn USDC
</span>
</div>
)}
</div>
</> </>
)} )}
</span> </span>

View File

@ -482,7 +482,7 @@ function UnstakeForm({
})} })}
</div> </div>
) : ( ) : (
`Unboost ${inputAmount} ${formatTokenSymbol(selectedToken)}` `Withdraw ${inputAmount} ${formatTokenSymbol(selectedToken)}`
)} )}
</Button> </Button>
) : ( ) : (

View File

@ -54,6 +54,7 @@ interface IconButtonProps {
hideBg?: boolean hideBg?: boolean
size?: 'small' | 'medium' | 'large' size?: 'small' | 'medium' | 'large'
ref?: Ref<HTMLButtonElement> ref?: Ref<HTMLButtonElement>
isPrimary?: boolean
} }
type IconButtonCombinedProps = AllButtonProps & IconButtonProps type IconButtonCombinedProps = AllButtonProps & IconButtonProps
@ -62,12 +63,20 @@ export const IconButton = forwardRef<
HTMLButtonElement, HTMLButtonElement,
IconButtonCombinedProps IconButtonCombinedProps
>((props, ref) => { >((props, ref) => {
const { children, onClick, disabled = false, className, hideBg, size } = props const {
children,
onClick,
disabled = false,
className,
hideBg,
size,
isPrimary,
} = props
return ( return (
<button <button
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className={`flex flex-shrink-0 ${ className={`flex shrink-0 ${
size === 'large' size === 'large'
? 'h-12 w-12' ? 'h-12 w-12'
: size === 'small' : size === 'small'
@ -78,7 +87,9 @@ export const IconButton = forwardRef<
} items-center justify-center rounded-full ${ } items-center justify-center rounded-full ${
hideBg hideBg
? 'md:hover:text-th-active' ? 'md:hover:text-th-active'
: 'raised-button-neutral group after:rounded-full' : `group after:rounded-full ${
isPrimary ? 'raised-button' : 'raised-button-neutral'
}`
} text-th-fgd-1 focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 } text-th-fgd-1 focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4
disabled:text-th-fgd-4 md:disabled:hover:text-th-fgd-4 ${className} focus-visible:text-th-active`} disabled:text-th-fgd-4 md:disabled:hover:text-th-fgd-4 ${className} focus-visible:text-th-active`}
ref={ref} ref={ref}

View File

@ -2,33 +2,32 @@ import { useQuery } from '@tanstack/react-query'
import { OHLCVPairItem, fetchOHLCPair } from 'apis/birdeye/helpers' import { OHLCVPairItem, fetchOHLCPair } from 'apis/birdeye/helpers'
import { SOL_MINT, STAKEABLE_TOKENS_DATA, USDC_MINT } from 'utils/constants' import { SOL_MINT, STAKEABLE_TOKENS_DATA, USDC_MINT } from 'utils/constants'
const avgOpenClose = (i: OHLCVPairItem) => (i.c + i.o) * .5; const avgOpenClose = (i: OHLCVPairItem) => (i.c + i.o) * 0.5
const sum = (x: number, y: number) => x + y; const sum = (x: number, y: number) => x + y
const ANNUAL_SECONDS = 60 * 60 * 24 * 365; const ANNUAL_SECONDS = 60 * 60 * 24 * 365
const calculateRate = (ohlcvs: OHLCVPairItem[]) => { const calculateRate = (ohlcvs: OHLCVPairItem[]) => {
if (ohlcvs && ohlcvs?.length > 30) { if (ohlcvs && ohlcvs?.length > 30) {
// basic least squares regression: // basic least squares regression:
// https://www.ncl.ac.uk/webtemplate/ask-assets/external/maths-resources/statistics/regression-and-correlation/simple-linear-regression.html // https://www.ncl.ac.uk/webtemplate/ask-assets/external/maths-resources/statistics/regression-and-correlation/simple-linear-regression.html
const xs = ohlcvs.map(o => o.unixTime); const xs = ohlcvs.map((o) => o.unixTime)
const ys = ohlcvs.map(avgOpenClose); const ys = ohlcvs.map(avgOpenClose)
const x_sum = xs.reduce(sum, 0); const x_sum = xs.reduce(sum, 0)
const y_sum = ys.reduce(sum, 0); const y_sum = ys.reduce(sum, 0)
const x_mean = x_sum / xs.length; const x_mean = x_sum / xs.length
const y_mean = y_sum / ys.length; const y_mean = y_sum / ys.length
const S_xy = xs.map((xi, i) => (xi - x_mean) * (ys[i] - y_mean)).reduce(sum, 0); const S_xy = xs
const S_xx = xs.map((xi) => (xi - x_mean) ** 2).reduce(sum, 0); .map((xi, i) => (xi - x_mean) * (ys[i] - y_mean))
const b = S_xy / S_xx; .reduce(sum, 0)
const a = y_mean - b * x_mean; const S_xx = xs.map((xi) => (xi - x_mean) ** 2).reduce(sum, 0)
const b = S_xy / S_xx
const a = y_mean - b * x_mean
const start = a + b * xs[0]; const start = a + b * xs[0]
const end = a + b * (xs[0] + ANNUAL_SECONDS); const end = a + b * (xs[0] + ANNUAL_SECONDS)
return { rate: (end - start)/start, start, end, a, b, S_xx, S_xy}; return { rate: (end - start) / start, start, end, a, b, S_xx, S_xy }
} else { } else {
return { rate: 0.082 }; // fixed rate to avoid outliers return { rate: 0.082 } // fixed rate to avoid outliers
} }
} }
@ -37,10 +36,10 @@ const fetchRates = async () => {
const promises = STAKEABLE_TOKENS_DATA.filter( const promises = STAKEABLE_TOKENS_DATA.filter(
(token) => token.mint_address !== USDC_MINT, (token) => token.mint_address !== USDC_MINT,
).map(async (t) => { ).map(async (t) => {
const isUsdcBorrow = t.name === 'JLP' || t.name === 'USDC' const isUsdcBorrow = t.symbol === 'JLP' || t.symbol === 'USDC'
const quoteMint = isUsdcBorrow ? USDC_MINT : SOL_MINT const quoteMint = isUsdcBorrow ? USDC_MINT : SOL_MINT
const dailyCandles = await fetchOHLCPair(t.mint_address, quoteMint, '90'); const dailyCandles = await fetchOHLCPair(t.mint_address, quoteMint, '90')
return dailyCandles; return dailyCandles
}) })
const [ const [
jlpPrices, jlpPrices,

169
hooks/useStakeableTokens.ts Normal file
View File

@ -0,0 +1,169 @@
import {
JLP_BORROW_TOKEN,
LST_BORROW_TOKEN,
STAKEABLE_TOKENS_DATA,
StakeableTokensData,
} from 'utils/constants'
import useStakeRates from './useStakeRates'
import useMangoGroup from './useMangoGroup'
import { Bank } from '@blockworks-foundation/mango-v4'
import { floorToDecimal } from 'utils/numbers'
import { getStakableTokensDataForTokenName } from 'utils/tokens'
type FinancialMetrics = {
APY: number
depositAPY: number
collectedReturnsAPY: number
collateralFeeAPY: number
borrowsAPY: number
nonMangoAPY: number
diffToNonMango: number
diffToNonLeveraged: number
}
export type StakeableToken = {
token: StakeableTokensData
financialMetrics: FinancialMetrics
}
const getLeverage = (
stakeBank: Bank | undefined,
borrowBank: Bank | undefined,
tokenSymbol: string,
) => {
if (!stakeBank || !borrowBank) return 0
const borrowInitLiabWeight = borrowBank.scaledInitLiabWeight(borrowBank.price)
const stakeInitAssetWeight = stakeBank.scaledInitAssetWeight(stakeBank.price)
if (!borrowInitLiabWeight || !stakeInitAssetWeight) return 1
const x = stakeInitAssetWeight.toNumber() / borrowInitLiabWeight.toNumber()
if (getStakableTokensDataForTokenName(tokenSymbol).clientContext === 'jlp') {
const leverageFactor = 1 / (1 - x)
const max = floorToDecimal(leverageFactor, 1).toNumber()
return max * 0.9 // Multiplied by 0.975 because you cant actually get to the end of the infinite geometric series?
} else {
const leverageFactor = 1 / (1 - x)
const max = floorToDecimal(leverageFactor, 2).toNumber()
return max * 0.9 // Multiplied by 0.975 because you cant actually get to the end of the infinite geometric series?
}
}
const getFinancialMetrics = (
stakeBank: Bank | undefined,
borrowBank: Bank | undefined,
leverage: number,
tokenStakeRateAPY: number,
) => {
const borrowBankBorrowRate = borrowBank
? Number(borrowBank.getBorrowRate())
: 0
// Collateral fee is charged on the assets needed to back borrows and
// 1 deposited JLP can back maintAssetWeight * (1 JLP-value) USDC borrows.
const collateralFeePerBorrowPerDay =
Number(stakeBank?.collateralFeePerDay) / Number(stakeBank?.maintAssetWeight)
// Convert the borrow APR to a daily rate
const borrowRatePerDay = Number(borrowBankBorrowRate) / 365
// Convert the JLP APY to a daily rate
const tokenRatePerDay = (1 + tokenStakeRateAPY) ** (1 / 365) - 1
// Assume the user deposits 1 JLP, then these are the starting deposits and
// borrows for the desired leverage (in terms of starting-value JLP)
const initialBorrows = leverage - 1
const initialDeposits = leverage
// In the following, we'll simulate time passing and how the deposits and
// borrows evolve.
// Note that these will be in terms of starting-value JLP, meaning that JLP
// price increases will be modelled as deposits increasing in amount.
let borrows = initialBorrows
let deposits = initialDeposits
let collectedCollateralFees = 0
let collectedReturns = 0
for (let day = 1; day <= 365; day++) {
borrows *= 1 + borrowRatePerDay
const collateralFees = collateralFeePerBorrowPerDay * borrows
deposits -= collateralFees
collectedCollateralFees += collateralFees
const tokenReturns = tokenRatePerDay * deposits
deposits += tokenReturns
collectedReturns += tokenReturns
}
// APY's for the calculation
const depositAPY = (deposits - initialDeposits) * 100
const collateralFeeAPY = collectedCollateralFees * 100
const collectedReturnsAPY = collectedReturns * 100
// Interest Fee APY: Reflecting borrowing cost as an annual percentage yield
const borrowsAPY = (borrows - initialBorrows) * 100
const stakeBankDepositRate = stakeBank ? stakeBank.getDepositRate() : 0
// Total APY, comparing the end value (deposits - borrows) to the starting value (1)
const APY = (deposits - borrows - 1) * 100
const APY_Daily_Compound =
Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1
const UiRate = stakeBank
? stakeBank.name === 'USDC'
? APY_Daily_Compound * 100
: APY
: 0
// Comparisons to outside
const nonMangoAPY = tokenStakeRateAPY * leverage * 100
const diffToNonMango = APY - nonMangoAPY
const diffToNonLeveraged = APY - tokenStakeRateAPY * 100
return {
APY: UiRate,
depositAPY,
collectedReturnsAPY,
collateralFeeAPY,
borrowsAPY,
nonMangoAPY,
diffToNonMango,
diffToNonLeveraged,
}
}
export default function useStakeableTokens() {
const { data: stakeRates } = useStakeRates()
const { jlpGroup, lstGroup } = useMangoGroup()
const stakeableTokens: StakeableToken[] = []
for (const token of STAKEABLE_TOKENS_DATA) {
const { symbol } = token
const isJlpGroup = symbol === 'JLP' || symbol === 'USDC'
const stakeBank = isJlpGroup
? jlpGroup?.banksMapByName.get(symbol)?.[0]
: lstGroup?.banksMapByName.get(symbol)?.[0]
const borrowBank = isJlpGroup
? jlpGroup?.banksMapByName.get(JLP_BORROW_TOKEN)?.[0]
: lstGroup?.banksMapByName.get(LST_BORROW_TOKEN)?.[0]
// const stakeBankDepositRate = stakeBank ? stakeBank.getDepositRate() : 0
// const borrowBankBorrowRate = borrowBank ? Number(borrowBank.getBorrowRate()) : 0
const tokenStakeRateAPY = stakeRates ? stakeRates[symbol.toLowerCase()] : 0
const leverage = getLeverage(stakeBank, borrowBank, symbol)
const financialMetrics = getFinancialMetrics(
stakeBank,
borrowBank,
leverage,
tokenStakeRateAPY,
)
stakeableTokens.push({ token, financialMetrics })
}
return { stakeableTokens }
}

View File

@ -4,109 +4,189 @@ import { PublicKey } from '@solana/web3.js'
export const JLP_BORROW_TOKEN = 'USDC' export const JLP_BORROW_TOKEN = 'USDC'
export const LST_BORROW_TOKEN = 'SOL' export const LST_BORROW_TOKEN = 'SOL'
export const STAKEABLE_TOKENS_DATA: { export type StakeableTokensData = {
name: string name: string
symbol: string
description: string
id: number id: number
active: boolean active: boolean
mint_address: string mint_address: string
clientContext: ClientContextKeys clientContext: ClientContextKeys
borrowToken: 'USDC' | 'SOL' borrowToken: 'USDC' | 'SOL'
}[] = [ links: {
website: string | undefined
twitter: string | undefined
}
}
export const STAKEABLE_TOKENS_DATA: StakeableTokensData[] = [
{ {
name: 'JLP', name: 'Jupiter Perps LP',
symbol: 'JLP',
description: '',
id: 1, id: 1,
active: true, active: true,
mint_address: '27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4', mint_address: '27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4',
clientContext: 'jlp', clientContext: 'jlp',
borrowToken: 'USDC', borrowToken: 'USDC',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'USDC', name: 'USD Coin',
symbol: 'USDC',
description: '',
id: 0, id: 0,
active: true, active: true,
mint_address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', mint_address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
clientContext: 'jlp', clientContext: 'jlp',
borrowToken: 'USDC', borrowToken: 'USDC',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'MSOL', name: 'Marinade Staked SOL',
symbol: 'MSOL',
description: '',
id: 521, id: 521,
active: true, active: true,
mint_address: 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So', mint_address: 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'JitoSOL', name: 'Jito Staked SOL',
symbol: 'JitoSOL',
description: '',
id: 621, id: 621,
active: true, active: true,
mint_address: 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn', mint_address: 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'bSOL', name: 'BlazeStake Staked SOL',
symbol: 'bSOL',
description: '',
id: 721, id: 721,
active: true, active: true,
mint_address: 'bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1', mint_address: 'bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'JSOL', name: 'JPool Staked SOL',
symbol: 'JSOL',
description: '',
id: 1063, id: 1063,
active: true, active: true,
mint_address: '7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn', mint_address: '7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'INF', name: 'Sanctum Infinity',
symbol: 'INF',
description: '',
id: 1105, id: 1105,
active: true, active: true,
mint_address: '5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm', mint_address: '5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'hubSOL', name: 'SolanaHub Staked SOL',
symbol: 'hubSOL',
description: '',
id: 1153, id: 1153,
active: true, active: true,
mint_address: 'HUBsveNpjo5pWqNkH57QzxjQASdTVXcSK7bVKTSZtcSX', mint_address: 'HUBsveNpjo5pWqNkH57QzxjQASdTVXcSK7bVKTSZtcSX',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'digitSOL', name: 'Simpdigit Staked SOL',
symbol: 'digitSOL',
description: '',
id: 1161, id: 1161,
active: true, active: true,
mint_address: 'D1gittVxgtszzY4fMwiTfM4Hp7uL5Tdi1S9LYaepAUUm', mint_address: 'D1gittVxgtszzY4fMwiTfM4Hp7uL5Tdi1S9LYaepAUUm',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'dualSOL', name: 'Dual Finance Staked SOL',
symbol: 'dualSOL',
description: '',
id: 1158, id: 1158,
active: true, active: true,
mint_address: 'DUAL6T9pATmQUFPYmrWq2BkkGdRxLtERySGScYmbHMER', mint_address: 'DUAL6T9pATmQUFPYmrWq2BkkGdRxLtERySGScYmbHMER',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'mangoSOL', name: 'Mango Staked SOL',
symbol: 'mangoSOL',
description: '',
id: 1162, id: 1162,
active: true, active: true,
mint_address: 'MangmsBgFqJhW4cLUR9LxfVgMboY1xAoP8UUBiWwwuY', mint_address: 'MangmsBgFqJhW4cLUR9LxfVgMboY1xAoP8UUBiWwwuY',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
{ {
name: 'compassSOL', name: 'Solana Compass Staked SOL',
symbol: 'compassSOL',
description: '',
id: 1163, id: 1163,
active: true, active: true,
mint_address: 'Comp4ssDzXcLeu2MnLuGNNFC4cmLPMng8qWHPvzAMU1h', mint_address: 'Comp4ssDzXcLeu2MnLuGNNFC4cmLPMng8qWHPvzAMU1h',
clientContext: 'lst', clientContext: 'lst',
borrowToken: 'SOL', borrowToken: 'SOL',
links: {
website: '',
twitter: '',
},
}, },
] ]
@ -114,7 +194,7 @@ export type ClientContextKeys = 'lst' | 'jlp'
export const STAKEABLE_TOKENS = STAKEABLE_TOKENS_DATA.filter( export const STAKEABLE_TOKENS = STAKEABLE_TOKENS_DATA.filter(
(d) => d.active, (d) => d.active,
).map((d) => d.name) ).map((d) => d.symbol)
export const SHOW_INACTIVE_POSITIONS_KEY = 'showInactivePositions-0.1' export const SHOW_INACTIVE_POSITIONS_KEY = 'showInactivePositions-0.1'
// end // end

View File

@ -80,6 +80,6 @@ export const getStakableTokensDataForMint = (mintPk: string) => {
return STAKEABLE_TOKENS_DATA.find((x) => x.mint_address === mintPk)! return STAKEABLE_TOKENS_DATA.find((x) => x.mint_address === mintPk)!
} }
export const getStakableTokensDataForTokenName = (tokenName: string) => { export const getStakableTokensDataForTokenName = (tokenSymbol: string) => {
return STAKEABLE_TOKENS_DATA.find((x) => x.name === tokenName)! return STAKEABLE_TOKENS_DATA.find((x) => x.symbol === tokenSymbol)!
} }