Compare commits
2 Commits
6ebca4f76c
...
abadeefd2a
Author | SHA1 | Date |
---|---|---|
Adrian Brzeziński | abadeefd2a | |
saml33 | 3c6df1dbcc |
|
@ -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
|
|
@ -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,
|
||||
})}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 /> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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}
|
||||
|
|
|
@ -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
|
|
@ -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'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've borrowed)
|
||||
against your "weighted" 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'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'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'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 "Total Earned" 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'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'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'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'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'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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -13,7 +13,6 @@ const TokenLogo = ({
|
|||
size?: number
|
||||
}) => {
|
||||
const { mangoTokens } = useJupiterMints()
|
||||
|
||||
const logoUri = useMemo(() => {
|
||||
if (!bank) return ''
|
||||
const tokenSymbol = bank.name.toLowerCase()
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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()) ?? []
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 || [],
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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 {}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)!
|
||||
}
|
||||
|
|
|
@ -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[] = []
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue