Merge pull request #27 from blockworks-foundation/boost-token-ux

boost token ux
This commit is contained in:
saml33 2024-06-26 09:32:57 +10:00 committed by GitHub
commit dc2c2fb833
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 787 additions and 397 deletions

View File

@ -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>
)
}

View File

@ -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'

View File

@ -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>

View File

@ -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&apos;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

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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}

View File

@ -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,

174
hooks/useStakeableTokens.ts Normal file
View File

@ -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 }
}

View File

@ -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

View File

@ -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)!
}