Compare commits

...

2 Commits

Author SHA1 Message Date
Adrian Brzeziński abadeefd2a fix liq price in position 2024-03-28 16:33:05 +01:00
saml33 3c6df1dbcc
add boost v1 tokens (#23)
* wip

* filter tokens in remove list

* show swap notification if no balance to add

* fix client initialization

* fix

* client context checkpoint + leverage fix

* fix stakerates

* leverage and usdc fixes

* fix edit position

* add more details to token select list

* set percent to 100 on max click

* fix input decimal scale

* update faqs

* add token list close button

* re-arrange stats

* fix

* small fixes/updates

* upgrade client

---------

Co-authored-by: Adrian Brzeziński <a.brzezinski94@gmail.com>
2024-03-28 15:52:55 +01:00
37 changed files with 1292 additions and 1584 deletions

View File

@ -1,71 +0,0 @@
import { formatTokenSymbol } from 'utils/tokens'
import { useMemo } from 'react'
import Decimal from 'decimal.js'
import useMangoGroup from 'hooks/useMangoGroup'
import FormatNumericValue from './shared/FormatNumericValue'
import mangoStore from '@store/mangoStore'
const AccountStats = ({ token }: { token: string }) => {
const { group } = useMangoGroup()
const estimatedMaxAPY = mangoStore((s) => s.estimatedMaxAPY.current)
const borrowBank = useMemo(() => {
return group?.banksMapByName.get('USDC')?.[0]
}, [group])
const tokenBank = useMemo(() => {
return group?.banksMapByName.get(token)?.[0]
}, [group, token])
const borrowDeposits = useMemo(() => {
if (!borrowBank) return null
return borrowBank.uiDeposits()
}, [borrowBank])
const tokenDeposits = useMemo(() => {
if (!tokenBank) return null
return tokenBank.uiDeposits()
}, [tokenBank])
const solAvailable = useMemo(() => {
if (!borrowBank || !borrowDeposits) return 0
const availableVaultBalance = group
? group.getTokenVaultBalanceByMintUi(borrowBank.mint) -
borrowDeposits * borrowBank.minVaultToDepositsRatio
: 0
return Decimal.max(0, availableVaultBalance.toFixed(borrowBank.mintDecimals))
}, [borrowBank, borrowDeposits, group])
return (
<>
<h2 className="mb-4 text-2xl">{`Boosted ${formatTokenSymbol(token)}`}</h2>
<div className="grid grid-cols-2 gap-6 md:grid-cols-1">
<div>
<p className="mb-1">Max Est. APY</p>
<span className="text-2xl font-bold">
{estimatedMaxAPY ? `${estimatedMaxAPY.toFixed(2)}%` : 0}
</span>
</div>
<div>
<p className="mb-1">Max Leverage</p>
<span className="text-2xl font-bold">3x</span>
</div>
<div>
<p className="mb-1">Capacity Remaining</p>
<span className="text-2xl font-bold">
<FormatNumericValue value={solAvailable} decimals={0} /> SOL
</span>
</div>
<div>
<p className="mb-1">Total Staked</p>
<span className="text-2xl font-bold">
<FormatNumericValue value={tokenDeposits || 0} decimals={1} />{' '}
{formatTokenSymbol(token)}
</span>
</div>
</div>
</>
)
}
export default AccountStats

View File

@ -2,10 +2,13 @@ import { ArrowPathIcon, ExclamationCircleIcon } from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
import { useTranslation } from 'next-i18next'
import React, { useCallback, useMemo, useState } from 'react'
import NumberFormat, { NumberFormatValues } from 'react-number-format'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
} from 'react-number-format'
import mangoStore from '@store/mangoStore'
import { notify } from '../utils/notifications'
import { TokenAccount, formatTokenSymbol } from '../utils/tokens'
import { formatTokenSymbol } from '../utils/tokens'
import Label from './forms/Label'
import Button, { IconButton } from './shared/Button'
import Loading from './shared/Loading'
@ -23,12 +26,13 @@ import Link from 'next/link'
import useMangoGroup from 'hooks/useMangoGroup'
import { depositAndCreate, getNextAccountNumber } from 'utils/transactions'
// import { MangoAccount } from '@blockworks-foundation/mango-v4'
import { AnchorProvider } from '@project-serum/anchor'
import SheenLoader from './shared/SheenLoader'
import { sleep } from 'utils'
import ButtonGroup from './forms/ButtonGroup'
import Decimal from 'decimal.js'
import useIpAddress from 'hooks/useIpAddress'
import { walletBalanceForToken } from './StakeForm'
import { ClientContextKeys } from 'utils/constants'
const set = mangoStore.getState().set
@ -37,27 +41,7 @@ export const NUMBERFORMAT_CLASSES =
interface StakeFormProps {
token: string
}
export const walletBalanceForToken = (
walletTokens: TokenAccount[],
token: string,
): { maxAmount: number; maxDecimals: number } => {
const group = mangoStore.getState().group
const bank = group?.banksMapByName.get(token)?.[0]
let walletToken
if (bank) {
const tokenMint = bank?.mint
walletToken = tokenMint
? walletTokens.find((t) => t.mint.toString() === tokenMint.toString())
: null
}
return {
maxAmount: walletToken ? walletToken.uiAmount : 0,
maxDecimals: bank?.mintDecimals || 6,
}
clientContext: ClientContextKeys
}
// const getNextAccountNumber = (accounts: MangoAccount[]): number => {
@ -73,14 +57,14 @@ export const walletBalanceForToken = (
// return 0
// }
function DespositForm({ token: selectedToken }: StakeFormProps) {
function DespositForm({ token: selectedToken, clientContext }: StakeFormProps) {
const { t } = useTranslation(['common', 'account'])
const [inputAmount, setInputAmount] = useState('')
const submitting = mangoStore((s) => s.submittingBoost)
const [refreshingWalletTokens, setRefreshingWalletTokens] = useState(false)
const { maxSolDeposit } = useSolBalance()
const { usedTokens, totalTokens } = useMangoAccountAccounts()
const { group } = useMangoGroup()
const { jlpGroup } = useMangoGroup()
const groupLoaded = mangoStore((s) => s.groupLoaded)
const { connected, publicKey } = useWallet()
const walletTokens = mangoStore((s) => s.wallet.tokens)
@ -88,11 +72,11 @@ function DespositForm({ token: selectedToken }: StakeFormProps) {
const { ipAllowed } = useIpAddress()
const depositBank = useMemo(() => {
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken, group])
return jlpGroup?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken, jlpGroup])
const tokenMax = useMemo(() => {
return walletBalanceForToken(walletTokens, selectedToken)
return walletBalanceForToken(walletTokens, selectedToken, clientContext)
}, [walletTokens, selectedToken])
const setMax = useCallback(() => {
@ -121,7 +105,7 @@ function DespositForm({ token: selectedToken }: StakeFormProps) {
return
}
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const group = mangoStore.getState().group[clientContext]
const actions = mangoStore.getState().actions
const mangoAccounts = mangoStore.getState().mangoAccounts
const mangoAccount = mangoStore.getState().mangoAccount.current
@ -138,7 +122,7 @@ function DespositForm({ token: selectedToken }: StakeFormProps) {
type: 'info',
})
const { signature: tx, slot } = await depositAndCreate(
client,
client[clientContext],
group,
mangoAccount,
depositBank.mint,
@ -154,13 +138,12 @@ function DespositForm({ token: selectedToken }: StakeFormProps) {
state.submittingBoost = false
})
setInputAmount('')
setSizePercentage('')
await sleep(500)
if (!mangoAccount) {
await actions.fetchMangoAccounts(
(client.program.provider as AnchorProvider).wallet.publicKey,
)
await actions.fetchMangoAccounts(publicKey)
}
await actions.reloadMangoAccount(slot)
await actions.reloadMangoAccount(clientContext, slot)
await actions.fetchWalletTokens(publicKey)
} catch (e) {
console.error('Error depositing:', e)
@ -175,7 +158,7 @@ function DespositForm({ token: selectedToken }: StakeFormProps) {
type: 'error',
})
}
}, [depositBank, publicKey, inputAmount])
}, [depositBank, publicKey, inputAmount, ipAllowed])
const showInsufficientBalance =
tokenMax.maxAmount < Number(inputAmount) ||
@ -238,10 +221,13 @@ function DespositForm({ token: selectedToken }: StakeFormProps) {
className={NUMBERFORMAT_CLASSES}
placeholder="0.00"
value={inputAmount}
onValueChange={(e: NumberFormatValues) => {
onValueChange={(e: NumberFormatValues, info: SourceInfo) => {
setInputAmount(
!Number.isNaN(Number(e.value)) ? e.value : '',
)
if (info.source === 'event') {
setSizePercentage('')
}
}}
isAllowed={withValueLimit}
/>
@ -288,7 +274,7 @@ function DespositForm({ token: selectedToken }: StakeFormProps) {
<Loading className="mr-2 h-5 w-5" />
) : showInsufficientBalance ? (
<div className="flex items-center">
<ExclamationCircleIcon className="icon-shadow mr-2 h-5 w-5 flex-shrink-0" />
<ExclamationCircleIcon className="icon-shadow mr-2 h-5 w-5 shrink-0" />
{t('swap:insufficient-balance', {
symbol: selectedToken,
})}

View File

@ -3,7 +3,10 @@ import { useTranslation } from 'next-i18next'
import React, { useCallback, useMemo, useState } from 'react'
import mangoStore from '@store/mangoStore'
import { notify } from '../utils/notifications'
import { TokenAccount, formatTokenSymbol } from '../utils/tokens'
import {
formatTokenSymbol,
getStakableTokensDataForTokenName,
} from '../utils/tokens'
import Label from './forms/Label'
import Button from './shared/Button'
import Loading from './shared/Loading'
@ -29,6 +32,7 @@ import { Disclosure } from '@headlessui/react'
import useLeverageMax from 'hooks/useLeverageMax'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import { simpleSwap } from 'utils/transactions'
import { JLP_BORROW_TOKEN, LST_BORROW_TOKEN } from 'utils/constants'
const set = mangoStore.getState().set
@ -40,27 +44,6 @@ interface EditLeverageFormProps {
onSuccess: () => void
}
export const walletBalanceForToken = (
walletTokens: TokenAccount[],
token: string,
): { maxAmount: number; maxDecimals: number } => {
const group = mangoStore.getState().group
const bank = group?.banksMapByName.get(token)?.[0]
let walletToken
if (bank) {
const tokenMint = bank?.mint
walletToken = tokenMint
? walletTokens.find((t) => t.mint.toString() === tokenMint.toString())
: null
}
return {
maxAmount: walletToken ? walletToken.uiAmount : 0,
maxDecimals: bank?.mintDecimals || 6,
}
}
// const getNextAccountNumber = (accounts: MangoAccount[]): number => {
// if (accounts.length > 1) {
// return (
@ -78,21 +61,28 @@ function EditLeverageForm({
token: selectedToken,
onSuccess,
}: EditLeverageFormProps) {
const clientContext =
getStakableTokensDataForTokenName(selectedToken).clientContext
const { t } = useTranslation(['common', 'account'])
const submitting = mangoStore((s) => s.submittingBoost)
const { ipAllowed } = useIpAddress()
const storedLeverage = mangoStore((s) => s.leverage)
const { usedTokens, totalTokens } = useMangoAccountAccounts()
const { group } = useMangoGroup()
const { jlpGroup, lstGroup } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const groupLoaded = mangoStore((s) => s.groupLoaded)
const leverageMax = useLeverageMax(selectedToken) * 0.9 // Multiplied by 0.975 becuase you cant actually get to the end of the inifinite geometric series?
const stakeBank = useMemo(() => {
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken, group])
const borrowBank = useMemo(() => {
return group?.banksMapByName.get('USDC')?.[0]
}, [group])
const leverageMax = useLeverageMax(selectedToken)
const [stakeBank, borrowBank] = useMemo(() => {
const stakeBank =
clientContext === 'jlp'
? jlpGroup?.banksMapByName.get(selectedToken)?.[0]
: lstGroup?.banksMapByName.get(selectedToken)?.[0]
const borrowBank =
clientContext === 'jlp'
? jlpGroup?.banksMapByName.get(JLP_BORROW_TOKEN)?.[0]
: lstGroup?.banksMapByName.get(LST_BORROW_TOKEN)?.[0]
return [stakeBank, borrowBank]
}, [selectedToken, jlpGroup, lstGroup, clientContext])
const stakeBankAmount =
mangoAccount && stakeBank && mangoAccount?.getTokenBalance(stakeBank)
@ -100,12 +90,25 @@ function EditLeverageForm({
const borrowAmount =
mangoAccount && borrowBank && mangoAccount?.getTokenBalance(borrowBank)
const borrowBankAmount =
mangoAccount && borrowBank && mangoAccount.getTokenBalance(borrowBank)
const current_leverage = useMemo(() => {
try {
if (stakeBankAmount && borrowAmount) {
const currentDepositValue = Number(stakeBankAmount) * stakeBank.uiPrice
const lev =
currentDepositValue / (currentDepositValue + Number(borrowAmount))
if (
stakeBankAmount &&
borrowBankAmount &&
borrowBankAmount.toNumber() < 0
) {
const stakeAmountValue = stakeBankAmount.mul(stakeBank.getAssetPrice())
const lev = stakeAmountValue
.div(
stakeAmountValue.sub(
borrowBankAmount.abs().mul(borrowBank.getAssetPrice()),
),
)
.toNumber()
return Math.sign(lev) !== -1 ? lev : 1
}
return 1
@ -113,7 +116,7 @@ function EditLeverageForm({
console.log(e)
return 1
}
}, [stakeBankAmount, borrowAmount, stakeBank])
}, [stakeBankAmount, borrowBankAmount, stakeBank, borrowBank])
const [leverage, setLeverage] = useState(current_leverage)
@ -157,12 +160,13 @@ function EditLeverageForm({
const stakePrice = stakeBank?.uiPrice
if (!borrowPrice || !stakePrice || !Number(tokenMax.maxAmount)) return 0
const borrowAmount =
stakeBank?.uiPrice * Number(tokenMax.maxAmount) * (leverage - 1)
(stakeBank?.uiPrice * Number(tokenMax.maxAmount) * (leverage - 1)) /
borrowBank.uiPrice
return borrowAmount
}, [leverage, borrowBank, stakeBank, tokenMax])
const availableVaultBalance = useMemo(() => {
if (!borrowBank || !group) return 0
if (!borrowBank) return 0
const maxUtilization = 1 - borrowBank.minVaultToDepositsRatio
const vaultBorrows = borrowBank.uiBorrows()
const vaultDeposits = borrowBank.uiDeposits()
@ -171,7 +175,7 @@ function EditLeverageForm({
const available =
(maxUtilization * vaultDeposits - vaultBorrows) * loanOriginationFeeFactor
return available
}, [borrowBank, group])
}, [borrowBank])
const changeInJLP = useMemo(() => {
if (stakeBankAmount) {
@ -235,8 +239,8 @@ function EditLeverageForm({
if (changeInJLP > 0) {
console.log('Swapping From USDC to JLP')
const { signature: tx, slot } = await simpleSwap(
client,
group,
client[clientContext],
group[clientContext]!,
mangoAccount,
borrowBank?.mint,
stakeBank?.mint,
@ -251,8 +255,8 @@ function EditLeverageForm({
} else {
console.log('Swapping From JLP to USDC')
const { signature: tx, slot } = await simpleSwap(
client,
group,
client[clientContext],
group[clientContext]!,
mangoAccount,
stakeBank?.mint,
borrowBank?.mint,
@ -273,14 +277,15 @@ function EditLeverageForm({
await sleep(500)
if (!mangoAccount) {
await actions.fetchMangoAccounts(
(client.program.provider as AnchorProvider).wallet.publicKey,
(client[clientContext].program.provider as AnchorProvider).wallet
.publicKey,
)
}
await actions.reloadMangoAccount(slot_retrieved)
await actions.reloadMangoAccount(clientContext, slot_retrieved)
await actions.fetchWalletTokens(publicKey)
await actions.fetchGroup()
await actions.reloadMangoAccount()
await actions.reloadMangoAccount(clientContext)
onSuccess()
} catch (e) {
console.error('Error depositing:', e)
@ -295,7 +300,16 @@ function EditLeverageForm({
type: 'error',
})
}
}, [ipAllowed, stakeBank, publicKey, amountToBorrow, borrowBank?.mint])
}, [
ipAllowed,
stakeBank,
borrowBank,
publicKey,
changeInJLP,
clientContext,
onSuccess,
changeInUSDC,
])
const tokenDepositLimitLeft = stakeBank?.getRemainingDepositLimit()
const tokenDepositLimitLeftUi =
@ -320,9 +334,10 @@ function EditLeverageForm({
useEffect(() => {
const group = mangoStore.getState().group
set((state) => {
state.swap.outputBank = group?.banksMapByName.get(selectedToken)?.[0]
state.swap.outputBank =
group[clientContext]?.banksMapByName.get(selectedToken)?.[0]
})
}, [selectedToken])
}, [selectedToken, clientContext])
return (
<>
@ -365,6 +380,7 @@ function EditLeverageForm({
{leverage.toFixed(2)}x
</p>
</div>
<LeverageSlider
startingValue={current_leverage}
leverageMax={leverageMax}

View File

@ -1,8 +1,6 @@
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid'
const Footer = () => {
return (
<div className="mt-6 flex items-center justify-between rounded-lg border-2 border-th-fgd-1 bg-th-bkg-1 px-6 py-4">
<div className="mt-6 flex items-center justify-center rounded-lg border-2 border-th-fgd-1 bg-th-bkg-1 px-6 py-4">
<a
href="https://app.mango.markets"
rel="noopener noreferrer"
@ -10,15 +8,6 @@ const Footer = () => {
>
<span className="font-bold text-th-fgd-1">Powered by 🥭</span>
</a>
<a
className="flex items-center rounded bg-th-bkg-1 px-1.5 py-1 text-th-fgd-1"
target="_blank"
href="https://boost-v1.mango.markets/"
rel="noopener noreferrer"
>
<span className="mr-1.5 block font-bold leading-none">Boost! v1</span>
<ArrowTopRightOnSquareIcon className="h-5 w-5" />
</a>
</div>
)
}

View File

@ -6,7 +6,7 @@ import TransactionHistory from './TransactionHistory'
import mangoStore, { ActiveTab } from '@store/mangoStore'
import { useCallback, useEffect } from 'react'
import { BOOST_ACCOUNT_PREFIX } from 'utils/constants'
import HowItWorks from './HowItWorks'
// import HowItWorks from './HowItWorks'
const set = mangoStore.getState().set
@ -48,7 +48,7 @@ const HomePage = () => {
/>
</div>
<TabContent activeTab={activeTab} setActiveTab={setActiveTab} />
<HowItWorks />
{/* <HowItWorks /> */}
</>
)
}

View File

@ -1,8 +1,5 @@
import { Fragment, ReactNode, useEffect, useMemo, useState } from 'react'
import {
ArrowPathIcon,
ArrowTopRightOnSquareIcon,
} from '@heroicons/react/20/solid'
import { ArrowPathIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import TopBar from './TopBar'
import useLocalStorageState from '../hooks/useLocalStorageState'
@ -43,7 +40,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
{children}
<Footer />
</div>
<div className="fixed bottom-8 right-8 hidden lg:block">
{/* <div className="fixed bottom-8 right-8 hidden lg:block">
<a
className="flex items-center rounded-md border-b-2 border-th-bkg-3 bg-th-bkg-1 px-2 py-0.5 text-th-fgd-1"
target="_blank"
@ -53,7 +50,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<span className="mr-1.5 block font-bold">Boost! v1</span>
<ArrowTopRightOnSquareIcon className="h-5 w-5" />
</a>
</div>
</div> */}
<DeployRefreshManager />
<TermsOfUse />
<RestrictedCountryCheck

View File

@ -8,14 +8,19 @@ import useNetworkSpeed from 'hooks/useNetworkSpeed'
import { useWallet } from '@solana/wallet-adapter-react'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { DEFAULT_PRIORITY_FEE_LEVEL } from './settings/RpcSettings'
import { getStakableTokensDataForTokenName } from 'utils/tokens'
const set = mangoStore.getState().set
const actions = mangoStore.getState().actions
const HydrateStore = () => {
const { mangoAccountPk } = useMangoAccount()
const selectedToken = mangoStore((s) => s.selectedToken)
const clientContext =
getStakableTokensDataForTokenName(selectedToken).clientContext
const connection = mangoStore((s) => s.connection)
const fee = mangoStore((s) => s.priorityFee)
const slowNetwork = useNetworkSpeed()
const { wallet } = useWallet()
@ -49,7 +54,7 @@ const HydrateStore = () => {
useInterval(
() => {
actions.fetchGroup()
actions.reloadMangoAccount()
actions.reloadMangoAccount(clientContext)
},
(slowNetwork ? 60 : 20) * SECONDS,
)
@ -95,7 +100,6 @@ const HydrateStore = () => {
},
(slowNetwork ? 60 : 10) * SECONDS,
)
console.log(fee)
// The websocket library solana/web3.js uses closes its websocket connection when the subscription list
// is empty after opening its first time, preventing subsequent subscriptions from receiving responses.
@ -111,7 +115,7 @@ const HydrateStore = () => {
// watch selected Mango Account for changes
useEffect(() => {
const client = mangoStore.getState().client
const client = mangoStore.getState().client[clientContext]
if (!mangoAccountPk) return
const subscriptionId = connection.onAccountChange(
mangoAccountPk,
@ -144,7 +148,7 @@ const HydrateStore = () => {
return () => {
connection.removeAccountChangeListener(subscriptionId)
}
}, [connection, mangoAccountPk])
}, [connection, mangoAccountPk, clientContext])
return null
}

View File

@ -21,7 +21,7 @@ import Tooltip from './shared/Tooltip'
const set = mangoStore.getState().set
type Position = {
export type Position = {
borrowBalance: number
stakeBalance: number
pnl: number
@ -29,18 +29,6 @@ type Position = {
acct: MangoAccount | undefined
}
const getLiquidationRatio = (
borrowBalance: number,
stakeBalance: number,
stakeBank: Bank,
borrowBank: Bank,
) => {
return (
(Math.abs(borrowBalance) * borrowBank.maintLiabWeight.toNumber()) /
(stakeBalance * stakeBank.maintAssetWeight.toNumber())
).toFixed(3)
}
const Positions = ({
setActiveTab,
}: {
@ -48,7 +36,9 @@ const Positions = ({
}) => {
const [showInactivePositions, setShowInactivePositions] =
useLocalStorageState(SHOW_INACTIVE_POSITIONS_KEY, true)
const { borrowBank, positions } = usePositions(showInactivePositions)
const { positions, jlpBorrowBank, lstBorrowBank } = usePositions(
showInactivePositions,
)
const numberOfPositions = useMemo(() => {
if (!positions.length) return 0
@ -71,12 +61,14 @@ const Positions = ({
<div className="grid grid-cols-1 gap-2">
{positions.length ? (
positions.map((position) => {
const { bank } = position
const isUsdcBorrow = bank.name === 'JLP' || bank.name === 'USDC'
return position.bank ? (
<PositionItem
key={position.bank.name}
key={bank.name}
position={position}
setActiveTab={setActiveTab}
borrowBank={borrowBank}
borrowBank={isUsdcBorrow ? jlpBorrowBank : lstBorrowBank}
/>
) : null
})
@ -99,8 +91,8 @@ const PositionItem = ({
setActiveTab: (v: ActiveTab) => void
borrowBank: Bank | undefined
}) => {
const { group } = useMangoGroup()
const { stakeBalance, borrowBalance, bank, pnl, acct } = position
const { jlpGroup, lstGroup } = useMangoGroup()
const { stakeBalance, bank, pnl, acct } = position
const handleAddOrManagePosition = (token: string) => {
setActiveTab('Boost!')
@ -111,7 +103,10 @@ const PositionItem = ({
const [showEditLeverageModal, setShowEditLeverageModal] = useState(false)
const leverage = useMemo(() => {
if (!group || !acct) return 1
if (!acct || !bank) return 1
const isJlpGroup = bank.name === 'JLP' || bank.name === 'USDC'
const group = isJlpGroup ? jlpGroup : lstGroup
if (!group) return 1
const accountValue = toUiDecimalsForQuote(acct.getEquity(group).toNumber())
const assetsValue = toUiDecimalsForQuote(
@ -123,22 +118,20 @@ const PositionItem = ({
} else {
return Math.abs(1 - assetsValue / accountValue) + 1
}
}, [group, acct])
}, [acct, bank, jlpGroup, lstGroup])
const [liqRatio] = useMemo(() => {
if (!borrowBalance || !borrowBank) return ['0.00', '']
const liqRatio = getLiquidationRatio(
borrowBalance,
stakeBalance,
bank,
borrowBank,
)
const currentPriceRatio = bank.uiPrice / borrowBank.uiPrice
const liqPriceChangePercentage =
((parseFloat(liqRatio) - currentPriceRatio) / currentPriceRatio) * 100
return [liqRatio, liqPriceChangePercentage.toFixed(2)]
}, [bank, borrowBalance, borrowBank, stakeBalance])
const liqRatio = useMemo(() => {
const price = Number(bank?.uiPrice)
const borrowMaintLiabWeight = Number(borrowBank?.maintLiabWeight)
const stakeMaintAssetWeight = Number(bank?.maintAssetWeight)
const loanOriginationFee = Number(borrowBank?.loanOriginationFeeRate)
const liqPrice =
price *
((borrowMaintLiabWeight * (1 + loanOriginationFee)) /
stakeMaintAssetWeight) *
(1 - 1 / leverage)
return liqPrice.toFixed(3)
}, [bank, borrowBank, leverage])
const { financialMetrics, stakeBankDepositRate, borrowBankBorrowRate } =
useBankRates(bank.name, leverage)
@ -296,9 +289,12 @@ const PositionItem = ({
{leverage ? leverage.toFixed(2) : 0.0}x
</span>
<button
onClick={() =>
onClick={async () => {
await set((state) => {
state.selectedToken = bank.name
})
setShowEditLeverageModal(!showEditLeverageModal)
}
}}
className="default-transition flex items-center rounded-md border-b-2 border-th-bkg-4 bg-th-bkg-2 px-2.5 py-1 text-th-fgd-1 md:hover:bg-th-bkg-3"
>
<AdjustmentsHorizontalIcon className="mr-1.5 h-4 w-4" />
@ -328,7 +324,9 @@ const PositionItem = ({
<EditLeverageModal
token={bank.name}
isOpen={showEditLeverageModal}
onClose={() => setShowEditLeverageModal(false)}
onClose={() => {
setShowEditLeverageModal(false)
}}
/>
) : null}
</div>

View File

@ -1,70 +1,206 @@
import TokenButton from './TokenButton'
import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import TabUnderline from './shared/TabUnderline'
import StakeForm from '@components/StakeForm'
import StakeForm, { walletBalanceForToken } from '@components/StakeForm'
import UnstakeForm from '@components/UnstakeForm'
import mangoStore from '@store/mangoStore'
import { STAKEABLE_TOKENS } from 'utils/constants'
import { formatTokenSymbol } from 'utils/tokens'
import {
formatTokenSymbol,
getStakableTokensDataForTokenName,
} from 'utils/tokens'
import { useViewport } from 'hooks/useViewport'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid'
import { 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'
const set = mangoStore.getState().set
const Stake = () => {
const [activeFormTab, setActiveFormTab] = useState('Add')
const [showTokenSelect, setShowTokenSelect] = useState(false)
const selectedToken = mangoStore((s) => s.selectedToken)
const walletTokens = mangoStore((s) => s.wallet.tokens)
const { isDesktop } = useViewport()
const { positions } = usePositions()
const handleTokenSelect = useCallback((token: string) => {
set((state) => {
state.selectedToken = token
})
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
})
}
},
[hasPosition, positions],
)
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])
const swapUrl = `https://app.mango.markets/swap?in=USDC&out=${selectedToken}&walletSwap=true`
return (
<>
<div className="grid grid-cols-2 rounded-t-2xl border-2 border-b-0 border-th-fgd-1 bg-th-bkg-1">
{STAKEABLE_TOKENS.map((token) => (
<TokenButton
key={token}
handleTokenSelect={handleTokenSelect}
selectedToken={selectedToken}
tokenName={token}
/>
))}
</div>
<div className="grid grid-cols-12">
<div className="relative overflow-hidden">
<EnterBottomExitBottom
className="absolute bottom-0 left-0 z-20 h-full w-full overflow-hidden rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 px-3 py-6 pb-0"
show={showTokenSelect}
>
<div className="mb-4 flex items-center justify-between">
<div className="h-10 w-10" />
<h2>
Select token to {activeFormTab === 'Add' ? 'Boost!' : 'Unboost'}
</h2>
<IconButton
onClick={() => setShowTokenSelect(false)}
hideBg
size="medium"
>
<XMarkIcon className="h-6 w-6" />
</IconButton>
</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>
</div>
<div>
{selectableTokens.map((token) => (
<TokenSelect
key={token}
onClick={() => handleTokenSelect(token)}
tokenName={token}
clientContext={
getStakableTokensDataForTokenName(token).clientContext
}
showPositionSize={activeFormTab === 'Remove'}
/>
))}
</div>
</EnterBottomExitBottom>
<div
className={`col-span-12 rounded-b-2xl border-2 border-t border-th-fgd-1 bg-th-bkg-1 text-th-fgd-1`}
className={`rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 text-th-fgd-1`}
>
<div className={`p-6 pt-4 md:p-8 md:pt-6`}>
<div className="pb-2">
<TabUnderline
activeValue={activeFormTab}
values={['Add', 'Remove']}
onChange={(v) => setActiveFormTab(v)}
onChange={(v) => handleTabChange(v)}
/>
</div>
{selectedToken == 'USDC' ? (
{selectableTokens.length ? (
<>
{activeFormTab === 'Add' ? <DespositForm token="USDC" /> : null}
{activeFormTab === 'Remove' ? (
<UnstakeForm token="USDC" />
) : null}
<div className="pb-6">
<Label text="Token" />
<TokenButton
onClick={() => setShowTokenSelect(true)}
tokenName={selectedToken}
/>
</div>
{selectedToken == 'USDC' ? (
<>
{activeFormTab === 'Add' ? (
<DespositForm
token="USDC"
clientContext={
getStakableTokensDataForTokenName('USDC')
.clientContext
}
/>
) : null}
{activeFormTab === 'Remove' ? (
<UnstakeForm
token="USDC"
clientContext={
getStakableTokensDataForTokenName('USDC')
.clientContext
}
/>
) : null}
</>
) : (
<>
{activeFormTab === 'Add' ? (
<StakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)
.clientContext
}
/>
) : null}
{activeFormTab === 'Remove' ? (
<UnstakeForm
token={selectedToken}
clientContext={
getStakableTokensDataForTokenName(selectedToken)
.clientContext
}
/>
) : null}
</>
)}
</>
) : (
<>
{activeFormTab === 'Add' ? (
<StakeForm token={selectedToken} />
) : null}
{activeFormTab === 'Remove' ? (
<UnstakeForm token={selectedToken} />
) : null}
</>
<div className="p-10">
<p className="text-center text-th-fgd-4">
No positions to remove
</p>
</div>
)}
</div>
</div>

View File

@ -6,7 +6,10 @@ import {
import { useWallet } from '@solana/wallet-adapter-react'
import { useTranslation } from 'next-i18next'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import NumberFormat, { NumberFormatValues } from 'react-number-format'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
} from 'react-number-format'
import mangoStore from '@store/mangoStore'
import { notify } from '../utils/notifications'
import { TokenAccount, formatTokenSymbol } from '../utils/tokens'
@ -33,7 +36,6 @@ import useMangoGroup from 'hooks/useMangoGroup'
import FormatNumericValue from './shared/FormatNumericValue'
import { getNextAccountNumber, stakeAndCreate } from 'utils/transactions'
// import { MangoAccount } from '@blockworks-foundation/mango-v4'
import { AnchorProvider } from '@project-serum/anchor'
import useBankRates from 'hooks/useBankRates'
import { Disclosure } from '@headlessui/react'
import SheenLoader from './shared/SheenLoader'
@ -43,6 +45,11 @@ import ButtonGroup from './forms/ButtonGroup'
import Decimal from 'decimal.js'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import useIpAddress from 'hooks/useIpAddress'
import {
ClientContextKeys,
JLP_BORROW_TOKEN,
LST_BORROW_TOKEN,
} from 'utils/constants'
const set = mangoStore.getState().set
@ -51,13 +58,15 @@ export const NUMBERFORMAT_CLASSES =
interface StakeFormProps {
token: string
clientContext: ClientContextKeys
}
export const walletBalanceForToken = (
walletTokens: TokenAccount[],
token: string,
clientContext: ClientContextKeys,
): { maxAmount: number; maxDecimals: number } => {
const group = mangoStore.getState().group
const group = mangoStore.getState().group[clientContext]
const bank = group?.banksMapByName.get(token)?.[0]
let walletToken
@ -87,7 +96,7 @@ export const walletBalanceForToken = (
// return 0
// }
function StakeForm({ token: selectedToken }: StakeFormProps) {
function StakeForm({ token: selectedToken, clientContext }: StakeFormProps) {
const { t } = useTranslation(['common', 'account'])
const [inputAmount, setInputAmount] = useState('')
const [sizePercentage, setSizePercentage] = useState('')
@ -99,21 +108,25 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
const storedLeverage = mangoStore((s) => s.leverage)
const { usedTokens, totalTokens } = useMangoAccountAccounts()
const { group } = useMangoGroup()
const { jlpGroup, lstGroup } = useMangoGroup()
const groupLoaded = mangoStore((s) => s.groupLoaded)
const { financialMetrics, borrowBankBorrowRate } = useBankRates(
selectedToken,
leverage,
)
const leverageMax = useLeverageMax(selectedToken) * 0.9 // Multiplied by 0.975 becuase you cant actually get to the end of the inifinite geometric series?
const leverageMax = useLeverageMax(selectedToken)
const stakeBank = useMemo(() => {
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken, group])
const borrowBank = useMemo(() => {
return group?.banksMapByName.get('USDC')?.[0]
}, [group])
const [stakeBank, borrowBank] = useMemo(() => {
const stakeBank =
clientContext === 'jlp'
? jlpGroup?.banksMapByName.get(selectedToken)?.[0]
: lstGroup?.banksMapByName.get(selectedToken)?.[0]
const borrowBank =
clientContext === 'jlp'
? jlpGroup?.banksMapByName.get(JLP_BORROW_TOKEN)?.[0]
: lstGroup?.banksMapByName.get(LST_BORROW_TOKEN)?.[0]
return [stakeBank, borrowBank]
}, [selectedToken, jlpGroup, lstGroup, clientContext])
const liquidationPrice = useMemo(() => {
const price = Number(stakeBank?.uiPrice)
@ -140,12 +153,13 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
const walletTokens = mangoStore((s) => s.wallet.tokens)
const tokenMax = useMemo(() => {
return walletBalanceForToken(walletTokens, selectedToken)
}, [walletTokens, selectedToken])
return walletBalanceForToken(walletTokens, selectedToken, clientContext)
}, [walletTokens, selectedToken, clientContext])
const setMax = useCallback(() => {
const max = floorToDecimal(tokenMax.maxAmount, 6)
setInputAmount(max.toFixed())
setSizePercentage('100')
}, [tokenMax])
const handleSizePercentage = useCallback(
@ -165,13 +179,21 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
const borrowPrice = borrowBank?.uiPrice
const stakePrice = stakeBank?.uiPrice
if (!borrowPrice || !stakePrice || !Number(inputAmount)) return 0
const borrowAmount =
stakeBank?.uiPrice * Number(inputAmount) * (leverage - 1)
return borrowAmount
if (clientContext === 'jlp') {
const borrowAmount =
stakeBank?.uiPrice * Number(inputAmount) * (leverage - 1)
return borrowAmount
} else {
const priceDifference = (stakePrice - borrowPrice) / borrowPrice
const borrowAmount =
(1 + priceDifference) * Number(inputAmount) * Math.min(leverage - 1, 1)
return borrowAmount
}
}, [leverage, borrowBank, stakeBank, inputAmount])
const availableVaultBalance = useMemo(() => {
if (!borrowBank || !group) return 0
if (!borrowBank) return 0
const maxUtilization = 1 - borrowBank.minVaultToDepositsRatio
const vaultBorrows = borrowBank.uiBorrows()
const vaultDeposits = borrowBank.uiDeposits()
@ -180,7 +202,7 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
const available =
(maxUtilization * vaultDeposits - vaultBorrows) * loanOriginationFeeFactor
return available
}, [borrowBank, group])
}, [borrowBank])
const handleRefreshWalletBalances = useCallback(async () => {
if (!publicKey) return
@ -191,18 +213,19 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
}, [publicKey])
const handleDeposit = useCallback(async () => {
if (!ipAllowed) {
if (!ipAllowed || !stakeBank || !publicKey) {
return
}
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const group = mangoStore.getState().group[clientContext]
const client = mangoStore.getState().client[clientContext]
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount.current
const mangoAccounts = mangoStore.getState().mangoAccounts
const accNumber = getNextAccountNumber(mangoAccounts)
if (!group || !stakeBank || !publicKey) return
console.log(mangoAccounts)
if (!group) return
set((state) => {
state.submittingBoost = true
})
@ -230,13 +253,12 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
state.submittingBoost = false
})
setInputAmount('')
setSizePercentage('')
await sleep(500)
if (!mangoAccount) {
await actions.fetchMangoAccounts(
(client.program.provider as AnchorProvider).wallet.publicKey,
)
await actions.fetchMangoAccounts(publicKey)
}
await actions.reloadMangoAccount(slot)
await actions.reloadMangoAccount(clientContext, slot)
await actions.fetchWalletTokens(publicKey)
} catch (e) {
console.error('Error depositing:', e)
@ -251,7 +273,14 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
type: 'error',
})
}
}, [ipAllowed, stakeBank, publicKey, amountToBorrow, inputAmount])
}, [
ipAllowed,
stakeBank,
publicKey,
amountToBorrow,
inputAmount,
clientContext,
])
const showInsufficientBalance =
tokenMax.maxAmount < Number(inputAmount) ||
@ -279,11 +308,11 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
}
useEffect(() => {
const group = mangoStore.getState().group
const group = mangoStore.getState().group[clientContext]
set((state) => {
state.swap.outputBank = group?.banksMapByName.get(selectedToken)?.[0]
})
}, [selectedToken])
}, [selectedToken, clientContext])
return (
<>
@ -338,14 +367,17 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={6}
decimalScale={stakeBank?.mintDecimals || 6}
className={NUMBERFORMAT_CLASSES}
placeholder="0.00"
value={inputAmount}
onValueChange={(e: NumberFormatValues) => {
onValueChange={(e: NumberFormatValues, info: SourceInfo) => {
setInputAmount(
!Number.isNaN(Number(e.value)) ? e.value : '',
)
if (info.source === 'event') {
setSizePercentage('')
}
}}
isAllowed={withValueLimit}
/>
@ -360,12 +392,33 @@ function StakeForm({ token: selectedToken }: StakeFormProps) {
</div>
</div>
<div className="col-span-2 mt-2">
<ButtonGroup
activeValue={sizePercentage}
onChange={(p) => handleSizePercentage(p)}
values={['10', '25', '50', '75', '100']}
unit="%"
/>
{connected && groupLoaded && tokenMax.maxAmount === 0 ? (
<InlineNotification
type="warning"
desc={
<div>
<p>
No {formatTokenSymbol(selectedToken)} balance to Boost!{' '}
<a
className="font-bold"
href={`https://app.mango.markets/swap?in=USDC&out=${selectedToken}&walletSwap=true`}
target="_blank"
rel="noopener noreferrer"
>
Get {formatTokenSymbol(selectedToken)} Now
</a>
</p>
</div>
}
/>
) : (
<ButtonGroup
activeValue={sizePercentage}
onChange={(p) => handleSizePercentage(p)}
values={['10', '25', '50', '75', '100']}
unit="%"
/>
)}
</div>
{depositLimitExceeded ? (
<div className="col-span-2 mt-2">

View File

@ -4,17 +4,16 @@ 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'
const TokenButton = ({
handleTokenSelect,
selectedToken,
onClick,
tokenName,
}: {
tokenName: string
selectedToken: string
handleTokenSelect: (v: string) => void
onClick: () => void
}) => {
const leverage = useLeverageMax(tokenName) * 0.9
const leverage = useLeverageMax(tokenName)
const groupLoaded = mangoStore((s) => s.groupLoaded)
const { stakeBankDepositRate, financialMetrics } = useBankRates(
@ -36,54 +35,41 @@ const TokenButton = ({
return (
<button
className={`col-span-1 flex items-center justify-center border-r border-th-fgd-1 p-4 first:rounded-tl-[13px] last:rounded-tr-[13px] last:border-r-0 hover:cursor-pointer ${
selectedToken === tokenName
? 'inner-shadow-top bg-th-active'
: 'inner-shadow-bottom default-transition bg-th-bkg-1 md:hover:bg-th-bkg-2'
}`}
onClick={() => handleTokenSelect(tokenName)}
className={`inner-shadow-bottom-sm w-full rounded-xl border border-th-bkg-3 bg-th-bkg-1 p-3 text-th-fgd-1 focus:outline-none focus-visible:border-th-fgd-4 md:hover:border-th-bkg-4 md:hover:focus-visible:border-th-fgd-4`}
onClick={onClick}
>
<div className="flex flex-col items-center">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full border ${
selectedToken === tokenName
? 'inner-shadow-top-sm border-th-bkg-1 bg-gradient-to-b from-th-active to-th-active-dark'
: 'inner-shadow-bottom-sm border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2'
}`}
>
<Image
src={`/icons/${tokenName.toLowerCase()}.svg`}
width={24}
height={24}
alt="Select a token"
/>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div
className={`inner-shadow-bottom-sm flex h-12 w-12 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`}
width={24}
height={24}
alt="Select a token"
/>
</div>
<div className="text-left">
<p className={`text-lg font-bold text-th-fgd-1`}>
{formatTokenSymbol(tokenName)}
</p>
<span className={`font-medium text-th-fgd-4`}>
{!groupLoaded ? (
<SheenLoader>
<div className={`h-5 w-10 bg-th-bkg-2`} />
</SheenLoader>
) : !UiRate || isNaN(UiRate) ? (
'Rate Unavailable'
) : tokenName === 'USDC' ? (
`${UiRate.toFixed(2)}% APY`
) : (
`Up to ${UiRate.toFixed(2)}% APY`
)}
</span>
</div>
</div>
<span className={`mt-1 text-lg font-bold text-th-fgd-1`}>
{formatTokenSymbol(tokenName)}
</span>
<span
className={`font-medium ${
selectedToken === tokenName ? 'text-th-fgd-1' : 'text-th-fgd-4'
}`}
>
{!groupLoaded ? (
<SheenLoader>
<div
className={`h-5 w-10 ${
selectedToken === tokenName
? 'bg-th-active-dark'
: 'bg-th-bkg-2'
}`}
/>
</SheenLoader>
) : !UiRate || isNaN(UiRate) ? (
'Rate Unavailable'
) : tokenName === 'USDC' ? (
`${UiRate.toFixed(2)}% APY`
) : (
`Up to ${UiRate.toFixed(2)}% APY`
)}
</span>
<ChevronDownIcon className="h-6 w-6" />
</div>
</button>
)

133
components/TokenSelect.tsx Normal file
View File

@ -0,0 +1,133 @@
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'
import FormatNumericValue from './shared/FormatNumericValue'
import { walletBalanceForToken } from './StakeForm'
import usePositions from 'hooks/usePositions'
import { ClientContextKeys } from 'utils/constants'
const TokenSelect = ({
onClick,
tokenName,
clientContext,
showPositionSize,
}: {
tokenName: string
onClick: () => void
clientContext: ClientContextKeys
showPositionSize?: boolean
}) => {
const leverage = useLeverageMax(tokenName)
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])
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 (
<button
className="default-transition w-full rounded-lg p-3 md:hover:bg-th-bkg-2"
onClick={onClick}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div
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`}
width={24}
height={24}
alt="Select a token"
/>
</div>
<div className="text-left">
<p className={`text-sm font-bold text-th-fgd-1 sm:text-lg`}>
{formatTokenSymbol(tokenName)}
</p>
<span className={`text-sm text-th-fgd-4`}>
{!groupLoaded ? (
<SheenLoader>
<div className={`h-5 w-10 bg-th-bkg-2`} />
</SheenLoader>
) : !UiRate || isNaN(UiRate) ? (
'Rate Unavailable'
) : tokenName === 'USDC' ? (
`${UiRate.toFixed(2)}% APY`
) : (
`Up to ${UiRate.toFixed(2)}% APY`
)}
</span>
</div>
</div>
<div className="pl-3 text-right">
{showPositionSize ? (
position ? (
<>
<span className="text-sm font-bold text-th-fgd-1 sm:text-lg">
<FormatNumericValue
value={
position.stakeBalance *
(position.bank.name != 'USDC'
? position.bank?.uiPrice
: 1)
}
decimals={2}
/>{' '}
{'USDC'}
</span>
{position.bank.name !== 'USDC' ? (
<p className="text-sm text-th-fgd-4">
<FormatNumericValue
roundUp={true}
value={position.stakeBalance}
decimals={3}
/>{' '}
{formatTokenSymbol(position.bank.name)}
</p>
) : null}
</>
) : (
''
)
) : (
<FormatNumericValue
value={walletBalance.maxAmount}
decimals={walletBalance.maxDecimals}
/>
)}
</div>
</div>
</button>
)
}
export default TokenSelect

View File

@ -6,10 +6,13 @@ import {
import { useWallet } from '@solana/wallet-adapter-react'
import { useTranslation } from 'next-i18next'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import NumberFormat, { NumberFormatValues } from 'react-number-format'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
} from 'react-number-format'
import mangoStore from '@store/mangoStore'
import { notify } from '../utils/notifications'
import { TokenAccount, formatTokenSymbol } from '../utils/tokens'
import { formatTokenSymbol } from '../utils/tokens'
// import ActionTokenList from './account/ActionTokenList'
import Label from './forms/Label'
import Button, { IconButton } from './shared/Button'
@ -38,58 +41,45 @@ import Decimal from 'decimal.js'
import { Disclosure } from '@headlessui/react'
import { sleep } from 'utils'
import useIpAddress from 'hooks/useIpAddress'
import { AnchorProvider } from '@project-serum/anchor'
import {
ClientContextKeys,
JLP_BORROW_TOKEN,
LST_BORROW_TOKEN,
} from 'utils/constants'
const set = mangoStore.getState().set
interface UnstakeFormProps {
token: string
clientContext: ClientContextKeys
}
export const walletBalanceForToken = (
walletTokens: TokenAccount[],
token: string,
): { maxAmount: number; maxDecimals: number } => {
const group = mangoStore.getState().group
const bank = group?.banksMapByName.get(token)?.[0]
let walletToken
if (bank) {
const tokenMint = bank?.mint
walletToken = tokenMint
? walletTokens.find((t) => t.mint.toString() === tokenMint.toString())
: null
}
return {
maxAmount: walletToken ? walletToken.uiAmount : 0,
maxDecimals: bank?.mintDecimals || 6,
}
}
function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
function UnstakeForm({
token: selectedToken,
clientContext,
}: UnstakeFormProps) {
const { t } = useTranslation(['common', 'account'])
const [inputAmount, setInputAmount] = useState('')
const [submitting, setSubmitting] = useState(false)
// const [selectedToken, setSelectedToken] = useState(
// token || INPUT_TOKEN_DEFAULT,
// )
const [refreshingWalletTokens, setRefreshingWalletTokens] = useState(false)
const [sizePercentage, setSizePercentage] = useState('')
const { maxSolDeposit } = useSolBalance()
// const banks = useBanksWithBalances('walletBalance')
const { usedTokens, totalTokens } = useMangoAccountAccounts()
const { group } = useMangoGroup()
const { jlpGroup, lstGroup } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const { ipAllowed } = useIpAddress()
const stakeBank = useMemo(() => {
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken, group])
const borrowBank = useMemo(() => {
return group?.banksMapByName.get('USDC')?.[0]
}, [group])
const [stakeBank, borrowBank] = useMemo(() => {
const stakeBank =
clientContext === 'jlp'
? jlpGroup?.banksMapByName.get(selectedToken)?.[0]
: lstGroup?.banksMapByName.get(selectedToken)?.[0]
const borrowBank =
clientContext === 'jlp'
? jlpGroup?.banksMapByName.get(JLP_BORROW_TOKEN)?.[0]
: lstGroup?.banksMapByName.get(LST_BORROW_TOKEN)?.[0]
return [stakeBank, borrowBank]
}, [selectedToken, jlpGroup, lstGroup, clientContext])
const tokenPositionsFull = useMemo(() => {
if (!stakeBank || !usedTokens.length || !totalTokens.length) return false
@ -114,10 +104,11 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
borrowBankAmount &&
borrowBankAmount.toNumber() < 0
) {
const lev = stakeBankAmount
const stakeAmountValue = stakeBankAmount.mul(stakeBank.getAssetPrice())
const lev = stakeAmountValue
.div(
stakeBankAmount.sub(
borrowBankAmount.abs().div(stakeBank.getAssetPrice()),
stakeAmountValue.sub(
borrowBankAmount.abs().mul(borrowBank.getAssetPrice()),
),
)
.toNumber()
@ -129,7 +120,7 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
console.log(e)
return 1
}
}, [stakeBankAmount, borrowBankAmount, stakeBank])
}, [stakeBankAmount, borrowBankAmount, stakeBank, borrowBank])
const tokenMax = useMemo(() => {
if (!stakeBank || !mangoAccount) return { maxAmount: 0.0, maxDecimals: 6 }
@ -142,6 +133,7 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
const setMax = useCallback(() => {
const max = floorToDecimal(tokenMax.maxAmount, tokenMax.maxDecimals)
setInputAmount(max.toFixed())
setSizePercentage('100')
}, [tokenMax])
const handleSizePercentage = useCallback(
@ -157,11 +149,6 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
[tokenMax, stakeBank],
)
// const handleSelectToken = (token: string) => {
// setSelectedToken(token)
// setShowTokenList(false)
// }
const handleRefreshWalletBalances = useCallback(async () => {
if (!publicKey) return
const actions = mangoStore.getState().actions
@ -176,16 +163,15 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
}, [borrowBank, mangoAccount])
const handleWithdraw = useCallback(async () => {
if (!ipAllowed) {
if (!ipAllowed || !stakeBank || !borrowBank || !publicKey) {
return
}
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const group = mangoStore.getState().group[clientContext]
const actions = mangoStore.getState().actions
let mangoAccount = mangoStore.getState().mangoAccount.current
if (!group || !stakeBank || !borrowBank || !publicKey || !mangoAccount)
return
if (!group || !mangoAccount) return
setSubmitting(true)
try {
@ -202,7 +188,7 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
const stakeAmountToRepay = (leverage - 1) * Number(inputAmount)
const { signature: tx } = await unstakeAndSwap(
client,
client[clientContext],
group,
mangoAccount,
stakeBank.mint,
@ -215,10 +201,8 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
txid: tx,
})
await sleep(100)
await actions.fetchMangoAccounts(
(client.program.provider as AnchorProvider).wallet.publicKey,
)
await actions.reloadMangoAccount()
await actions.fetchMangoAccounts(publicKey)
await actions.reloadMangoAccount(clientContext)
await actions.fetchWalletTokens(publicKey)
mangoAccount = mangoStore.getState().mangoAccount.current
notify({
@ -228,7 +212,7 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
}
if (!mangoAccount) return
const { signature: tx2 } = await withdrawAndClose(
client,
client[clientContext],
group,
mangoAccount,
stakeBank.mint,
@ -241,11 +225,10 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
})
setSubmitting(false)
setInputAmount('')
setSizePercentage('')
await sleep(100)
await actions.fetchMangoAccounts(
(client.program.provider as AnchorProvider).wallet.publicKey,
)
await actions.reloadMangoAccount()
await actions.fetchMangoAccounts(publicKey)
await actions.reloadMangoAccount(clientContext)
await actions.fetchWalletTokens(publicKey)
} catch (e) {
console.error('Error withdrawing:', e)
@ -263,28 +246,58 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
type: 'error',
})
}
}, [ipAllowed, stakeBank, borrowBank, publicKey, inputAmount, leverage])
}, [
ipAllowed,
stakeBank,
borrowBank,
publicKey,
inputAmount,
leverage,
clientContext,
])
const maxWithdraw =
group && mangoAccount && stakeBank
? mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, stakeBank.mint)
: 0
const maxWithdraw = useMemo(() => {
if (!mangoAccount || !stakeBank) return 0
const group = clientContext === 'jlp' ? jlpGroup : lstGroup
if (!group) return 0
try {
return mangoAccount.getMaxWithdrawWithBorrowForTokenUi(
group,
stakeBank.mint,
)
} catch (e) {
return 0
}
}, [jlpGroup, lstGroup, mangoAccount, stakeBank, clientContext])
const availableVaultBalance = useMemo(() => {
if (!stakeBank) return 0
const group = clientContext === 'jlp' ? jlpGroup : lstGroup
if (!group) return 0
const vaultBalance = group.getTokenVaultBalanceByMintUi(stakeBank.mint)
const vaultDeposits = stakeBank.uiDeposits()
const available =
vaultBalance - vaultDeposits * stakeBank.minVaultToDepositsRatio
return available
}, [stakeBank, jlpGroup, lstGroup, clientContext])
const showInsufficientBalance =
tokenMax.maxAmount < Number(inputAmount) ||
(selectedToken === 'USDC' && maxSolDeposit <= 0)
const lowVaultBalance =
Math.floor(tokenMax.maxAmount * 100000) <
Math.floor(Number(inputAmount) * 100000) &&
Number(inputAmount) > maxWithdraw
const lowVaultBalance = maxWithdraw > availableVaultBalance
// const lowVaultBalance =
// Math.floor(tokenMax.maxAmount * 100000) <
// Math.floor(Number(inputAmount) * 100000) &&
// Number(inputAmount) > maxWithdraw
useEffect(() => {
const group = mangoStore.getState().group
const group = mangoStore.getState().group[clientContext]
set((state) => {
state.swap.outputBank = group?.banksMapByName.get(selectedToken)?.[0]
})
}, [selectedToken])
}, [selectedToken, clientContext])
return (
<>
@ -330,10 +343,13 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
className={NUMBERFORMAT_CLASSES}
placeholder="0.00"
value={inputAmount}
onValueChange={(e: NumberFormatValues) => {
onValueChange={(e: NumberFormatValues, info: SourceInfo) => {
setInputAmount(
!Number.isNaN(Number(e.value)) ? e.value : '',
)
if (info.source === 'event') {
setSizePercentage('')
}
}}
isAllowed={withValueLimit}
/>
@ -419,7 +435,9 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
</span>
</div>
<div className="flex justify-between">
<p className="text-th-fgd-4">USDC borrowed</p>
<p className="text-th-fgd-4">
{borrowBank.name} borrowed
</p>
{borrowBank ? (
<span
className={`font-bold ${
@ -487,7 +505,7 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
<div className="mt-4">
<InlineNotification
type="error"
desc={`The ${selectedToken} vault balance is too low. ${selectedToken} deposits are required to unboost.`}
desc={`The available ${selectedToken} vault balance is low. ${selectedToken} deposits are required to unboost your full position.`}
/>
</div>
) : null}

View File

@ -1,407 +0,0 @@
import {
ArrowPathIcon,
ChevronDownIcon,
ExclamationCircleIcon,
} from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
import { useTranslation } from 'next-i18next'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import NumberFormat, { NumberFormatValues } from 'react-number-format'
import mangoStore from '@store/mangoStore'
import { notify } from '../utils/notifications'
import { TokenAccount, formatTokenSymbol } from '../utils/tokens'
// import ActionTokenList from './account/ActionTokenList'
import Label from './forms/Label'
import Button, { IconButton } from './shared/Button'
import Loading from './shared/Loading'
import MaxAmountButton from '@components/shared/MaxAmountButton'
import Tooltip from '@components/shared/Tooltip'
import SolBalanceWarnings from '@components/shared/SolBalanceWarnings'
import useSolBalance from 'hooks/useSolBalance'
import { floorToDecimal, withValueLimit } from 'utils/numbers'
import BankAmountWithValue from './shared/BankAmountWithValue'
// import useBanksWithBalances from 'hooks/useBanksWithBalances'
import { isMangoError } from 'types'
// import TokenListButton from './shared/TokenListButton'
import TokenLogo from './shared/TokenLogo'
import SecondaryConnectButton from './shared/SecondaryConnectButton'
import useMangoAccountAccounts from 'hooks/useMangoAccountAccounts'
import InlineNotification from './shared/InlineNotification'
import Link from 'next/link'
import useMangoGroup from 'hooks/useMangoGroup'
import FormatNumericValue from './shared/FormatNumericValue'
import useMangoAccount from 'hooks/useMangoAccount'
import { unstakeAndSwap, withdrawAndClose } from 'utils/transactions'
import { NUMBERFORMAT_CLASSES } from './StakeForm'
import ButtonGroup from './forms/ButtonGroup'
import Decimal from 'decimal.js'
import { Disclosure } from '@headlessui/react'
import { sleep } from 'utils'
import useIpAddress from 'hooks/useIpAddress'
const set = mangoStore.getState().set
interface UnstakeFormProps {
token: string
}
export const walletBalanceForToken = (
walletTokens: TokenAccount[],
token: string,
): { maxAmount: number; maxDecimals: number } => {
const group = mangoStore.getState().group
const bank = group?.banksMapByName.get(token)?.[0]
let walletToken
if (bank) {
const tokenMint = bank?.mint
walletToken = tokenMint
? walletTokens.find((t) => t.mint.toString() === tokenMint.toString())
: null
}
return {
maxAmount: walletToken ? walletToken.uiAmount : 0,
maxDecimals: bank?.mintDecimals || 6,
}
}
function WithdrawForm({ token: selectedToken }: UnstakeFormProps) {
const { t } = useTranslation(['common', 'account'])
const [inputAmount, setInputAmount] = useState('')
const [submitting, setSubmitting] = useState(false)
const { ipAllowed } = useIpAddress()
// const [selectedToken, setSelectedToken] = useState(
// token || INPUT_TOKEN_DEFAULT,
// )
const [refreshingWalletTokens, setRefreshingWalletTokens] = useState(false)
const [sizePercentage, setSizePercentage] = useState('')
const { maxSolDeposit } = useSolBalance()
// const banks = useBanksWithBalances('walletBalance')
const { usedTokens, totalTokens } = useMangoAccountAccounts()
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const stakeBank = useMemo(() => {
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken, group])
const borrowBank = useMemo(() => {
return group?.banksMapByName.get('USDC')?.[0]
}, [group])
const tokenPositionsFull = useMemo(() => {
if (!stakeBank || !usedTokens.length || !totalTokens.length) return false
const hasTokenPosition = usedTokens.find(
(token) => token.tokenIndex === stakeBank.tokenIndex,
)
return hasTokenPosition ? false : usedTokens.length >= totalTokens.length
}, [stakeBank, usedTokens, totalTokens])
const { connected, publicKey } = useWallet()
const tokenMax = useMemo(() => {
if (!stakeBank || !mangoAccount) return { maxAmount: 0.0, maxDecimals: 6 }
return {
maxAmount: mangoAccount.getTokenBalanceUi(stakeBank),
maxDecimals: stakeBank.mintDecimals,
}
}, [stakeBank, mangoAccount])
const setMax = useCallback(() => {
const max = floorToDecimal(tokenMax.maxAmount, tokenMax.maxDecimals)
setInputAmount(max.toFixed())
}, [tokenMax])
const handleSizePercentage = useCallback(
(percentage: string) => {
if (!stakeBank) return
setSizePercentage(percentage)
const amount = floorToDecimal(
new Decimal(percentage).div(100).mul(tokenMax.maxAmount),
stakeBank.mintDecimals,
)
setInputAmount(amount.toFixed())
},
[tokenMax, stakeBank],
)
// const handleSelectToken = (token: string) => {
// setSelectedToken(token)
// setShowTokenList(false)
// }
const handleRefreshWalletBalances = useCallback(async () => {
if (!publicKey) return
const actions = mangoStore.getState().actions
setRefreshingWalletTokens(true)
await actions.fetchMangoAccounts(publicKey)
setRefreshingWalletTokens(false)
}, [publicKey])
const borrowed = useMemo(() => {
if (!borrowBank || !mangoAccount) return 0.0
return mangoAccount.getTokenBalanceUi(borrowBank)
}, [borrowBank, mangoAccount])
const handleWithdraw = useCallback(async () => {
if (!ipAllowed) {
return
}
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
let mangoAccount = mangoStore.getState().mangoAccount.current
if (!group || !stakeBank || !borrowBank || !publicKey || !mangoAccount)
return
setSubmitting(true)
try {
if (mangoAccount.getTokenBalanceUi(borrowBank) < 0) {
notify({
title: 'Sending transaction 1 of 2',
type: 'info',
})
console.log(
'unstake and swap',
mangoAccount.getTokenBalanceUi(borrowBank),
)
const { signature: tx } = await unstakeAndSwap(
client,
group,
mangoAccount,
stakeBank.mint,
)
notify({
title: 'Swap Transaction confirmed.',
type: 'success',
txid: tx,
})
await sleep(300)
await actions.fetchMangoAccounts(mangoAccount.owner)
await actions.fetchWalletTokens(publicKey)
mangoAccount = mangoStore.getState().mangoAccount.current
notify({
title: 'Sending transaction 2 of 2',
type: 'info',
})
}
if (!mangoAccount) return
const { signature: tx2 } = await withdrawAndClose(
client,
group,
mangoAccount,
stakeBank.mint,
Number(inputAmount),
)
notify({
title: 'Withdraw transaction confirmed.',
type: 'success',
txid: tx2,
})
setSubmitting(false)
setInputAmount('')
await sleep(500)
await actions.fetchMangoAccounts(mangoAccount.owner)
await actions.fetchWalletTokens(publicKey)
} catch (e) {
console.error('Error depositing:', e)
setSubmitting(false)
if (!isMangoError(e)) return
notify({
title: 'Transaction failed',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
}, [stakeBank, publicKey, inputAmount])
const showInsufficientBalance =
tokenMax.maxAmount < Number(inputAmount) ||
(selectedToken === 'SOL' && maxSolDeposit <= 0)
useEffect(() => {
const group = mangoStore.getState().group
set((state) => {
state.swap.outputBank = group?.banksMapByName.get(selectedToken)?.[0]
})
}, [selectedToken])
return (
<>
<div className="flex flex-col justify-between">
<div className="pb-8">
<SolBalanceWarnings
amount={inputAmount}
className="mb-4"
setAmount={setInputAmount}
selectedToken={selectedToken}
/>
<div className="grid grid-cols-2">
<div className="col-span-2 flex justify-between">
<Label text="Amount" />
<div className="mb-2 flex items-center space-x-2">
<MaxAmountButton
decimals={tokenMax.maxDecimals}
label={t('balance')}
onClick={setMax}
value={tokenMax.maxAmount}
/>
<Tooltip content="Refresh Balance">
<IconButton
className={refreshingWalletTokens ? 'animate-spin' : ''}
onClick={handleRefreshWalletBalances}
hideBg
>
<ArrowPathIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
</div>
</div>
<div className="col-span-2">
<div className="relative">
<NumberFormat
name="amountIn"
id="amountIn"
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={stakeBank?.mintDecimals || 6}
className={NUMBERFORMAT_CLASSES}
placeholder="0.00"
value={inputAmount}
onValueChange={(e: NumberFormatValues) => {
setInputAmount(
!Number.isNaN(Number(e.value)) ? e.value : '',
)
}}
isAllowed={withValueLimit}
/>
<div className="absolute left-4 top-1/2 -translate-y-1/2">
<TokenLogo bank={stakeBank} size={24} />
</div>
<div className="absolute right-4 top-1/2 -translate-y-1/2">
<span className="font-bold text-th-fgd-1">
{formatTokenSymbol(selectedToken)}
</span>
</div>
</div>
</div>
<div className="col-span-2 mt-2">
<ButtonGroup
activeValue={sizePercentage}
onChange={(p) => handleSizePercentage(p)}
values={['10', '25', '50', '75', '100']}
unit="%"
/>
</div>
</div>
{stakeBank && borrowBank ? (
<div className="pt-8">
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button
className={`w-full rounded-xl border-2 border-th-bkg-3 px-4 py-3 text-left focus:outline-none ${
open ? 'rounded-b-none border-b-0' : ''
}`}
>
<div className="flex items-center justify-between">
<p className="font-medium">Staked Amount</p>
<div className="flex items-center space-x-2">
<span className="text-lg font-bold text-th-fgd-1">
<FormatNumericValue
value={tokenMax.maxAmount}
decimals={stakeBank.mintDecimals}
/>
</span>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : ''
} h-6 w-6 shrink-0 text-th-fgd-1`}
/>
</div>
</div>
</Disclosure.Button>
<Disclosure.Panel className="space-y-2 rounded-xl rounded-t-none border-2 border-t-0 border-th-bkg-3 px-4 pb-3">
<div className="flex justify-between">
<p className="text-th-fgd-4">Staked Amount</p>
<span className="font-bold text-th-fgd-1">
<BankAmountWithValue
amount={tokenMax.maxAmount}
bank={stakeBank}
/>
</span>
</div>
<div className="flex justify-between">
<p className="text-th-fgd-4">USDC borrowed</p>
{borrowBank ? (
<span
className={`font-bold ${
borrowed > 0.001
? 'text-th-fgd-1'
: 'text-th-bkg-4'
}`}
>
<FormatNumericValue value={borrowed} decimals={3} />
</span>
) : null}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
) : null}
</div>
{connected ? (
<Button
onClick={handleWithdraw}
className="w-full"
disabled={
connected &&
(!inputAmount || showInsufficientBalance || !ipAllowed)
}
size="large"
>
{submitting ? (
<Loading className="mr-2 h-5 w-5" />
) : showInsufficientBalance ? (
<div className="flex items-center">
<ExclamationCircleIcon className="icon-shadow mr-2 h-5 w-5 shrink-0" />
{t('swap:insufficient-balance', {
symbol: formatTokenSymbol(selectedToken),
})}
</div>
) : ipAllowed ? (
`Boost! ${inputAmount} ${formatTokenSymbol(selectedToken)}`
) : (
'Country not allowed'
)}
</Button>
) : (
<SecondaryConnectButton className="w-full" isLarge />
)}
{tokenPositionsFull ? (
<InlineNotification
type="error"
desc={
<>
{t('error-token-positions-full')}{' '}
<Link href="/settings" shallow>
{t('manage')}
</Link>
</>
}
/>
) : null}
</div>
</>
)
}
export default WithdrawForm

View File

@ -2,26 +2,174 @@ import { Disclosure } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
const FAQS = [
{
question: 'Why would I want to Boost!?',
answer: (
<>
<h3>JLP/Liquid staking tokens</h3>
<p>
Boost! let&apos;s you easily add leverage to your token positions. The
tokens listed on Boost! (excluding USDC) all have native yield so in
the right market conditions you can borrow to increase your exposure
to this native yield. When the extra yield is larger than your cost of
borrowing you earn more yield than you would by simply holding the
token.
</p>
<h3>USDC</h3>
<p>
Boosting USDC is different in that when you deposit USDC you are
adding it to the lending pool for JLP boosters. There is no leverage
involved and JLP borrowers pay a variable interest rate to borrow your
USDC.
</p>
</>
),
},
{
question: 'How does Boost! work?',
answer: (
<p>
Boost! allows you to increase your position size by borrowing USDC and
swapping it for JLP. This means you earn more yield from JLP due to a
larger position size. As long as this yield exceeds the rate of the USDC
borrow and collateral fees, you earn a premium.
</p>
<>
<h3>JLP</h3>
<p>
Boosting JLP works by using your deposited JLP as collateral to borrow
USDC which is then swapped to JLP. This leaves you with an increased
balance of JLP and a borrowed amount of USDC.
</p>
<p>The JLP pool is completely isolated from Mango v4.</p>
<h3>Liquid staking tokens (LSTs)</h3>
<p>
Boosting liquid staking tokens (mSOL, JitoSOL, bSOL etc) works by
using your deposited token as collateral to borrow SOL which is then
swapped to more of your deposited token. This leaves you with an
increased balance of your LST and a borrowed amount of USDC.
</p>
<p>
The pools for LSTs on Boost! draw from the same liquidity available on
Mango v4.
</p>
<h3>USDC</h3>
<p>
USDC is part of the isolated JLP group. When you deposit USDC it will
be lent out to JLP boosters. You earn a varialbe interest rate in
return that is determined by the amount of USDC deposited and the
amount borrowed.
</p>
</>
),
},
{
question: 'How does unboosting work?',
answer: (
<p>
Unboosting works by selling some of your JLP token to repay your USDC
borrow and withdrawing to your wallet. If the JLP token price increases
enough to cover your borrow fee and collateral fee, you will earn a
higher APY over time.
</p>
<>
<h3>JLP/Liquid staking tokens</h3>
<p>
Unboosting works by unwinding your leveraged position. Your borrow
will be repaid by swapping the token you boosted to the token you
borrowed with the remainder being withdrawn to your wallet.
</p>
<p>
There are no fees associated with unboosting however, there could be
up to 1% slippage when swapping to repay your loan.
</p>
<h3>USDC</h3>
<p>
Unboosting USDC removes it from the lending pool and withdraws it to
your wallet.
</p>
<p>There are no fees associated with unboosting USDC.</p>
</>
),
},
{
question: 'Is boosting always profitalbe?',
answer: (
<>
<h3>JLP/Liquid staking tokens</h3>
<p>
No. For one, there is a risk of liquidation (especially when boosting
JLP). If the price of your boosted token drops below your liquidation
threshold you will lose some or all of your funds. This risk increases
with the amount of leverage you use.
</p>
<p>
There are also fees and costs for borrowing that will affect your
positions profitability. To earn more yield than simply holding JLP or
an LST the cost of borrowing needs to be less than the additional
yield you earn.
</p>
<h3>USDC</h3>
<p>
Boosting USDC is always profitable unless there is a systemic failure
that results in loss of funds. See the risks FAQ to learn about some
of the potential risks of using Boost!
</p>
</>
),
},
{
question: 'What are the costs/fees?',
answer: (
<>
<p>The costs and fees depend on the token you are boosting.</p>
<h3>JLP/Liquid staking tokens</h3>
<p className="font-bold">Borrow Interest Rate</p>
<p>
This variable APR can change significantly and frequently depending on
the ratio of deposits and borrows. It is charged continuosly on the
balance of your USDC or SOL borrow and paid to USDC depositors
(lenders) on Boost! and SOL depositors on Mango v4.
</p>
<p className="font-bold">Loan Origination Fee</p>
<p>
This is a one-time, 50 basis points (0.5%) fee applied to the total
balance of your borrow and paid to Mango DAO.
</p>
<p className="font-bold">Collateral Fee (JLP Only)</p>
<p>
This is charged on your JLP collateral once every two days as
insurance for JLP suffering a catastrophic failure resulting in bad
debt. It will reduce the size of your JLP position over time. The fee
accrues to Mango DAO.
</p>
<p>
The collateral fee is a dynamic formula that uses a fixed Annual
Percentage Rate (APR) of 41%. This rate is then multiplied by the
ratio of your USDC liabilities (the amount you&apos;ve borrowed)
against your &quot;weighted&quot; JLP deposits (the value of your
position adjusted by a factor between 0 and 1). The JLP weight is
currently set at 0.9.
</p>
<p>
The key aspect of this fee is its dynamism; it scales with your
position&apos;s proximity to the liquidation price. Positions closer
to liquidation are subjected to a higher fee, reflecting increased
risk, while positions further from liquidation incur a lower fee.
Consequently, the more leverage you take on the more collateral fees
you&apos;ll pay.
</p>
<p className="font-bold">Position Entry Costs</p>
<p>
When boosting the USDC or SOL you borrow gets swapped via Jupiter to
more of your boosted token. This can incur up to 1% slippage resulting
in an entry price worse than expected.
</p>
<h3>USDC</h3>
<p>There are no fees associated with boosting USDC.</p>
</>
),
},
{
question: 'Why is my "Total Earned" negative?',
answer: (
<>
<p>
When you open a leveraged position there are some immediate costs
associated with borrowing. You&apos;ll be paying a loan origination
fee, interest on the borrowed amount, and a collateral fee (if
boosting JLP) instantaneously. Over time and in the right market
conditions your &quot;Total Earned&quot; will become positive.
</p>
</>
),
},
{
@ -33,7 +181,7 @@ const FAQS = [
good understanding of these risks and how Boost! works before
depositing any funds
</p>
<h4>Code</h4>
<h3>Code</h3>
<p>
Boost! is an integration with the Mango v4 program. Although it is
open source and has been audited extensively, it&apos;s possible bugs
@ -41,14 +189,14 @@ const FAQS = [
also possible for a bug in the UI to affect the ability to open and
close positions in a timely manner.
</p>
<h4>Price Depeg</h4>
<h3>Price Depeg</h3>
<p>
It&apos;s possible for the staking token price to diverge
significantly from the USDC price. A large drop in price could result
in postions being liquidated. Positions with higher leverage are more
exposed to this risk.
</p>
<h4>Liquidity</h4>
<h3>Liquidity</h3>
<p>
Opening and closing positions on Boost! relies on swapping between the
staking tokens and USDC without significant price impact. During an
@ -56,20 +204,21 @@ const FAQS = [
effectively. This could affect the liquidity available to open/close
positions.
</p>
<h4>Oracles</h4>
<h3>Oracles</h3>
<p>
The price data for Boost! comes from third party oracle providers.
It&apos;s possible for the data to be incorrect due to a failure with
the oracle provider. This could result in bad liquidations and loss of
funds.
</p>
<h4>Yield Duration</h4>
<h3>Yield Duration</h3>
<p>
When you borrow USDC to open a position on Boost! you&apos;ll be
paying an initial loan origination fee, interest on the borrowed
amount, and a collateral fee instantaneously. This means you could
open a position and close it before earning any additional yeild,
whilst paying interest and collateral fees to borrow USDC.
When you borrow USDC or SOL to open a position on Boost! you&apos;ll
be paying an initial loan origination fee, interest on the borrowed
amount, and a collateral fee (if boosting JLP) instantaneously. This
means you could open a position and close it before earning any
additional yeild, whilst paying interest and collateral fees to borrow
USDC or SOL.
</p>
</>
),
@ -256,7 +405,7 @@ const FaqsPage = () => {
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} h-6 w-6 flex-shrink-0 text-th-fgd-1`}
} h-6 w-6 shrink-0 text-th-fgd-1`}
/>
</div>
</Disclosure.Button>

View File

@ -44,7 +44,7 @@ const InlineNotification: FunctionComponent<InlineNotificationProps> = ({
: type === 'info'
? 'text-th-bkg-4'
: 'text-th-warning'
} flex items-center rounded-md ${!hidePadding ? 'p-2' : ''}`}
} flex items-center rounded-lg ${!hidePadding ? 'p-3' : ''}`}
>
{type === 'error' ? (
<ExclamationCircleIcon className={`${iconClasses} text-th-error`} />
@ -60,9 +60,7 @@ const InlineNotification: FunctionComponent<InlineNotificationProps> = ({
) : null}
<div>
<div className="text-th-fgd-2">{title}</div>
<div
className={`${title && desc && 'pt-1'} text-left text-xs font-normal`}
>
<div className={`${title && desc && 'pt-1'} text-left font-normal`}>
{desc}
</div>
</div>

View File

@ -13,7 +13,6 @@ const TokenLogo = ({
size?: number
}) => {
const { mangoTokens } = useJupiterMints()
const logoUri = useMemo(() => {
if (!bank) return ''
const tokenSymbol = bank.name.toLowerCase()

View File

@ -5,18 +5,18 @@ import { fetchTokenStatsData } from 'utils/stats'
import TokenRatesChart from './TokenRatesChart'
const HistoricalStats = () => {
const { group } = useMangoGroup()
const { jlpGroup } = useMangoGroup()
const [depositDaysToShow, setDepositDaysToShow] = useState('30')
const { data: historicalStats, isLoading: loadingHistoricalStats } = useQuery(
['historical-stats'],
() => fetchTokenStatsData(group),
() => fetchTokenStatsData(jlpGroup),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!group,
enabled: !!jlpGroup,
},
)

View File

@ -1,145 +1,37 @@
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import TokenLogo from '@components/shared/TokenLogo'
import useMangoGroup from 'hooks/useMangoGroup'
import { useViewport } from 'hooks/useViewport'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { STAKEABLE_TOKENS } from 'utils/constants'
import { formatCurrencyValue } from 'utils/numbers'
import { formatTokenSymbol } from 'utils/tokens'
import HistoricalStats from './HistoricalStats'
import StatsTable from './StatsTable'
const StatsPage = () => {
const { group } = useMangoGroup()
const { t } = useTranslation('common')
const { isMobile } = useViewport()
const { jlpGroup, lstGroup } = useMangoGroup()
const banks = useMemo(() => {
if (!group) return []
const positionBanks = []
const [jlpBanks, lstBanks] = useMemo(() => {
const jlpBanks = []
const lstBanks = []
for (const token of STAKEABLE_TOKENS) {
const bank = group.banksMapByName.get(token)?.[0]
const isJlpGroup = token === 'JLP' || token === 'USDC'
const bank = isJlpGroup
? jlpGroup?.banksMapByName.get(token)?.[0]
: lstGroup?.banksMapByName.get(token)?.[0]
if (bank !== undefined) {
positionBanks.push(bank)
isJlpGroup ? jlpBanks.push(bank) : lstBanks.push(bank)
}
}
return positionBanks
}, [group])
return [jlpBanks, lstBanks]
}, [jlpGroup, lstGroup])
return (
<div className="rounded-2xl border-2 border-th-fgd-1 bg-th-bkg-1 p-6">
<h1>Stats</h1>
{!isMobile ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('token')}</Th>
<Th className="text-right">Deposits</Th>
<Th className="text-right">Borrows</Th>
</TrHead>
</thead>
<tbody>
{banks.map((bank) => {
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
return (
<TrBody key={bank.name} className="text-sm">
<Td>
<div className="flex items-center space-x-3">
<div
className={`inner-shadow-bottom-sm flex h-12 w-12 items-center justify-center rounded-full border border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2`}
>
<TokenLogo bank={bank} size={28} />
</div>
<div>
<h3>{formatTokenSymbol(bank.name)}</h3>
</div>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={deposits} decimals={2} />
</span>
<p className="font-normal text-th-fgd-4">
{formatCurrencyValue(deposits * bank.uiPrice)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
{bank.name === 'USDC' ? (
<>
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={borrows} decimals={2} />
</span>
<p className="font-normal text-th-fgd-4">
{formatCurrencyValue(borrows * bank.uiPrice)}
</p>
</>
) : (
''
)}
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="mt-4 space-y-2">
{banks.map((bank) => {
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
return (
<div
className="border-th-bk-3 rounded-xl border p-4"
key={bank.name}
>
<div className="flex items-center space-x-3">
<div
className={`inner-shadow-bottom-sm flex h-12 w-12 items-center justify-center rounded-full border border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2`}
>
<TokenLogo bank={bank} size={28} />
</div>
<div>
<h3>{formatTokenSymbol(bank.name)}</h3>
</div>
</div>
<div className="mt-4 flex flex-col space-y-3 sm:flex-row sm:justify-between sm:space-y-0">
<div className="flex w-1/2 flex-col">
<p className="text-th-fgd-4">Deposits</p>
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={deposits} decimals={2} />
</span>
<p className="font-normal text-th-fgd-4">
{formatCurrencyValue(deposits * bank.uiPrice)}
</p>
</div>
<div className="flex w-1/2 flex-col">
<p className="text-th-fgd-4">Borrows</p>
{bank.name === 'USDC' ? (
<>
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={borrows} decimals={2} />
</span>
<p className="font-normal text-th-fgd-4">
{formatCurrencyValue(borrows * bank.uiPrice)}
</p>
</>
) : (
''
)}
</div>
</div>
</div>
)
})}
</div>
)}
<HistoricalStats />
<h1 className="mb-6">Stats</h1>
<h2>Isolated JLP Pool</h2>
<div className="pb-8">
<StatsTable banks={jlpBanks} />
<HistoricalStats />
</div>
<h2>Mango v4 Pools</h2>
<StatsTable banks={lstBanks} />
</div>
)
}

View File

@ -0,0 +1,121 @@
import { Bank } from '@blockworks-foundation/mango-v4'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import TokenLogo from '@components/shared/TokenLogo'
import { useViewport } from 'hooks/useViewport'
import { useTranslation } from 'react-i18next'
import { formatCurrencyValue } from 'utils/numbers'
import { formatTokenSymbol } from 'utils/tokens'
const StatsTable = ({ banks }: { banks: Bank[] }) => {
const { t } = useTranslation('common')
const { isMobile } = useViewport()
return !isMobile ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('token')}</Th>
<Th className="text-right">Deposits</Th>
<Th className="text-right">Borrows</Th>
</TrHead>
</thead>
<tbody>
{banks.map((bank) => {
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
return (
<TrBody key={bank.name} className="text-sm">
<Td>
<div className="flex items-center space-x-3">
<div
className={`inner-shadow-bottom-sm flex h-12 w-12 items-center justify-center rounded-full border border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2`}
>
<TokenLogo bank={bank} size={28} />
</div>
<div>
<h3>{formatTokenSymbol(bank.name)}</h3>
</div>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={deposits} decimals={2} />
</span>
<p className="font-normal text-th-fgd-4">
{formatCurrencyValue(deposits * bank.uiPrice)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
{bank.name === 'USDC' ? (
<>
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={borrows} decimals={2} />
</span>
<p className="font-normal text-th-fgd-4">
{formatCurrencyValue(borrows * bank.uiPrice)}
</p>
</>
) : (
''
)}
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="mt-4 space-y-2">
{banks.map((bank) => {
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
return (
<div className="border-th-bk-3 rounded-xl border p-4" key={bank.name}>
<div className="flex items-center space-x-3">
<div
className={`inner-shadow-bottom-sm flex h-12 w-12 items-center justify-center rounded-full border border-th-bkg-2 bg-gradient-to-b from-th-bkg-1 to-th-bkg-2`}
>
<TokenLogo bank={bank} size={28} />
</div>
<div>
<h3>{formatTokenSymbol(bank.name)}</h3>
</div>
</div>
<div className="mt-4 flex flex-col space-y-3 sm:flex-row sm:justify-between sm:space-y-0">
<div className="flex w-1/2 flex-col">
<p className="text-th-fgd-4">Deposits</p>
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={deposits} decimals={2} />
</span>
<p className="font-normal text-th-fgd-4">
{formatCurrencyValue(deposits * bank.uiPrice)}
</p>
</div>
<div className="flex w-1/2 flex-col">
<p className="text-th-fgd-4">Borrows</p>
{bank.name === 'USDC' ? (
<>
<span className="text-xl font-bold text-th-fgd-1">
<FormatNumericValue value={borrows} decimals={2} />
</span>
<p className="font-normal text-th-fgd-4">
{formatCurrencyValue(borrows * bank.uiPrice)}
</p>
</>
) : (
''
)}
</div>
</div>
</div>
)
})}
</div>
)
}
export default StatsTable

View File

@ -40,16 +40,19 @@ const accountNums = STAKEABLE_TOKENS_DATA.map((d) => d.id)
export default function useAccountHistory() {
const { stakeAccounts } = useStakeAccounts()
const { group } = useMangoGroup()
const { jlpGroup, lstGroup } = useMangoGroup()
const { wallet } = useWallet()
// const accountPks = stakeAccounts?.map((acc) => acc.publicKey.toString()) || []
const accountPks = useMemo(() => {
const client = mangoStore.getState().client
const payer = wallet?.adapter.publicKey?.toBuffer()
if (!group || !payer) return []
if (!jlpGroup || !lstGroup || !payer) return []
const x = accountNums.map((n) => {
const isJlpGroup = n === 0 || n === 1
const group = isJlpGroup ? jlpGroup : lstGroup
const acctNumBuffer = Buffer.alloc(4)
acctNumBuffer.writeUInt32LE(n)
const [mangoAccountPda] = PublicKey.findProgramAddressSync(
@ -59,12 +62,12 @@ export default function useAccountHistory() {
payer,
acctNumBuffer,
],
client.program.programId,
client[isJlpGroup ? 'jlp' : 'lst'].program.programId,
)
return mangoAccountPda.toString()
})
return x
}, [group, wallet])
}, [jlpGroup, lstGroup, wallet])
const activeStakeAccts =
stakeAccounts?.map((acc) => acc.publicKey.toString()) ?? []

View File

@ -3,23 +3,27 @@ import useStakeRates from './useStakeRates'
import useMangoGroup from './useMangoGroup'
// import mangoStore from '@store/mangoStore'
import useLeverageMax from './useLeverageMax'
import { JLP_BORROW_TOKEN, LST_BORROW_TOKEN } from 'utils/constants'
// const set = mangoStore.getState().set
export default function useBankRates(selectedToken: string, leverage: number) {
const { data: stakeRates } = useStakeRates()
const { group } = useMangoGroup()
const { jlpGroup, lstGroup } = useMangoGroup()
// const estimatedMaxAPY = mangoStore((s) => s.estimatedMaxAPY.current)
const leverageMax = useLeverageMax(selectedToken)
const stakeBank = useMemo(() => {
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken, group])
const borrowBank = useMemo(() => {
return group?.banksMapByName.get('USDC')?.[0]
}, [group])
const [stakeBank, borrowBank] = useMemo(() => {
const isJlpGroup = selectedToken === 'JLP' || selectedToken === 'USDC'
const stakeBank = isJlpGroup
? jlpGroup?.banksMapByName.get(selectedToken)?.[0]
: lstGroup?.banksMapByName.get(selectedToken)?.[0]
const borrowBank = isJlpGroup
? jlpGroup?.banksMapByName.get(JLP_BORROW_TOKEN)?.[0]
: lstGroup?.banksMapByName.get(LST_BORROW_TOKEN)?.[0]
return [stakeBank, borrowBank]
}, [selectedToken, jlpGroup, lstGroup])
const stakeBankDepositRate = useMemo(() => {
return stakeBank ? stakeBank.getDepositRate() : 0
@ -29,7 +33,7 @@ export default function useBankRates(selectedToken: string, leverage: number) {
return borrowBank ? Number(borrowBank.getBorrowRate()) : 0
}, [borrowBank])
const jlpStakeRateAPY = useMemo(() => {
const tokenStakeRateAPY = useMemo(() => {
return stakeRates ? stakeRates[selectedToken.toLowerCase()] : 0
}, [stakeRates, selectedToken])
@ -44,7 +48,7 @@ export default function useBankRates(selectedToken: string, leverage: number) {
const borrowRatePerDay = Number(borrowBankBorrowRate) / 365
// Convert the JLP APY to a daily rate
const jlpRatePerDay = (1 + jlpStakeRateAPY) ** (1 / 365) - 1
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)
@ -68,9 +72,9 @@ export default function useBankRates(selectedToken: string, leverage: number) {
deposits -= collateralFees
collectedCollateralFees += collateralFees
const jlpReturns = jlpRatePerDay * deposits
deposits += jlpReturns
collectedReturns += jlpReturns
const tokenReturns = tokenRatePerDay * deposits
deposits += tokenReturns
collectedReturns += tokenReturns
}
// APY's for the calculation
@ -85,9 +89,9 @@ export default function useBankRates(selectedToken: string, leverage: number) {
const APY = (deposits - borrows - 1) * 100
// Comparisons to outside
const nonMangoAPY = jlpStakeRateAPY * leverage * 100
const nonMangoAPY = tokenStakeRateAPY * leverage * 100
const diffToNonMango = APY - nonMangoAPY
const diffToNonLeveraged = APY - jlpStakeRateAPY * 100
const diffToNonLeveraged = APY - tokenStakeRateAPY * 100
return {
APY,
@ -100,25 +104,25 @@ export default function useBankRates(selectedToken: string, leverage: number) {
diffToNonLeveraged,
}
}, [
leverage,
borrowBankBorrowRate,
jlpStakeRateAPY,
stakeBank?.collateralFeePerDay,
stakeBank?.maintAssetWeight,
borrowBankBorrowRate,
tokenStakeRateAPY,
leverage,
])
const estimatedMaxAPY = useMemo(() => {
return (
jlpStakeRateAPY * leverageMax -
tokenStakeRateAPY * leverageMax -
Number(borrowBankBorrowRate) * (leverageMax - 1)
)
}, [jlpStakeRateAPY, borrowBankBorrowRate, leverageMax])
}, [tokenStakeRateAPY, borrowBankBorrowRate, leverageMax])
return {
financialMetrics,
stakeBankDepositRate,
borrowBankBorrowRate,
jlpStakeRateAPY,
jlpStakeRateAPY: tokenStakeRateAPY,
estimatedMaxAPY,
}
}

View File

@ -1,19 +1,25 @@
import { Group } from '@blockworks-foundation/mango-v4'
import { CLUSTER } from '@store/mangoStore'
import mangoStore, { CLUSTER } from '@store/mangoStore'
import { useQuery } from '@tanstack/react-query'
import useMangoGroup from 'hooks/useMangoGroup'
import { Token } from 'types/jupiter'
import { JUPITER_API_DEVNET, JUPITER_API_MAINNET } from 'utils/constants'
const fetchJupiterTokens = async (group: Group) => {
const fetchJupiterTokens = async () => {
const { jlp, lst } = mangoStore.getState().group
if (!jlp || !lst) return
const url = CLUSTER === 'devnet' ? JUPITER_API_DEVNET : JUPITER_API_MAINNET
const response = await fetch(url)
const data: Token[] = await response.json()
const bankMints = Array.from(group.banksMapByName.values()).map((b) =>
const jlpBankMints = Array.from(jlp.banksMapByName.values()).map((b) =>
b[0].mint.toString(),
)
const mangoTokens = data.filter((t) => bankMints.includes(t.address))
const lstBankMints = Array.from(lst.banksMapByName.values()).map((b) =>
b[0].mint.toString(),
)
const mangoTokens = data.filter(
(t) => jlpBankMints.includes(t.address) || lstBankMints.includes(t.address),
)
return {
mangoTokens,
@ -26,19 +32,15 @@ const useJupiterMints = (): {
jupiterTokens: Token[]
isFetching: boolean
} => {
const { group } = useMangoGroup()
const { jlpGroup, lstGroup } = useMangoGroup()
const res = useQuery(
['jupiter-mango-tokens'],
() => fetchJupiterTokens(group!),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60 * 10,
retry: 3,
enabled: !!group,
refetchOnWindowFocus: false,
},
)
const res = useQuery(['jupiter-mango-tokens'], () => fetchJupiterTokens(), {
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60 * 10,
retry: 3,
enabled: !!(jlpGroup && lstGroup),
refetchOnWindowFocus: false,
})
return {
mangoTokens: res?.data?.mangoTokens || [],

View File

@ -1,34 +1,54 @@
import { useMemo } from 'react'
import useMangoGroup from './useMangoGroup'
import { floorToDecimal } from 'utils/numbers'
import { JLP_BORROW_TOKEN, LST_BORROW_TOKEN } from 'utils/constants'
import { getStakableTokensDataForTokenName } from 'utils/tokens'
export default function useLeverageMax(selectedToken: string) {
const { group } = useMangoGroup()
const { jlpGroup, lstGroup } = useMangoGroup()
const stakeBank = useMemo(() => {
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken, group])
const borrowBank = useMemo(() => {
return group?.banksMapByName.get('USDC')?.[0]
}, [group])
const [stakeBank, borrowBank] = useMemo(() => {
const isJlpGroup = selectedToken === 'JLP' || selectedToken === 'USDC'
const stakeBank = isJlpGroup
? jlpGroup?.banksMapByName.get(selectedToken)?.[0]
: lstGroup?.banksMapByName.get(selectedToken)?.[0]
const borrowBank = isJlpGroup
? jlpGroup?.banksMapByName.get(JLP_BORROW_TOKEN)?.[0]
: lstGroup?.banksMapByName.get(LST_BORROW_TOKEN)?.[0]
return [stakeBank, borrowBank]
}, [selectedToken, jlpGroup, lstGroup])
const leverageMax = useMemo(() => {
if (!stakeBank || !borrowBank) return 0
const borrowInitLiabWeight = borrowBank.initLiabWeight
const stakeInitAssetWeight = stakeBank.initAssetWeight
if (!borrowInitLiabWeight || !stakeInitAssetWeight) return 1
const borrowInitLiabWeight = borrowBank.scaledInitLiabWeight(
borrowBank.price,
)
const stakeInitAssetWeight = stakeBank.scaledInitAssetWeight(
stakeBank.price,
)
if (!borrowInitLiabWeight || !stakeInitAssetWeight) return 1
const x = stakeInitAssetWeight.toNumber() / borrowInitLiabWeight.toNumber()
const leverageFactor = 1 / (1 - x)
if (
getStakableTokensDataForTokenName(selectedToken).clientContext === 'jlp'
) {
const leverageFactor = 1 / (1 - x)
const max = floorToDecimal(leverageFactor, 1).toNumber()
const max = floorToDecimal(leverageFactor, 1).toNumber()
return max
}, [stakeBank, borrowBank])
return max * 0.9 // Multiplied by 0.975 because you cant actually get to the end of the infinite geometric series?
} else {
const conversionRate = borrowBank.uiPrice / stakeBank.uiPrice
const y = 1 - conversionRate * stakeInitAssetWeight.toNumber()
const max = floorToDecimal(1 + (x / y) * 0.9, 1).toNumber()
return max
}
}, [stakeBank, borrowBank, selectedToken])
return leverageMax
}

View File

@ -2,9 +2,11 @@ import { Group } from '@blockworks-foundation/mango-v4'
import mangoStore from '@store/mangoStore'
export default function useMangoGroup(): {
group: Group | undefined
jlpGroup: Group | undefined
lstGroup: Group | undefined
} {
const group = mangoStore((s) => s.group)
const jlpGroup = mangoStore((s) => s.group.jlp)
const lstGroup = mangoStore((s) => s.group.lst)
return { group }
return { jlpGroup, lstGroup }
}

View File

@ -1,38 +1,51 @@
import { useMemo } from 'react'
import { BORROW_TOKEN, STAKEABLE_TOKENS } from 'utils/constants'
import {
JLP_BORROW_TOKEN,
LST_BORROW_TOKEN,
STAKEABLE_TOKENS,
} from 'utils/constants'
import useStakeAccounts from './useStakeAccounts'
import useMangoGroup from './useMangoGroup'
import {
toUiDecimalsForQuote,
} from '@blockworks-foundation/mango-v4'
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
export default function usePositions(showInactive = false) {
const { stakeAccounts } = useStakeAccounts()
const { group } = useMangoGroup()
const { jlpGroup, lstGroup } = useMangoGroup()
const borrowBank = useMemo(() => {
return group?.banksMapByName.get(BORROW_TOKEN)?.[0]
}, [group])
const jlpBorrowBank = useMemo(() => {
return jlpGroup?.banksMapByName.get(JLP_BORROW_TOKEN)?.[0]
}, [jlpGroup])
const lstBorrowBank = useMemo(() => {
return lstGroup?.banksMapByName.get(LST_BORROW_TOKEN)?.[0]
}, [lstGroup])
const banks = useMemo(() => {
if (!group) return []
if (!jlpGroup || !lstGroup) return []
const positionBanks = []
for (const token of STAKEABLE_TOKENS) {
const bank = group.banksMapByName.get(token)?.[0]
const isJlpGroup = token === 'JLP' || token === 'USDC'
const bank = isJlpGroup
? jlpGroup.banksMapByName.get(token)?.[0]
: lstGroup.banksMapByName.get(token)?.[0]
positionBanks.push(bank)
}
return positionBanks
}, [group])
}, [jlpGroup, lstGroup])
const positions = useMemo(() => {
const positions = []
for (const bank of banks) {
if (!bank || !group) continue
if (!bank || !jlpGroup || !lstGroup) continue
const isJlpGroup = bank.name === 'JLP' || bank.name === 'USDC'
const group = isJlpGroup ? jlpGroup : lstGroup
const borrowBank = isJlpGroup ? jlpBorrowBank : lstBorrowBank
const acct = stakeAccounts?.find((acc) => acc.getTokenBalanceUi(bank) > 0)
const stakeBalance = acct ? acct.getTokenBalanceUi(bank) : 0
const pnl = acct ? toUiDecimalsForQuote(acct.getPnl(group).toNumber()) : 0
const borrowBalance = acct && borrowBank ? acct.getTokenBalanceUi(borrowBank) : 0
const borrowBalance =
acct && borrowBank ? acct.getTokenBalanceUi(borrowBank) : 0
positions.push({ borrowBalance, stakeBalance, bank, pnl, acct })
}
const sortedPositions = positions.sort(
@ -41,7 +54,15 @@ export default function usePositions(showInactive = false) {
return showInactive
? sortedPositions
: sortedPositions.filter((pos) => pos.stakeBalance > 0)
}, [banks, showInactive, stakeAccounts, group, borrowBank])
}, [
banks,
showInactive,
stakeAccounts,
jlpGroup,
lstGroup,
jlpBorrowBank,
lstBorrowBank,
])
return { borrowBank, positions }
return { jlpBorrowBank, lstBorrowBank, positions }
}

View File

@ -1,116 +0,0 @@
import { Serum3Market } from '@blockworks-foundation/mango-v4'
import mangoStore from '@store/mangoStore'
import { useMemo } from 'react'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import useJupiterMints from './useJupiterMints'
import useMangoGroup from './useMangoGroup'
import { CUSTOM_TOKEN_ICONS } from 'utils/constants'
export default function useSelectedMarket() {
const { group } = useMangoGroup()
const selectedMarket = mangoStore((s) => s.selectedMarket.current)
const { mangoTokens } = useJupiterMints()
const marketAddress = useMemo(() => {
return selectedMarket?.publicKey.toString()
}, [selectedMarket])
const price: number = useMemo(() => {
if (!group) return 0
if (selectedMarket instanceof Serum3Market) {
const baseBank = group.getFirstBankByTokenIndex(
selectedMarket.baseTokenIndex,
)
const quoteBank = group.getFirstBankByTokenIndex(
selectedMarket.quoteTokenIndex,
)
const market = group.getSerum3ExternalMarket(
selectedMarket.serumMarketExternal,
)
return floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(market.tickSize),
).toNumber()
} else if (selectedMarket) {
return selectedMarket._uiPrice
} else return 0
}, [selectedMarket, group])
const serumOrPerpMarket = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarket) return
if (selectedMarket instanceof Serum3Market) {
return group?.getSerum3ExternalMarket(selectedMarket.serumMarketExternal)
} else {
return selectedMarket
}
}, [selectedMarket])
const baseSymbol = useMemo(() => {
return selectedMarket?.name.split(/-|\//)[0]
}, [selectedMarket])
const baseLogoURI = useMemo(() => {
if (!baseSymbol || !mangoTokens.length) return ''
const lowerCaseBaseSymbol = baseSymbol.toLowerCase()
const hasCustomIcon = CUSTOM_TOKEN_ICONS[lowerCaseBaseSymbol]
if (hasCustomIcon) {
return `/icons/${lowerCaseBaseSymbol}.svg`
} else {
const token =
mangoTokens.find(
(t) => t.symbol.toLowerCase() === lowerCaseBaseSymbol,
) ||
mangoTokens.find(
(t) => t.symbol.toLowerCase()?.includes(lowerCaseBaseSymbol),
)
if (token) {
return token.logoURI
}
}
}, [baseSymbol, mangoTokens])
const quoteBank = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarket) return
const tokenIdx =
selectedMarket instanceof Serum3Market
? selectedMarket.quoteTokenIndex
: selectedMarket?.settleTokenIndex
return group?.getFirstBankByTokenIndex(tokenIdx)
}, [selectedMarket])
const quoteSymbol = useMemo(() => {
return quoteBank?.name
}, [quoteBank])
const quoteLogoURI = useMemo(() => {
if (!quoteSymbol || !mangoTokens.length) return ''
const lowerCaseQuoteSymbol = quoteSymbol.toLowerCase()
const hasCustomIcon = CUSTOM_TOKEN_ICONS[lowerCaseQuoteSymbol]
if (hasCustomIcon) {
return `/icons/${lowerCaseQuoteSymbol}.svg`
} else {
const token = mangoTokens.find(
(t) => t.symbol.toLowerCase() === lowerCaseQuoteSymbol,
)
if (token) {
return token.logoURI
}
}
}, [quoteSymbol, mangoTokens])
return {
selectedMarket,
selectedMarketAddress: marketAddress,
price,
serumOrPerpMarket,
baseSymbol,
quoteBank,
quoteSymbol,
baseLogoURI,
quoteLogoURI,
}
}

View File

@ -1,12 +1,18 @@
import { useQuery } from '@tanstack/react-query'
import { fetchSwapChartPrices } from 'apis/birdeye/helpers'
import { STAKEABLE_TOKENS_DATA } from 'utils/constants'
import { SOL_MINT, STAKEABLE_TOKENS_DATA, USDC_MINT } from 'utils/constants'
const fetchRates = async () => {
try {
const [jlpPrices] = await Promise.all([
fetchSwapChartPrices(STAKEABLE_TOKENS_DATA[0]?.mint_address, STAKEABLE_TOKENS_DATA[1]?.mint_address, '30')
])
const promises = STAKEABLE_TOKENS_DATA.filter(
(token) => token.mint_address !== USDC_MINT,
).map((t) => {
const isUsdcBorrow = t.name === 'JLP' || t.name === 'USDC'
const outputMint = isUsdcBorrow ? USDC_MINT : SOL_MINT
return fetchSwapChartPrices(t.mint_address, outputMint, '30')
})
const [jlpPrices, msolPrices, jitoPrices, bsolPrices] =
await Promise.all(promises)
// may be null if the price range cannot be calculated
/*
@ -19,10 +25,26 @@ const fetchRates = async () => {
*/
const rateData: Record<string, number> = {}
rateData.jlp =
(12 * (jlpPrices[jlpPrices.length - 2].price - jlpPrices[0].price)) /
jlpPrices[0].price
if (jlpPrices && jlpPrices?.length > 1) {
rateData.jlp =
(12 * (jlpPrices[jlpPrices.length - 2].price - jlpPrices[0].price)) /
jlpPrices[0].price
}
if (msolPrices && msolPrices?.length > 1) {
rateData.msol =
(12 * (msolPrices[msolPrices.length - 2].price - msolPrices[0].price)) /
msolPrices[0].price
}
if (jitoPrices && jitoPrices?.length > 1) {
rateData.jitosol =
(12 * (jitoPrices[jitoPrices.length - 2].price - jitoPrices[0].price)) /
jitoPrices[0].price
}
if (bsolPrices && bsolPrices?.length > 1) {
rateData.bsol =
(12 * (bsolPrices[bsolPrices.length - 2].price - bsolPrices[0].price)) /
bsolPrices[0].price
}
/*
@ -40,7 +62,7 @@ const fetchRates = async () => {
}
*/
return rateData
} catch (e) {
return {}

View File

@ -36,7 +36,7 @@
},
"dependencies": {
"@blockworks-foundation/mango-feeds": "0.1.7",
"@blockworks-foundation/mango-v4": "0.23.0-rc5",
"@blockworks-foundation/mango-v4": "0.23.3",
"@blockworks-foundation/mango-v4-settings": "0.14.15",
"@glitchful-dev/sol-apy-sdk": "3.0.2",
"@headlessui/react": "1.6.6",

View File

@ -24,6 +24,7 @@ import {
PerpPosition,
BookSide,
ParsedFillEvent,
MANGO_V4_ID,
} from '@blockworks-foundation/mango-v4'
import EmptyWallet from '../utils/wallet'
@ -36,12 +37,10 @@ import {
BOOST_ACCOUNT_PREFIX,
BOOST_DATA_API_URL,
CONNECTION_COMMITMENT,
DEFAULT_MARKET_NAME,
FALLBACK_ORACLES,
INPUT_TOKEN_DEFAULT,
ClientContextKeys,
MANGO_DATA_API_URL,
MAX_PRIORITY_FEE_KEYS,
OUTPUT_TOKEN_DEFAULT,
PAGINATION_PAGE_LENGTH,
RPC_PROVIDER_KEY,
STAKEABLE_TOKENS,
@ -67,9 +66,7 @@ import {
PositionStat,
OrderbookTooltip,
} from 'types'
import spotBalancesUpdater from './spotBalancesUpdater'
import { PerpMarket } from '@blockworks-foundation/mango-v4/'
import perpPositionsUpdater from './perpPositionsUpdater'
import {
DEFAULT_PRIORITY_FEE,
TRITON_DEDICATED_URL,
@ -85,7 +82,9 @@ import { sleep } from 'utils'
const MANGO_BOOST_ID = new PublicKey(
'zF2vSz6V9g1YHGmfrzsY497NJzbRr84QUrPry4bLQ25',
)
const GROUP = new PublicKey('AKeMSYiJekyKfwCc3CUfVNDVAiqk9FfbQVMY3G7RUZUf')
const GROUP_JLP = new PublicKey('AKeMSYiJekyKfwCc3CUfVNDVAiqk9FfbQVMY3G7RUZUf')
const GROUP_V1 = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX')
const ENDPOINTS = [
{
@ -117,20 +116,35 @@ const initMangoClient = (
prioritizationFee: DEFAULT_PRIORITY_FEE,
fallbackOracleConfig: FALLBACK_ORACLES,
},
): MangoClient => {
return MangoClient.connect(provider, CLUSTER, MANGO_BOOST_ID, {
prioritizationFee: opts.prioritizationFee,
fallbackOracleConfig: opts.fallbackOracleConfig,
idsSource: 'get-program-accounts',
postSendTxCallback: ({ txid }: { txid: string }) => {
notify({
title: 'Transaction sent',
description: 'Waiting for confirmation',
type: 'confirm',
txid: txid,
})
},
})
): { lst: MangoClient; jlp: MangoClient } => {
return {
lst: MangoClient.connect(provider, CLUSTER, MANGO_V4_ID['mainnet-beta'], {
prioritizationFee: opts.prioritizationFee,
fallbackOracleConfig: opts.fallbackOracleConfig,
idsSource: 'api',
postSendTxCallback: ({ txid }: { txid: string }) => {
notify({
title: 'Transaction sent',
description: 'Waiting for confirmation',
type: 'confirm',
txid: txid,
})
},
}),
jlp: MangoClient.connect(provider, CLUSTER, MANGO_BOOST_ID, {
prioritizationFee: opts.prioritizationFee,
fallbackOracleConfig: opts.fallbackOracleConfig,
idsSource: 'get-program-accounts',
postSendTxCallback: ({ txid }: { txid: string }) => {
notify({
title: 'Transaction sent',
description: 'Waiting for confirmation',
type: 'confirm',
txid: txid,
})
},
}),
}
}
export const DEFAULT_TRADE_FORM: TradeForm = {
@ -163,9 +177,9 @@ export type MangoStore = {
}
connected: boolean
connection: Connection
group: Group | undefined
group: { jlp: Group | undefined; lst: Group | undefined }
groupLoaded: boolean
client: MangoClient
client: { jlp: MangoClient; lst: MangoClient }
showUserSetup: boolean
leverage: number
mangoAccount: {
@ -270,7 +284,10 @@ export type MangoStore = {
limit?: number,
) => Promise<void>
fetchGroup: () => Promise<void>
reloadMangoAccount: (slot?: number) => Promise<void>
reloadMangoAccount: (
clientContext: ClientContextKeys,
slot?: number,
) => Promise<void>
fetchMangoAccounts: (ownerPk: PublicKey) => Promise<void>
fetchProfileDetails: (walletPk: string) => void
fetchSwapHistory: (
@ -328,7 +345,7 @@ const mangoStore = create<MangoStore>()(
},
connected: false,
connection,
group: undefined,
group: { jlp: undefined, lst: undefined },
groupLoaded: false,
client,
showUserSetup: false,
@ -532,64 +549,20 @@ const mangoStore = create<MangoStore>()(
try {
const set = get().set
const client = get().client
const group = await client.getGroup(GROUP)
let selectedMarketName = get().selectedMarket.name
if (!selectedMarketName) {
selectedMarketName = DEFAULT_MARKET_NAME
}
const inputBank =
group?.banksMapByName.get(INPUT_TOKEN_DEFAULT)?.[0]
const outputBank =
group?.banksMapByName.get(OUTPUT_TOKEN_DEFAULT)?.[0]
const serumMarkets = Array.from(
group.serum3MarketsMapByExternal.values(),
).map((m) => {
// remove this when market name is updated
if (m.name === 'MSOL/SOL') {
m.name = 'mSOL/SOL'
}
return m
})
const perpMarkets = Array.from(group.perpMarketsMapByName.values())
.filter(
(p) =>
p.publicKey.toString() !==
'9Y8paZ5wUpzLFfQuHz8j2RtPrKsDtHx9sbgFmWb5abCw',
)
.sort((a, b) => a.name.localeCompare(b.name))
const selectedMarket =
serumMarkets.find((m) => m.name === selectedMarketName) ||
perpMarkets.find((m) => m.name === selectedMarketName) ||
serumMarkets[0]
const lstGroup = await client.lst.getGroup(GROUP_V1)
const jlpGroup = await client.jlp.getGroup(GROUP_JLP)
set((state) => {
state.group = group
state.group.jlp = jlpGroup
state.group.lst = lstGroup
state.groupLoaded = true
state.serumMarkets = serumMarkets
state.perpMarkets = perpMarkets
state.selectedMarket.current = selectedMarket
if (!state.swap.inputBank && !state.swap.outputBank) {
state.swap.inputBank = inputBank
state.swap.outputBank = outputBank
} else {
state.swap.inputBank = group.getFirstBankByMint(
state.swap.inputBank!.mint,
)
state.swap.outputBank = group.getFirstBankByMint(
state.swap.outputBank!.mint,
)
}
})
} catch (e) {
notify({ type: 'info', title: 'Unable to refresh data' })
console.error('Error fetching group', e)
console.error('Error fetching groups', e)
}
},
reloadMangoAccount: async (confirmationSlot) => {
reloadMangoAccount: async (clientContext, confirmationSlot) => {
const set = get().set
const actions = get().actions
try {
@ -600,7 +573,7 @@ const mangoStore = create<MangoStore>()(
if (!mangoAccount) return
const { value: reloadedMangoAccount, slot } =
await mangoAccount.reloadWithSlot(client)
await mangoAccount.reloadWithSlot(client[clientContext])
const lastSlot = get().mangoAccount.lastSlot
if (
@ -620,7 +593,7 @@ const mangoStore = create<MangoStore>()(
})
}
} else if (confirmationSlot && slot < confirmationSlot) {
await actions.reloadMangoAccount(confirmationSlot)
await actions.reloadMangoAccount(clientContext, confirmationSlot)
await sleep(100)
}
} catch (e) {
@ -633,20 +606,34 @@ const mangoStore = create<MangoStore>()(
},
fetchMangoAccounts: async (ownerPk: PublicKey) => {
const set = get().set
// const actions = get().actions
try {
const group = get().group
const client = get().client
const selectedMangoAccount = get().mangoAccount.current
const jlpGroup = get().group.jlp
const lstGroup = get().group.lst
const selectedToken = get().selectedToken
if (!group) throw new Error('Group not loaded')
const client = get().client
const selectedMangoAccount = get().mangoAccount.current
if (!jlpGroup) throw new Error('JLP group not loaded')
if (!lstGroup) throw new Error('LST group not loaded')
if (!client) throw new Error('Client not loaded')
const [ownerMangoAccounts, delegateAccounts] = await Promise.all([
client.getMangoAccountsForOwner(group, ownerPk),
client.getMangoAccountsForDelegate(group, ownerPk),
const [
jlpOwnerMangoAccounts,
lstOwnerMangoAccounts,
jlpDelegateAccounts,
lstDelegateAccounts,
] = await Promise.all([
client.jlp.getMangoAccountsForOwner(jlpGroup, ownerPk),
client.lst.getMangoAccountsForOwner(lstGroup, ownerPk),
client.jlp.getMangoAccountsForDelegate(jlpGroup, ownerPk),
client.lst.getMangoAccountsForDelegate(lstGroup, ownerPk),
])
const mangoAccounts = [...ownerMangoAccounts, ...delegateAccounts]
const mangoAccounts = [
...jlpOwnerMangoAccounts,
...lstOwnerMangoAccounts,
...jlpDelegateAccounts,
...lstDelegateAccounts,
]
console.log('mango accounts: ', mangoAccounts)
const selectedAccountIsNotInAccountsList = mangoAccounts.find(
(x) =>
@ -676,16 +663,10 @@ const mangoStore = create<MangoStore>()(
}
console.log('newSelectedMangoAccount', newSelectedMangoAccount)
// await newSelectedMangoAccount.reloadSerum3OpenOrders(client)
set((state) => {
state.mangoAccount.current = newSelectedMangoAccount
state.mangoAccount.initialLoad = false
})
// actions.fetchOpenOrders()
// await Promise.all(
// mangoAccounts.map((ma) => ma.reloadSerum3OpenOrders(client)),
// )
set((state) => {
state.mangoAccounts = mangoAccounts
@ -827,7 +808,7 @@ const mangoStore = create<MangoStore>()(
endpointUrl,
CONNECTION_COMMITMENT,
)
const oldProvider = client.program.provider as AnchorProvider
const oldProvider = client.jlp.program.provider as AnchorProvider
const newProvider = new AnchorProvider(
newConnection,
oldProvider.wallet,
@ -889,7 +870,8 @@ const mangoStore = create<MangoStore>()(
LAMPORTS_PER_SOL * 0.01,
)
const provider = client.program.provider as AnchorProvider
//can use any provider doesn't matter both should be same
const provider = client.jlp.program.provider as AnchorProvider
provider.opts.skipPreflight = true
const newClient = initMangoClient(provider, {
@ -906,14 +888,4 @@ const mangoStore = create<MangoStore>()(
}),
)
mangoStore.subscribe((state) => state.mangoAccount.current, spotBalancesUpdater)
mangoStore.subscribe(
(state) => state.mangoAccount.openOrderAccounts,
spotBalancesUpdater,
)
mangoStore.subscribe(
(state) => state.mangoAccount.current,
perpPositionsUpdater,
)
export default mangoStore

View File

@ -1,25 +0,0 @@
import { PerpPosition } from '@blockworks-foundation/mango-v4'
import mangoStore from './mangoStore'
const perpPositionsUpdater = () => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const group = mangoStore.getState().group
const set = mangoStore.getState().set
if (!mangoAccount || !group) return
const positions: PerpPosition[] = []
for (const perpMarket of mangoAccount.perpActive()) {
const position = mangoAccount.getPerpPosition(perpMarket.marketIndex)
if (position) {
positions.push(position)
}
}
set((s) => {
s.mangoAccount.perpPositions = positions
})
}
export default perpPositionsUpdater

View File

@ -1,93 +0,0 @@
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import { SpotBalances } from 'types'
import mangoStore from './mangoStore'
const spotBalancesUpdater = () => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const group = mangoStore.getState().group
const openOrdersAccounts =
mangoStore.getState().mangoAccount.openOrderAccounts
const set = mangoStore.getState().set
if (!mangoAccount || !group) return
const balances: SpotBalances = {}
for (const serumMarket of mangoAccount.serum3Active()) {
const market = group.getSerum3MarketByMarketIndex(serumMarket.marketIndex)
if (!market) continue
const openOrdersAccForMkt = openOrdersAccounts.find((oo) =>
oo.market.equals(market.serumMarketExternal),
)
let baseTokenUnsettled = 0
let quoteTokenUnsettled = 0
let baseTokenLockedInOrder = 0
let quoteTokenLockedInOrder = 0
if (openOrdersAccForMkt) {
baseTokenUnsettled = toUiDecimals(
openOrdersAccForMkt.baseTokenFree.toNumber(),
group.getFirstBankByTokenIndex(serumMarket.baseTokenIndex).mintDecimals,
)
quoteTokenUnsettled = toUiDecimals(
openOrdersAccForMkt.quoteTokenFree
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.add((openOrdersAccForMkt as any)['referrerRebatesAccrued'])
.toNumber(),
group.getFirstBankByTokenIndex(serumMarket.quoteTokenIndex)
.mintDecimals,
)
baseTokenLockedInOrder = toUiDecimals(
openOrdersAccForMkt.baseTokenTotal
.sub(openOrdersAccForMkt.baseTokenFree)
.toNumber(),
group.getFirstBankByTokenIndex(serumMarket.baseTokenIndex).mintDecimals,
)
quoteTokenLockedInOrder = toUiDecimals(
openOrdersAccForMkt.quoteTokenTotal
.sub(openOrdersAccForMkt.quoteTokenFree)
.toNumber(),
group.getFirstBankByTokenIndex(serumMarket.quoteTokenIndex)
.mintDecimals,
)
}
let quoteBalances =
balances[
group
.getSerum3ExternalMarket(market.serumMarketExternal)
.quoteMintAddress.toString()
]
if (!quoteBalances) {
quoteBalances = balances[
group
.getSerum3ExternalMarket(market.serumMarketExternal)
.quoteMintAddress.toString()
] = { inOrders: 0, unsettled: 0 }
}
quoteBalances.inOrders += quoteTokenLockedInOrder || 0
quoteBalances.unsettled += quoteTokenUnsettled
let baseBalances =
balances[
group
.getSerum3ExternalMarket(market.serumMarketExternal)
.baseMintAddress.toString()
]
if (!baseBalances) {
baseBalances = balances[
group
.getSerum3ExternalMarket(market.serumMarketExternal)
.baseMintAddress.toString()
] = { inOrders: 0, unsettled: 0 }
}
baseBalances.inOrders += baseTokenLockedInOrder
baseBalances.unsettled += baseTokenUnsettled
}
set((s) => {
s.mangoAccount.spotBalances = balances
})
}
export default spotBalancesUpdater

View File

@ -1,12 +1,61 @@
import { PublicKey } from '@solana/web3.js'
// lev stake
export const BORROW_TOKEN = 'USDC'
export const JLP_BORROW_TOKEN = 'USDC'
export const LST_BORROW_TOKEN = 'SOL'
export const STAKEABLE_TOKENS_DATA = [
{ name: 'JLP', id: 1, active: true, mint_address: '27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4' },
{ name: 'USDC', id: 0, active: true, mint_address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' },
export const STAKEABLE_TOKENS_DATA: {
name: string
id: number
active: boolean
mint_address: string
clientContext: ClientContextKeys
borrowToken: 'USDC' | 'SOL'
}[] = [
{
name: 'JLP',
id: 1,
active: true,
mint_address: '27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4',
clientContext: 'jlp',
borrowToken: 'USDC',
},
{
name: 'USDC',
id: 0,
active: true,
mint_address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
clientContext: 'jlp',
borrowToken: 'USDC',
},
{
name: 'MSOL',
id: 521,
active: true,
mint_address: 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So',
clientContext: 'lst',
borrowToken: 'SOL',
},
{
name: 'JitoSOL',
id: 621,
active: true,
mint_address: 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn',
clientContext: 'lst',
borrowToken: 'SOL',
},
{
name: 'bSOL',
id: 721,
active: true,
mint_address: 'bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1',
clientContext: 'lst',
borrowToken: 'SOL',
},
]
export type ClientContextKeys = 'lst' | 'jlp'
export const STAKEABLE_TOKENS = STAKEABLE_TOKENS_DATA.filter(
(d) => d.active,
).map((d) => d.name)
@ -27,6 +76,7 @@ export const SECONDS = 1000
export const INPUT_TOKEN_DEFAULT = 'USDC'
export const MANGO_MINT = 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'
export const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
export const SOL_MINT = 'So11111111111111111111111111111111111111112'
export const OUTPUT_TOKEN_DEFAULT = 'JLP'
export const JUPITER_V4_PROGRAM_ID =
@ -152,6 +202,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = {
'eth (portal)': true,
hnt: true,
jitosol: true,
jlp: true,
kin: true,
ldo: true,
mngo: true,

View File

@ -1,168 +0,0 @@
import {
BookSide,
BookSideType,
MangoClient,
PerpMarket,
} from '@blockworks-foundation/mango-v4'
import { Market, Orderbook as SpotOrderBook } from '@project-serum/serum'
import { AccountInfo } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
import Big from 'big.js'
import { cumOrderbookSide } from 'types'
import { getDecimalCount } from './numbers'
export const getMarket = () => {
const group = mangoStore.getState().group
const selectedMarket = mangoStore.getState().selectedMarket.current
if (!group || !selectedMarket) return
return selectedMarket instanceof PerpMarket
? selectedMarket
: group?.getSerum3ExternalMarket(selectedMarket.serumMarketExternal)
}
export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => {
const depth = 300
if (book instanceof SpotOrderBook) {
return book.getL2(depth).map(([price, size]) => [price, size])
} else if (book instanceof BookSide) {
return book.getL2Ui(depth)
}
return []
}
export function decodeBook(
client: MangoClient,
market: Market | PerpMarket,
accInfo: AccountInfo<Buffer>,
side: 'bids' | 'asks',
): SpotOrderBook | BookSide {
if (market instanceof Market) {
const book = SpotOrderBook.decode(market, accInfo.data)
return book
} else {
const decodedAcc = client.program.coder.accounts.decode(
'bookSide',
accInfo.data,
)
const book = BookSide.from(
client,
market,
side === 'bids' ? BookSideType.bids : BookSideType.asks,
decodedAcc,
)
return book
}
}
export const updatePerpMarketOnGroup = (
book: BookSide,
side: 'bids' | 'asks',
) => {
const group = mangoStore.getState().group
const perpMarket = group?.getPerpMarketByMarketIndex(
book.perpMarket.perpMarketIndex,
)
if (perpMarket) {
perpMarket[`_${side}`] = book
// mangoStore.getState().actions.fetchOpenOrders()
}
}
export const hasOpenOrderForPriceGroup = (
openOrderPrices: number[],
price: number,
grouping: number,
isGrouped: boolean,
) => {
if (!isGrouped) {
return !!openOrderPrices.find((ooPrice) => {
return ooPrice === price
})
}
return !!openOrderPrices.find((ooPrice) => {
return ooPrice >= price - grouping && ooPrice <= price + grouping
})
}
export const getCumulativeOrderbookSide = (
orders: number[][],
totalSize: number,
maxSize: number,
depth: number,
usersOpenOrderPrices: number[],
grouping: number,
isGrouped: boolean,
): cumOrderbookSide[] => {
let cumulativeSize = 0
let cumulativeValue = 0
return orders.slice(0, depth).map(([price, size]) => {
cumulativeSize += size
cumulativeValue += price * size
return {
price: Number(price),
size,
averagePrice: cumulativeValue / cumulativeSize,
cumulativeValue: cumulativeValue,
cumulativeSize,
sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100),
cumulativeSizePercent: Math.round((size / (cumulativeSize || 1)) * 100),
maxSizePercent: Math.round((size / (maxSize || 1)) * 100),
isUsersOrder: hasOpenOrderForPriceGroup(
usersOpenOrderPrices,
price,
grouping,
isGrouped,
),
}
})
}
export const groupBy = (
ordersArray: number[][],
market: PerpMarket | Market,
grouping: number,
isBids: boolean,
) => {
if (!ordersArray || !market || !grouping || grouping == market?.tickSize) {
return ordersArray || []
}
const groupFloors: Record<number, number> = {}
for (let i = 0; i < ordersArray.length; i++) {
if (typeof ordersArray[i] == 'undefined') {
break
}
const bigGrouping = Big(grouping)
const bigOrder = Big(ordersArray[i][0])
const floor = isBids
? bigOrder
.div(bigGrouping)
.round(0, Big.roundDown)
.times(bigGrouping)
.toNumber()
: bigOrder
.div(bigGrouping)
.round(0, Big.roundUp)
.times(bigGrouping)
.toNumber()
if (typeof groupFloors[floor] == 'undefined') {
groupFloors[floor] = ordersArray[i][1]
} else {
groupFloors[floor] = ordersArray[i][1] + groupFloors[floor]
}
}
const sortedGroups = Object.entries(groupFloors)
.map((entry) => {
return [
+parseFloat(entry[0]).toFixed(getDecimalCount(grouping)),
entry[1],
]
})
.sort((a: number[], b: number[]) => {
if (!a || !b) {
return -1
}
return isBids ? b[0] - a[0] : a[0] - b[0]
})
return sortedGroups
}

View File

@ -1,6 +1,7 @@
import { PublicKey, Connection } from '@solana/web3.js'
import { TokenInstructions } from '@project-serum/serum'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import { STAKEABLE_TOKENS_DATA } from './constants'
export class TokenAccount {
publicKey!: PublicKey
@ -26,7 +27,7 @@ export class TokenAccount {
}
}
function exists<T>(item: T | null | undefined): item is T {
export function exists<T>(item: T | null | undefined): item is T {
return !!item
}
@ -74,3 +75,11 @@ export const formatTokenSymbol = (symbol: string) => {
}
return symbol === 'MSOL' ? 'mSOL' : symbol
}
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)!
}

View File

@ -37,6 +37,7 @@ import {
import { floorToDecimal } from './numbers'
import { BOOST_ACCOUNT_PREFIX } from './constants'
import { notify } from './notifications'
import { getStakableTokensDataForMint } from './tokens'
export const withdrawAndClose = async (
client: MangoClient,
@ -47,10 +48,12 @@ export const withdrawAndClose = async (
) => {
console.log('withdraw and close')
const borrowBank = group?.banksMapByName.get('USDC')?.[0]
const borrowBank = group?.banksMapByName.get(
getStakableTokensDataForMint(stakeMintPk.toBase58()).borrowToken,
)?.[0]
const stakeBank = group?.banksMapByMint.get(stakeMintPk.toString())?.[0]
const instructions: TransactionInstruction[] = []
console.log(borrowBank, stakeBank, mangoAccount)
if (!borrowBank || !stakeBank || !mangoAccount) {
throw Error('Unable to find USDC bank or stake bank or mango account')
}
@ -117,7 +120,9 @@ export const unstakeAndSwap = async (
console.log('unstake and swap')
const payer = (client.program.provider as AnchorProvider).wallet.publicKey
const borrowBank = group?.banksMapByName.get('USDC')?.[0]
const borrowBank = group?.banksMapByName.get(
getStakableTokensDataForMint(stakeMintPk.toBase58()).borrowToken,
)?.[0]
const stakeBank = group?.banksMapByMint.get(stakeMintPk.toString())?.[0]
const instructions: TransactionInstruction[] = []
@ -282,7 +287,9 @@ export const stakeAndCreate = async (
name?: string,
): Promise<MangoSignatureStatus> => {
const payer = (client.program.provider as AnchorProvider).wallet.publicKey
const borrowBank = group?.banksMapByName.get('USDC')?.[0]
const borrowBank = group?.banksMapByName.get(
getStakableTokensDataForMint(stakeMintPk.toBase58()).borrowToken,
)?.[0]
const stakeBank = group?.banksMapByMint.get(stakeMintPk.toString())?.[0]
const instructions: TransactionInstruction[] = []

View File

@ -39,10 +39,10 @@
bn.js "^5.2.1"
eslint-config-prettier "^9.0.0"
"@blockworks-foundation/mango-v4@0.23.0-rc5":
version "0.23.0-rc5"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.23.0-rc5.tgz#8b49aa9439f9b997732246ef3834e929d28cf225"
integrity sha512-IS2zW3bG3slPSCDYeDpSGmHTHJ9qJEPiARLzpwtiQxYf3WfqgTHwgC7sxorZGcmjkZeZTr4xPgBzqaOeTWGDsw==
"@blockworks-foundation/mango-v4@0.23.3":
version "0.23.3"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.23.3.tgz#a50d456303e2ba4278962aae9eab330d32688d1f"
integrity sha512-dL47bv7p+um7iiDm0VBNluWZG9EJQMFEhoiGb8zRraFKEZgxWa+B2eLFPn2rN8JnMbvnrntUGFePaXs4XK2D5A==
dependencies:
"@blockworks-foundation/mango-v4-settings" "0.14.15"
"@blockworks-foundation/mangolana" "0.0.14"