Merge pull request #27 from blockworks-foundation/boost-token-ux
boost token ux
This commit is contained in:
commit
dc2c2fb833
|
@ -1,40 +1,32 @@
|
|||
import Image from 'next/image'
|
||||
import { formatTokenSymbol } from 'utils/tokens'
|
||||
import useBankRates from 'hooks/useBankRates'
|
||||
import useLeverageMax from 'hooks/useLeverageMax'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import SheenLoader from './shared/SheenLoader'
|
||||
import { SOL_YIELD } from './Stake'
|
||||
import Tooltip from './shared/Tooltip'
|
||||
import Link from 'next/link'
|
||||
import { StakeableToken } from 'hooks/useStakeableTokens'
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
|
||||
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 = ({
|
||||
onClick,
|
||||
tokenName,
|
||||
tokenInfo,
|
||||
}: {
|
||||
tokenName: string
|
||||
tokenInfo: StakeableToken
|
||||
onClick: () => void
|
||||
}) => {
|
||||
const leverage = useLeverageMax(tokenName)
|
||||
const { symbol, name } = tokenInfo.token ?? {}
|
||||
const { estNetApy } = tokenInfo ?? {}
|
||||
const groupLoaded = mangoStore((s) => s.groupLoaded)
|
||||
|
||||
const { stakeBankDepositRate, financialMetrics } = useBankRates(
|
||||
tokenName,
|
||||
leverage,
|
||||
)
|
||||
|
||||
const { financialMetrics: estimatedNetAPYFor1xLev } = useBankRates(
|
||||
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)
|
||||
|
||||
const renderRateEmoji = (token: string, rate: number) => {
|
||||
if (token.toLowerCase().includes('sol')) {
|
||||
if (rate >= 20) {
|
||||
|
@ -53,97 +45,105 @@ const HeroTokenButton = ({
|
|||
}
|
||||
}
|
||||
|
||||
const emoji = renderRateEmoji(tokenName, UiRate)
|
||||
const emoji = renderRateEmoji(symbol, estNetApy)
|
||||
|
||||
return (
|
||||
<button
|
||||
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}
|
||||
>
|
||||
<button className={HERO_TOKEN_BUTTON_CLASSES} onClick={onClick}>
|
||||
<div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
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`}
|
||||
>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className={HERO_TOKEN_IMAGE_WRAPPER_CLASSES}>
|
||||
<Image
|
||||
src={`/icons/${tokenName.toLowerCase()}.svg`}
|
||||
src={`/icons/${symbol.toLowerCase()}.svg`}
|
||||
width={32}
|
||||
height={32}
|
||||
alt="Select a token"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<p className={`text-th-fgd-1`}>{formatTokenSymbol(tokenName)}</p>
|
||||
<span className={`text-2xl font-bold`}>
|
||||
{!groupLoaded ? (
|
||||
<SheenLoader>
|
||||
<div className={`h-6 w-10 bg-th-bkg-2`} />
|
||||
</SheenLoader>
|
||||
) : !UiRate || isNaN(UiRate) ? (
|
||||
<span className="text-base font-normal text-th-fgd-4">
|
||||
Rate Unavailable
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="text-left">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1.5 text-xl font-bold">
|
||||
{formatTokenSymbol(symbol)}
|
||||
</span>
|
||||
) : (
|
||||
`${UiRate.toFixed(2)}%`
|
||||
)}
|
||||
</span>
|
||||
{groupLoaded ? (
|
||||
<div className="mt-1 flex items-center">
|
||||
{SOL_YIELD.includes(tokenName) ? (
|
||||
<>
|
||||
<Image
|
||||
className="mr-1.5"
|
||||
src={`/icons/sol.svg`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt="SOL Logo"
|
||||
/>
|
||||
<span className="text-sm text-th-fgd-4">Earn SOL</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Image
|
||||
className="mr-1.5"
|
||||
src={`/icons/usdc.svg`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt="USDC Logo"
|
||||
/>
|
||||
<span className="text-sm text-th-fgd-4">Earn USDC</span>
|
||||
</>
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
{tokenInfo?.token?.description
|
||||
? tokenInfo.token.description
|
||||
: name}
|
||||
</p>
|
||||
<div className="flex">
|
||||
{tokenInfo?.token?.links?.website ? (
|
||||
<a
|
||||
className="mr-2 mt-2 flex items-center"
|
||||
href={tokenInfo.token.links.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="mr-0.5">Website</span>
|
||||
<ArrowTopRightOnSquareIcon className="h-3 w-3" />
|
||||
</a>
|
||||
) : null}
|
||||
{tokenInfo?.token?.links?.twitter ? (
|
||||
<a
|
||||
className="mt-2 flex items-center"
|
||||
href={tokenInfo.token.links.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="mr-0.5">Twitter</span>
|
||||
<ArrowTopRightOnSquareIcon className="h-3 w-3" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InformationCircleIcon className="mb-0.5 h-4 w-4 cursor-help text-th-bkg-4" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
<p className={`text-xs text-th-fgd-4`}>{name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`text-xs text-th-fgd-4`}>Max APY</p>
|
||||
<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>
|
||||
) : !estNetApy || isNaN(estNetApy) ? (
|
||||
<span className="text-base font-normal text-th-fgd-4">
|
||||
Rate Unavailable
|
||||
</span>
|
||||
) : (
|
||||
`${estNetApy.toFixed(2)}%`
|
||||
)}
|
||||
</span>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ const NavTabs = <T extends Values>({
|
|||
<>
|
||||
{values.map(([value, count], i) => (
|
||||
<button
|
||||
className={`mx-auto flex w-full items-center justify-center border-y-2 border-r border-th-fgd-1 py-3.5 font-bold first:rounded-l-lg first:border-l-2 last:rounded-r-lg last:border-r-2 ${
|
||||
className={`mx-auto flex h-14 w-full items-center justify-center border-y-2 border-r border-th-fgd-1 font-bold first:rounded-l-xl first:border-l-2 last:rounded-r-xl last:border-r-2 ${
|
||||
activeValue === value
|
||||
? 'inner-shadow-top-sm bg-th-active text-th-fgd-1'
|
||||
: 'inner-shadow-bottom-sm default-transition bg-th-bkg-1 text-th-fgd-1 md:hover:bg-th-bkg-2'
|
||||
|
|
|
@ -2,8 +2,11 @@ import useMangoGroup from 'hooks/useMangoGroup'
|
|||
import { useMemo, useState } from 'react'
|
||||
import { SHOW_INACTIVE_POSITIONS_KEY } from 'utils/constants'
|
||||
import TokenLogo from './shared/TokenLogo'
|
||||
import Button from './shared/Button'
|
||||
import { formatTokenSymbol } from 'utils/tokens'
|
||||
import Button, { IconButton } from './shared/Button'
|
||||
import {
|
||||
formatTokenSymbol,
|
||||
getStakableTokensDataForTokenName,
|
||||
} from 'utils/tokens'
|
||||
import mangoStore, { ActiveTab } from '@store/mangoStore'
|
||||
import Switch from './forms/Switch'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
|
@ -15,10 +18,16 @@ import {
|
|||
} from '@blockworks-foundation/mango-v4'
|
||||
import useBankRates from 'hooks/useBankRates'
|
||||
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 Tooltip from './shared/Tooltip'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import UnstakeForm from './UnstakeForm'
|
||||
import StakeForm from './StakeForm'
|
||||
import DespositForm from './DepositForm'
|
||||
|
||||
const set = mangoStore.getState().set
|
||||
|
||||
|
@ -35,18 +44,20 @@ const Positions = ({
|
|||
}: {
|
||||
setActiveTab: (tab: ActiveTab) => void
|
||||
}) => {
|
||||
const selectedToken = mangoStore((s) => s.selectedToken)
|
||||
const [showInactivePositions, setShowInactivePositions] =
|
||||
useLocalStorageState(SHOW_INACTIVE_POSITIONS_KEY, true)
|
||||
useLocalStorageState(SHOW_INACTIVE_POSITIONS_KEY, false)
|
||||
const { positions, jlpBorrowBank, lstBorrowBank } = usePositions(
|
||||
showInactivePositions,
|
||||
)
|
||||
const [showAddRemove, setShowAddRemove] = useState('')
|
||||
|
||||
const numberOfPositions = useMemo(() => {
|
||||
if (!positions.length) return 0
|
||||
return positions.filter((pos) => pos.stakeBalance > 0).length
|
||||
}, [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">
|
||||
<p className="font-medium">{`You have ${numberOfPositions} active position${
|
||||
|
@ -69,6 +80,7 @@ const Positions = ({
|
|||
key={bank.name}
|
||||
position={position}
|
||||
setActiveTab={setActiveTab}
|
||||
setShowAddRemove={setShowAddRemove}
|
||||
borrowBank={isUsdcBorrow ? jlpBorrowBank : lstBorrowBank}
|
||||
/>
|
||||
) : null
|
||||
|
@ -80,29 +92,80 @@ const Positions = ({
|
|||
)}
|
||||
</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="small" 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 = ({
|
||||
position,
|
||||
setActiveTab,
|
||||
setShowAddRemove,
|
||||
borrowBank,
|
||||
}: {
|
||||
position: Position
|
||||
setActiveTab: (v: ActiveTab) => void
|
||||
setShowAddRemove: (v: 'add' | 'remove') => void
|
||||
borrowBank: Bank | undefined
|
||||
}) => {
|
||||
const { connected } = useWallet()
|
||||
const { jlpGroup, lstGroup } = useMangoGroup()
|
||||
const { stakeBalance, bank, pnl, acct } = position
|
||||
const [showEditLeverageModal, setShowEditLeverageModal] = useState(false)
|
||||
|
||||
const handleAddOrManagePosition = (token: string) => {
|
||||
const handleAddNoPosition = (token: string) => {
|
||||
setActiveTab('Boost!')
|
||||
set((state) => {
|
||||
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(() => {
|
||||
if (!acct || !bank) return 1
|
||||
|
@ -162,11 +225,24 @@ const PositionItem = ({
|
|||
<p>${bank.uiPrice.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => handleAddOrManagePosition(bank.name)}>
|
||||
<p className="mb-1 text-base tracking-wider text-th-bkg-1">
|
||||
{stakeBalance ? 'Add/Remove' : `Boost! ${bank.name}`}
|
||||
</p>
|
||||
</Button>
|
||||
{stakeBalance ? (
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={() => handleAddPosition(bank.name)}>
|
||||
<p className="mb-1 text-base tracking-wider text-th-bkg-1">Add</p>
|
||||
</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 className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
import TokenButton from './TokenButton'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import TabUnderline from './shared/TabUnderline'
|
||||
import StakeForm, { walletBalanceForToken } from '@components/StakeForm'
|
||||
import UnstakeForm from '@components/UnstakeForm'
|
||||
import StakeForm from '@components/StakeForm'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { STAKEABLE_TOKENS } from 'utils/constants'
|
||||
import {
|
||||
formatTokenSymbol,
|
||||
getStakableTokensDataForTokenName,
|
||||
} from 'utils/tokens'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { ArrowTopRightOnSquareIcon, XMarkIcon } from '@heroicons/react/20/solid'
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import DespositForm from './DepositForm'
|
||||
import { EnterBottomExitBottom } from './shared/Transitions'
|
||||
import TokenSelect from './TokenSelect'
|
||||
import Label from './forms/Label'
|
||||
import usePositions from 'hooks/usePositions'
|
||||
import { IconButton } from './shared/Button'
|
||||
import HeroTokenButton from './HeroTokenButton'
|
||||
import ButtonGroup from './forms/ButtonGroup'
|
||||
import HeroTokenButton, {
|
||||
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
|
||||
|
||||
|
@ -37,13 +39,12 @@ export const SOL_YIELD = [
|
|||
const USDC_YIELD = ['JLP', 'USDC']
|
||||
|
||||
const Stake = () => {
|
||||
const [activeFormTab, setActiveFormTab] = useState('Add')
|
||||
const [tokensToShow, setTokensToShow] = useState('All')
|
||||
const [tokensToShow, setTokensToShow] = useState('')
|
||||
const [showTokenSelect, setShowTokenSelect] = useState(false)
|
||||
const selectedToken = mangoStore((s) => s.selectedToken)
|
||||
const walletTokens = mangoStore((s) => s.wallet.tokens)
|
||||
// const walletTokens = mangoStore((s) => s.wallet.tokens)
|
||||
const { isDesktop } = useViewport()
|
||||
const { positions } = usePositions()
|
||||
const { stakeableTokens } = useStakeableTokens()
|
||||
|
||||
const handleTokenSelect = useCallback((token: string) => {
|
||||
set((state) => {
|
||||
|
@ -52,64 +53,38 @@ const Stake = () => {
|
|||
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(() => {
|
||||
if (activeFormTab === 'Add') {
|
||||
return STAKEABLE_TOKENS.sort((a: string, b: string) => {
|
||||
if (activeFormTab === 'Add') {
|
||||
const aClientContext =
|
||||
getStakableTokensDataForTokenName(a).clientContext
|
||||
const aWalletBalance = walletBalanceForToken(
|
||||
walletTokens,
|
||||
a,
|
||||
aClientContext,
|
||||
)
|
||||
const bClientContext =
|
||||
getStakableTokensDataForTokenName(b).clientContext
|
||||
const bWalletBalance = walletBalanceForToken(
|
||||
walletTokens,
|
||||
b,
|
||||
bClientContext,
|
||||
)
|
||||
return bWalletBalance.maxAmount - aWalletBalance.maxAmount
|
||||
} else {
|
||||
const aHasPosition = positions.find((pos) => pos.bank.name === a)
|
||||
const bHasPosition = positions.find((pos) => pos.bank.name === b)
|
||||
const aPositionValue = aHasPosition
|
||||
? aHasPosition.stakeBalance * aHasPosition.bank.uiPrice
|
||||
: 0
|
||||
const bPositionValue = bHasPosition
|
||||
? bHasPosition.stakeBalance * bHasPosition.bank.uiPrice
|
||||
: 0
|
||||
return bPositionValue - aPositionValue
|
||||
}
|
||||
})
|
||||
} else if (positions?.length) {
|
||||
const positionTokens = positions.map((position) => position.bank.name)
|
||||
return positionTokens
|
||||
} else return []
|
||||
}, [activeFormTab, positions, walletTokens])
|
||||
return stakeableTokens.sort((a: StakeableToken, b: StakeableToken) => {
|
||||
// const aClientContext = getStakableTokensDataForTokenName(
|
||||
// a.token.symbol,
|
||||
// ).clientContext
|
||||
// const aWalletBalance = walletBalanceForToken(
|
||||
// walletTokens,
|
||||
// a.token.symbol,
|
||||
// aClientContext,
|
||||
// )
|
||||
// const bClientContext = getStakableTokensDataForTokenName(
|
||||
// b.token.symbol,
|
||||
// ).clientContext
|
||||
// const bWalletBalance = walletBalanceForToken(
|
||||
// walletTokens,
|
||||
// b.token.symbol,
|
||||
// bClientContext,
|
||||
// )
|
||||
|
||||
// const aMaxAmount = aWalletBalance.maxAmount
|
||||
// const bMaxAmount = bWalletBalance.maxAmount
|
||||
const aApy = a.estNetApy
|
||||
const bApy = b.estNetApy
|
||||
|
||||
// if (bMaxAmount !== aMaxAmount) {
|
||||
// return bMaxAmount - aMaxAmount
|
||||
// } else {
|
||||
// return bApy - aApy
|
||||
// }
|
||||
return bApy - aApy
|
||||
})
|
||||
}, [stakeableTokens])
|
||||
|
||||
const swapUrl = `https://app.mango.markets/swap?in=USDC&out=${selectedToken}&walletSwap=true`
|
||||
|
||||
|
@ -122,9 +97,7 @@ const Stake = () => {
|
|||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="h-10 w-10" />
|
||||
<h2>
|
||||
Select token to {activeFormTab === 'Add' ? 'Boost!' : 'Unboost'}
|
||||
</h2>
|
||||
<h2>Select token to Boost!</h2>
|
||||
<IconButton
|
||||
onClick={() => setShowTokenSelect(false)}
|
||||
hideBg
|
||||
|
@ -135,20 +108,18 @@ const Stake = () => {
|
|||
</div>
|
||||
<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">
|
||||
{activeFormTab === 'Add' ? 'Wallet Balance' : 'Position Size'}
|
||||
</p>
|
||||
<p className="text-sm text-th-fgd-4">Wallet Balance</p>
|
||||
</div>
|
||||
<div className="h-full max-h-[500px] overflow-auto">
|
||||
<div className="h-full max-h-[500px] overflow-auto pb-10">
|
||||
{selectableTokens.map((token) => (
|
||||
<TokenSelect
|
||||
key={token}
|
||||
onClick={() => handleTokenSelect(token)}
|
||||
tokenName={token}
|
||||
key={token.token.symbol}
|
||||
onClick={() => handleTokenSelect(token.token.symbol)}
|
||||
tokenInfo={token}
|
||||
clientContext={
|
||||
getStakableTokensDataForTokenName(token).clientContext
|
||||
getStakableTokensDataForTokenName(token.token.symbol)
|
||||
.clientContext
|
||||
}
|
||||
showPositionSize={activeFormTab === 'Remove'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -156,114 +127,139 @@ const Stake = () => {
|
|||
<div
|
||||
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`}>
|
||||
<div className="pb-2">
|
||||
<TabUnderline
|
||||
activeValue={activeFormTab}
|
||||
values={['Add', 'Remove']}
|
||||
onChange={(v) => handleTabChange(v)}
|
||||
/>
|
||||
</div>
|
||||
{selectableTokens.length ? (
|
||||
!selectedToken ? (
|
||||
<>
|
||||
<div className="mb-6 flex flex-col items-center">
|
||||
<h2 className="mb-1 text-lg font-normal">Earn yield in</h2>
|
||||
<div className="w-full">
|
||||
<ButtonGroup
|
||||
activeValue={tokensToShow}
|
||||
onChange={(p) => setTokensToShow(p)}
|
||||
values={['All', 'SOL', 'USDC']}
|
||||
/>
|
||||
{selectableTokens.length ? (
|
||||
!selectedToken ? (
|
||||
<>
|
||||
<div className="flex flex-col items-center ">
|
||||
<div className="w-full border-b border-th-bkg-3 p-6 text-center md:p-8">
|
||||
<h1 className="mb-1">Let's Boost!</h1>
|
||||
<p>Leverage up your liquid staking yield.</p>
|
||||
</div>
|
||||
<div className="p-6 md:p-8">
|
||||
<h2 className="mb-3 text-center text-lg font-normal">
|
||||
Select your yield
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4 text-lg font-bold">
|
||||
<button
|
||||
className={`${HERO_TOKEN_BUTTON_CLASSES} ${
|
||||
tokensToShow === 'SOL' ? 'bg-th-bkg-2' : ''
|
||||
}`}
|
||||
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 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
|
||||
.filter((t) => {
|
||||
if (tokensToShow === 'SOL') {
|
||||
return SOL_YIELD.includes(t)
|
||||
return SOL_YIELD.includes(t.token.symbol)
|
||||
} else if (tokensToShow === 'USDC') {
|
||||
return USDC_YIELD.includes(t)
|
||||
} else return t
|
||||
return USDC_YIELD.includes(t.token.symbol)
|
||||
} else return
|
||||
})
|
||||
.map((token) => (
|
||||
<HeroTokenButton
|
||||
key={token}
|
||||
onClick={() =>
|
||||
set((state) => {
|
||||
state.selectedToken = token
|
||||
})
|
||||
}
|
||||
tokenName={token}
|
||||
/>
|
||||
))}
|
||||
.map((token) => {
|
||||
const { symbol } = token.token
|
||||
return (
|
||||
<HeroTokenButton
|
||||
key={symbol}
|
||||
onClick={() =>
|
||||
set((state) => {
|
||||
state.selectedToken = symbol
|
||||
})
|
||||
}
|
||||
tokenInfo={token}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-10">
|
||||
<p className="text-center text-th-fgd-4">
|
||||
No positions to remove
|
||||
</p>
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="mb-3 flex items-center space-x-3">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
set((state) => {
|
||||
state.selectedToken = ''
|
||||
})
|
||||
}
|
||||
size="small"
|
||||
isPrimary
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
<h2>Boost! {selectedToken}</h2>
|
||||
</div>
|
||||
{/* <div className="pb-6">
|
||||
<Label text="Token to Boost!" />
|
||||
<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 className="p-10">
|
||||
<p className="text-center text-th-fgd-4">
|
||||
No positions to remove
|
||||
</p>
|
||||
</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">
|
||||
{isDesktop ? (
|
||||
<a
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import Image from 'next/image'
|
||||
import { formatTokenSymbol } from 'utils/tokens'
|
||||
import useBankRates from 'hooks/useBankRates'
|
||||
import useLeverageMax from 'hooks/useLeverageMax'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import SheenLoader from './shared/SheenLoader'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { SOL_YIELD } from './Stake'
|
||||
import useStakeableTokens, { StakeableToken } from 'hooks/useStakeableTokens'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
const TokenButton = ({
|
||||
onClick,
|
||||
|
@ -13,25 +14,15 @@ const TokenButton = ({
|
|||
tokenName: string
|
||||
onClick: () => void
|
||||
}) => {
|
||||
const leverage = useLeverageMax(tokenName)
|
||||
const groupLoaded = mangoStore((s) => s.groupLoaded)
|
||||
const { stakeableTokens } = useStakeableTokens()
|
||||
|
||||
const { stakeBankDepositRate, financialMetrics } = useBankRates(
|
||||
tokenName,
|
||||
leverage,
|
||||
)
|
||||
const tokenInfo: StakeableToken | undefined = useMemo(() => {
|
||||
if (!tokenName || !stakeableTokens?.length) return
|
||||
return stakeableTokens.find((token) => token.token.symbol === tokenName)
|
||||
}, [tokenName, stakeableTokens])
|
||||
|
||||
const { financialMetrics: estimatedNetAPYFor1xLev } = useBankRates(
|
||||
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)
|
||||
const apy = tokenInfo?.estNetApy
|
||||
|
||||
return (
|
||||
<button
|
||||
|
@ -59,21 +50,58 @@ const TokenButton = ({
|
|||
<SheenLoader>
|
||||
<div className={`h-5 w-10 bg-th-bkg-2`} />
|
||||
</SheenLoader>
|
||||
) : !UiRate || isNaN(UiRate) ? (
|
||||
) : !apy || isNaN(apy) ? (
|
||||
<span className="text-base font-normal text-th-fgd-4">
|
||||
Rate Unavailable
|
||||
</span>
|
||||
) : tokenName === 'USDC' ? (
|
||||
<>
|
||||
{`${UiRate.toFixed(2)}%`}{' '}
|
||||
<span className="text-sm font-normal text-th-fgd-4">APY</span>
|
||||
{`${apy.toFixed(2)}%`}
|
||||
<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)}%`}{' '}
|
||||
<span className="text-sm font-normal text-th-fgd-4">
|
||||
Max APY
|
||||
</span>
|
||||
{`${apy.toFixed(2)}%`}
|
||||
<div className="mt-1">
|
||||
{SOL_YIELD.includes(tokenName) ? (
|
||||
<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>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import Image from 'next/image'
|
||||
import { formatTokenSymbol } from 'utils/tokens'
|
||||
import useBankRates from 'hooks/useBankRates'
|
||||
import useLeverageMax from 'hooks/useLeverageMax'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import SheenLoader from './shared/SheenLoader'
|
||||
import { useMemo } from 'react'
|
||||
|
@ -9,48 +7,34 @@ import FormatNumericValue from './shared/FormatNumericValue'
|
|||
import { walletBalanceForToken } from './StakeForm'
|
||||
import usePositions from 'hooks/usePositions'
|
||||
import { ClientContextKeys } from 'utils/constants'
|
||||
import { StakeableToken } from 'hooks/useStakeableTokens'
|
||||
import { SOL_YIELD } from './Stake'
|
||||
|
||||
const TokenSelect = ({
|
||||
onClick,
|
||||
tokenName,
|
||||
tokenInfo,
|
||||
clientContext,
|
||||
showPositionSize,
|
||||
}: {
|
||||
tokenName: string
|
||||
tokenInfo: StakeableToken
|
||||
onClick: () => void
|
||||
clientContext: ClientContextKeys
|
||||
showPositionSize?: boolean
|
||||
}) => {
|
||||
const leverage = useLeverageMax(tokenName)
|
||||
const { symbol } = tokenInfo.token
|
||||
const { estNetApy } = tokenInfo
|
||||
const groupLoaded = mangoStore((s) => s.groupLoaded)
|
||||
const walletTokens = mangoStore((s) => s.wallet.tokens)
|
||||
const { positions } = usePositions()
|
||||
|
||||
const { stakeBankDepositRate, financialMetrics } = useBankRates(
|
||||
tokenName,
|
||||
leverage,
|
||||
)
|
||||
|
||||
const { financialMetrics: estimatedNetAPYFor1xLev } = useBankRates(
|
||||
tokenName,
|
||||
1,
|
||||
)
|
||||
|
||||
const walletBalance = useMemo(() => {
|
||||
return walletBalanceForToken(walletTokens, tokenName, clientContext)
|
||||
}, [walletTokens, tokenName, clientContext])
|
||||
return walletBalanceForToken(walletTokens, symbol, clientContext)
|
||||
}, [walletTokens, symbol, clientContext])
|
||||
|
||||
const position = useMemo(() => {
|
||||
if (!positions || !positions?.length) return
|
||||
return positions.find((position) => position.bank.name === tokenName)
|
||||
}, [positions, tokenName])
|
||||
|
||||
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 positions.find((position) => position.bank.name === symbol)
|
||||
}, [positions, symbol])
|
||||
|
||||
return (
|
||||
<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`}
|
||||
>
|
||||
<Image
|
||||
src={`/icons/${tokenName.toLowerCase()}.svg`}
|
||||
src={`/icons/${symbol.toLowerCase()}.svg`}
|
||||
width={24}
|
||||
height={24}
|
||||
alt="Select a token"
|
||||
|
@ -71,7 +55,7 @@ const TokenSelect = ({
|
|||
</div>
|
||||
<div className="text-left">
|
||||
<p className={`text-sm text-th-fgd-1 lg:text-base`}>
|
||||
{formatTokenSymbol(tokenName)}
|
||||
{formatTokenSymbol(symbol)}
|
||||
</p>
|
||||
<span
|
||||
className={`text-sm font-bold leading-none text-th-fgd-1 sm:text-lg`}
|
||||
|
@ -80,19 +64,56 @@ const TokenSelect = ({
|
|||
<SheenLoader>
|
||||
<div className={`h-5 w-10 bg-th-bkg-2`} />
|
||||
</SheenLoader>
|
||||
) : !UiRate || isNaN(UiRate) ? (
|
||||
) : !estNetApy || isNaN(estNetApy) ? (
|
||||
'Rate Unavailable'
|
||||
) : tokenName === 'USDC' ? (
|
||||
) : symbol === 'USDC' ? (
|
||||
<>
|
||||
{`${UiRate.toFixed(2)}%`}{' '}
|
||||
<span className="text-sm font-normal text-th-fgd-4">APY</span>
|
||||
{`${estNetApy.toFixed(2)}%`}
|
||||
<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)}%`}{' '}
|
||||
<span className="text-sm font-normal text-th-fgd-4">
|
||||
Max APY
|
||||
</span>
|
||||
{`${estNetApy.toFixed(2)}%`}
|
||||
<div className="mt-1">
|
||||
{SOL_YIELD.includes(symbol) ? (
|
||||
<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>
|
||||
|
|
|
@ -482,7 +482,7 @@ function UnstakeForm({
|
|||
})}
|
||||
</div>
|
||||
) : (
|
||||
`Unboost ${inputAmount} ${formatTokenSymbol(selectedToken)}`
|
||||
`Withdraw ${inputAmount} ${formatTokenSymbol(selectedToken)}`
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
|
|
|
@ -54,6 +54,7 @@ interface IconButtonProps {
|
|||
hideBg?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
ref?: Ref<HTMLButtonElement>
|
||||
isPrimary?: boolean
|
||||
}
|
||||
|
||||
type IconButtonCombinedProps = AllButtonProps & IconButtonProps
|
||||
|
@ -62,12 +63,20 @@ export const IconButton = forwardRef<
|
|||
HTMLButtonElement,
|
||||
IconButtonCombinedProps
|
||||
>((props, ref) => {
|
||||
const { children, onClick, disabled = false, className, hideBg, size } = props
|
||||
const {
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
className,
|
||||
hideBg,
|
||||
size,
|
||||
isPrimary,
|
||||
} = props
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`flex flex-shrink-0 ${
|
||||
className={`flex shrink-0 ${
|
||||
size === 'large'
|
||||
? 'h-12 w-12'
|
||||
: size === 'small'
|
||||
|
@ -78,7 +87,9 @@ export const IconButton = forwardRef<
|
|||
} items-center justify-center rounded-full ${
|
||||
hideBg
|
||||
? '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
|
||||
disabled:text-th-fgd-4 md:disabled:hover:text-th-fgd-4 ${className} focus-visible:text-th-active`}
|
||||
ref={ref}
|
||||
|
|
|
@ -2,33 +2,32 @@ import { useQuery } from '@tanstack/react-query'
|
|||
import { OHLCVPairItem, fetchOHLCPair } from 'apis/birdeye/helpers'
|
||||
import { SOL_MINT, STAKEABLE_TOKENS_DATA, USDC_MINT } from 'utils/constants'
|
||||
|
||||
const avgOpenClose = (i: OHLCVPairItem) => (i.c + i.o) * .5;
|
||||
const sum = (x: number, y: number) => x + y;
|
||||
const ANNUAL_SECONDS = 60 * 60 * 24 * 365;
|
||||
const avgOpenClose = (i: OHLCVPairItem) => (i.c + i.o) * 0.5
|
||||
const sum = (x: number, y: number) => x + y
|
||||
const ANNUAL_SECONDS = 60 * 60 * 24 * 365
|
||||
|
||||
const calculateRate = (ohlcvs: OHLCVPairItem[]) => {
|
||||
|
||||
|
||||
if (ohlcvs && ohlcvs?.length > 30) {
|
||||
|
||||
// basic least squares regression:
|
||||
// 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 ys = ohlcvs.map(avgOpenClose);
|
||||
const x_sum = xs.reduce(sum, 0);
|
||||
const y_sum = ys.reduce(sum, 0);
|
||||
const x_mean = x_sum / xs.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_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 xs = ohlcvs.map((o) => o.unixTime)
|
||||
const ys = ohlcvs.map(avgOpenClose)
|
||||
const x_sum = xs.reduce(sum, 0)
|
||||
const y_sum = ys.reduce(sum, 0)
|
||||
const x_mean = x_sum / xs.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_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 end = a + b * (xs[0] + ANNUAL_SECONDS);
|
||||
return { rate: (end - start)/start, start, end, a, b, S_xx, S_xy};
|
||||
const start = a + b * xs[0]
|
||||
const end = a + b * (xs[0] + ANNUAL_SECONDS)
|
||||
return { rate: (end - start) / start, start, end, a, b, S_xx, S_xy }
|
||||
} 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(
|
||||
(token) => token.mint_address !== USDC_MINT,
|
||||
).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 dailyCandles = await fetchOHLCPair(t.mint_address, quoteMint, '90');
|
||||
return dailyCandles;
|
||||
const dailyCandles = await fetchOHLCPair(t.mint_address, quoteMint, '90')
|
||||
return dailyCandles
|
||||
})
|
||||
const [
|
||||
jlpPrices,
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
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
|
||||
estNetApy: number
|
||||
}
|
||||
|
||||
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 tokenStakeRateAPY = stakeRates ? stakeRates[symbol.toLowerCase()] : 0
|
||||
|
||||
const leverage = getLeverage(stakeBank, borrowBank, symbol)
|
||||
const financialMetrics = getFinancialMetrics(
|
||||
stakeBank,
|
||||
borrowBank,
|
||||
leverage,
|
||||
tokenStakeRateAPY,
|
||||
)
|
||||
const financialMetricsAt1x = getFinancialMetrics(
|
||||
stakeBank,
|
||||
borrowBank,
|
||||
1,
|
||||
tokenStakeRateAPY,
|
||||
)
|
||||
const estNetApy = Math.max(financialMetrics.APY, financialMetricsAt1x.APY)
|
||||
stakeableTokens.push({ token, financialMetrics, estNetApy })
|
||||
}
|
||||
return { stakeableTokens }
|
||||
}
|
|
@ -4,109 +4,194 @@ import { PublicKey } from '@solana/web3.js'
|
|||
export const JLP_BORROW_TOKEN = 'USDC'
|
||||
export const LST_BORROW_TOKEN = 'SOL'
|
||||
|
||||
export const STAKEABLE_TOKENS_DATA: {
|
||||
export type StakeableTokensData = {
|
||||
name: string
|
||||
symbol: string
|
||||
description: string
|
||||
id: number
|
||||
active: boolean
|
||||
mint_address: string
|
||||
clientContext: ClientContextKeys
|
||||
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,
|
||||
active: true,
|
||||
mint_address: '27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4',
|
||||
clientContext: 'jlp',
|
||||
borrowToken: 'USDC',
|
||||
links: {
|
||||
website: 'https://jup.ag/',
|
||||
twitter: 'https://twitter.com/JupiterExchange',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'USDC',
|
||||
name: 'USD Coin',
|
||||
symbol: 'USDC',
|
||||
description: '',
|
||||
id: 0,
|
||||
active: true,
|
||||
mint_address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
clientContext: 'jlp',
|
||||
borrowToken: 'USDC',
|
||||
links: {
|
||||
website: 'https://www.circle.com/en/usdc',
|
||||
twitter: 'https://twitter.com/circle',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MSOL',
|
||||
name: 'Marinade Staked SOL',
|
||||
symbol: 'MSOL',
|
||||
description:
|
||||
'Marinade is a stake automation platform that monitors all Solana validators and delegates to the 100+ best-performing ones.',
|
||||
id: 521,
|
||||
active: true,
|
||||
mint_address: 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://marinade.finance/',
|
||||
twitter: 'https://twitter.com/marinadefinance',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'JitoSOL',
|
||||
name: 'Jito Staked SOL',
|
||||
symbol: 'JitoSOL',
|
||||
description:
|
||||
'JitoSOL supports the decentralization and health of the Solana network through efficient MEV extraction and spam reduction.',
|
||||
id: 621,
|
||||
active: true,
|
||||
mint_address: 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://www.jito.wtf/',
|
||||
twitter: 'https://twitter.com/jito_labs',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'bSOL',
|
||||
name: 'BlazeStake Staked SOL',
|
||||
symbol: 'bSOL',
|
||||
description: '',
|
||||
id: 721,
|
||||
active: true,
|
||||
mint_address: 'bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://solblaze.org/',
|
||||
twitter: 'https://twitter.com/solblaze_org',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'JSOL',
|
||||
name: 'JPool Staked SOL',
|
||||
symbol: 'JSOL',
|
||||
description: '',
|
||||
id: 1063,
|
||||
active: true,
|
||||
mint_address: '7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://jpool.one/',
|
||||
twitter: 'https://twitter.com/JPoolSolana',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'INF',
|
||||
name: 'Sanctum Infinity',
|
||||
symbol: 'INF',
|
||||
description:
|
||||
"Infinity is the first infinite-LST pool and is Sanctum's flagship product. Infinity holds a basket of LSTs and earns staking yields and trading fees.",
|
||||
id: 1105,
|
||||
active: true,
|
||||
mint_address: '5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://www.sanctum.so/',
|
||||
twitter: 'https://twitter.com/sanctumso',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'hubSOL',
|
||||
name: 'SolanaHub Staked SOL',
|
||||
symbol: 'hubSOL',
|
||||
description: 'A revenue-sharing validator empowering SolanaHub users',
|
||||
id: 1153,
|
||||
active: true,
|
||||
mint_address: 'HUBsveNpjo5pWqNkH57QzxjQASdTVXcSK7bVKTSZtcSX',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://www.solanahub.app/',
|
||||
twitter: 'https://twitter.com/SolanaHubApp',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'digitSOL',
|
||||
name: 'Simpdigit Staked SOL',
|
||||
symbol: 'digitSOL',
|
||||
description:
|
||||
'High performance LST with MEV and voting rewards kickback. Support developers in South America.',
|
||||
id: 1161,
|
||||
active: true,
|
||||
mint_address: 'D1gittVxgtszzY4fMwiTfM4Hp7uL5Tdi1S9LYaepAUUm',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://simpdigit.com/',
|
||||
twitter: 'https://twitter.com/simpdigit',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dualSOL',
|
||||
name: 'Dual Finance Staked SOL',
|
||||
symbol: 'dualSOL',
|
||||
description: 'The LST to grow onchain options',
|
||||
id: 1158,
|
||||
active: true,
|
||||
mint_address: 'DUAL6T9pATmQUFPYmrWq2BkkGdRxLtERySGScYmbHMER',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: '',
|
||||
twitter: 'https://x.com/DualFinance',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mangoSOL',
|
||||
name: 'Mango Staked SOL',
|
||||
symbol: 'mangoSOL',
|
||||
description: 'The juiciest LST to borrow everything & trade anything',
|
||||
id: 1162,
|
||||
active: true,
|
||||
mint_address: 'MangmsBgFqJhW4cLUR9LxfVgMboY1xAoP8UUBiWwwuY',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://mango.markets/',
|
||||
twitter: 'https://x.com/mangomarkets',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'compassSOL',
|
||||
name: 'Solana Compass Staked SOL',
|
||||
symbol: 'compassSOL',
|
||||
description:
|
||||
'Earn boosted yield from staking yields, MEV tips and priority fees, then put your tokens to work in high performance liquidity pools for even more yield on your SOL.',
|
||||
id: 1163,
|
||||
active: true,
|
||||
mint_address: 'Comp4ssDzXcLeu2MnLuGNNFC4cmLPMng8qWHPvzAMU1h',
|
||||
clientContext: 'lst',
|
||||
borrowToken: 'SOL',
|
||||
links: {
|
||||
website: 'https://solanacompass.com/',
|
||||
twitter: 'https://twitter.com/SolanaCompass',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -114,7 +199,7 @@ export type ClientContextKeys = 'lst' | 'jlp'
|
|||
|
||||
export const STAKEABLE_TOKENS = STAKEABLE_TOKENS_DATA.filter(
|
||||
(d) => d.active,
|
||||
).map((d) => d.name)
|
||||
).map((d) => d.symbol)
|
||||
|
||||
export const SHOW_INACTIVE_POSITIONS_KEY = 'showInactivePositions-0.1'
|
||||
// end
|
||||
|
|
|
@ -80,6 +80,6 @@ export const getStakableTokensDataForMint = (mintPk: string) => {
|
|||
return STAKEABLE_TOKENS_DATA.find((x) => x.mint_address === mintPk)!
|
||||
}
|
||||
|
||||
export const getStakableTokensDataForTokenName = (tokenName: string) => {
|
||||
return STAKEABLE_TOKENS_DATA.find((x) => x.name === tokenName)!
|
||||
export const getStakableTokensDataForTokenName = (tokenSymbol: string) => {
|
||||
return STAKEABLE_TOKENS_DATA.find((x) => x.symbol === tokenSymbol)!
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue