diff --git a/components/AccountStats.tsx b/components/AccountStats.tsx deleted file mode 100644 index f94bf00..0000000 --- a/components/AccountStats.tsx +++ /dev/null @@ -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 ( - <> -

{`Boosted ${formatTokenSymbol(token)}`}

-
-
-

Max Est. APY

- - {estimatedMaxAPY ? `${estimatedMaxAPY.toFixed(2)}%` : 0} - -
-
-

Max Leverage

- 3x -
-
-

Capacity Remaining

- - SOL - -
-
-

Total Staked

- - {' '} - {formatTokenSymbol(token)} - -
-
- - ) -} - -export default AccountStats diff --git a/components/DepositForm.tsx b/components/DepositForm.tsx index edf928f..6edcad5 100644 --- a/components/DepositForm.tsx +++ b/components/DepositForm.tsx @@ -5,7 +5,7 @@ import React, { useCallback, 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 { formatTokenSymbol } from '../utils/tokens' import Label from './forms/Label' import Button, { IconButton } from './shared/Button' import Loading from './shared/Loading' @@ -29,6 +29,7 @@ import { sleep } from 'utils' import ButtonGroup from './forms/ButtonGroup' import Decimal from 'decimal.js' import useIpAddress from 'hooks/useIpAddress' +import { walletBalanceForToken } from './StakeForm' const set = mangoStore.getState().set @@ -39,27 +40,6 @@ 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, - } -} - // const getNextAccountNumber = (accounts: MangoAccount[]): number => { // if (accounts.length > 1) { // return ( @@ -80,7 +60,7 @@ function DespositForm({ token: selectedToken }: StakeFormProps) { 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,8 +68,8 @@ 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) @@ -121,7 +101,7 @@ function DespositForm({ token: selectedToken }: StakeFormProps) { return } const client = mangoStore.getState().client - const group = mangoStore.getState().group + const group = mangoStore.getState().group.jlpGroup const actions = mangoStore.getState().actions const mangoAccounts = mangoStore.getState().mangoAccounts const mangoAccount = mangoStore.getState().mangoAccount.current @@ -175,7 +155,7 @@ function DespositForm({ token: selectedToken }: StakeFormProps) { type: 'error', }) } - }, [depositBank, publicKey, inputAmount]) + }, [depositBank, publicKey, inputAmount, ipAllowed]) const showInsufficientBalance = tokenMax.maxAmount < Number(inputAmount) || @@ -288,7 +268,7 @@ function DespositForm({ token: selectedToken }: StakeFormProps) { ) : showInsufficientBalance ? (
- + {t('swap:insufficient-balance', { symbol: selectedToken, })} diff --git a/components/Positions.tsx b/components/Positions.tsx index 08c5b29..a7cced6 100644 --- a/components/Positions.tsx +++ b/components/Positions.tsx @@ -46,7 +46,11 @@ const Positions = ({ }) => { const [showInactivePositions, setShowInactivePositions] = useLocalStorageState(SHOW_INACTIVE_POSITIONS_KEY, true) - const { borrowBank, positions } = usePositions(showInactivePositions) + const { positions, jlpBorrowBank, lstBorrowBank } = usePositions( + showInactivePositions, + ) + + console.log(positions) const numberOfPositions = useMemo(() => { if (!positions.length) return 0 @@ -69,12 +73,14 @@ const Positions = ({
{positions.length ? ( positions.map((position) => { + const { bank } = position + const isUsdcBorrow = bank.name === 'JLP' || bank.name === 'USDC' return position.bank ? ( ) : null }) @@ -97,7 +103,7 @@ const PositionItem = ({ setActiveTab: (v: ActiveTab) => void borrowBank: Bank | undefined }) => { - const { group } = useMangoGroup() + const { jlpGroup, lstGroup } = useMangoGroup() const { stakeBalance, borrowBalance, bank, pnl, acct } = position const handleAddOrManagePosition = (token: string) => { @@ -108,7 +114,10 @@ const PositionItem = ({ } 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( @@ -120,7 +129,7 @@ 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', ''] diff --git a/components/Stake.tsx b/components/Stake.tsx index 85bdf9f..e68b60d 100644 --- a/components/Stake.tsx +++ b/components/Stake.tsx @@ -9,11 +9,15 @@ import { formatTokenSymbol } from 'utils/tokens' import { useViewport } from 'hooks/useViewport' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid' import DespositForm from './DepositForm' +import { EnterBottomExitBottom } from './shared/Transitions' +import TokenSelect from './TokenSelect' +import Label from './forms/Label' const set = mangoStore.getState().set const Stake = () => { const [activeFormTab, setActiveFormTab] = useState('Add') + const [showTokenSelect, setShowTokenSelect] = useState(false) const selectedToken = mangoStore((s) => s.selectedToken) const { isDesktop } = useViewport() @@ -21,25 +25,33 @@ const Stake = () => { set((state) => { state.selectedToken = token }) + setShowTokenSelect(false) }, []) const swapUrl = `https://app.mango.markets/swap?in=USDC&out=${selectedToken}&walletSwap=true` return ( <> -
- {STAKEABLE_TOKENS.map((token) => ( - - ))} -
-
+
+ +

+ Select token to {activeFormTab === 'Add' ? 'Boost!' : 'Unboost'} +

+
+ {STAKEABLE_TOKENS.map((token) => ( + handleTokenSelect(token)} + tokenName={token} + /> + ))} +
+
@@ -49,6 +61,13 @@ const Stake = () => { onChange={(v) => setActiveFormTab(v)} />
+
+
{selectedToken == 'USDC' ? ( <> {activeFormTab === 'Add' ? : null} diff --git a/components/StakeForm.tsx b/components/StakeForm.tsx index b114a24..a367117 100644 --- a/components/StakeForm.tsx +++ b/components/StakeForm.tsx @@ -43,6 +43,7 @@ import ButtonGroup from './forms/ButtonGroup' import Decimal from 'decimal.js' import { toUiDecimals } from '@blockworks-foundation/mango-v4' import useIpAddress from 'hooks/useIpAddress' +import { JLP_BORROW_TOKEN, LST_BORROW_TOKEN } from 'utils/constants' const set = mangoStore.getState().set @@ -57,8 +58,12 @@ export const walletBalanceForToken = ( walletTokens: TokenAccount[], token: string, ): { maxAmount: number; maxDecimals: number } => { - const group = mangoStore.getState().group - const bank = group?.banksMapByName.get(token)?.[0] + const jlpGroup = mangoStore.getState().group.jlpGroup + const lstGroup = mangoStore.getState().group.lstGroup + const isJlpGroup = token === 'JLP' || token === 'USDC' + const bank = isJlpGroup + ? jlpGroup?.banksMapByName.get(token)?.[0] + : lstGroup?.banksMapByName.get(token)?.[0] let walletToken if (bank) { @@ -99,7 +104,7 @@ 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, @@ -107,13 +112,16 @@ function StakeForm({ token: selectedToken }: StakeFormProps) { ) 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 [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 liquidationPrice = useMemo(() => { const price = Number(stakeBank?.uiPrice) @@ -171,7 +179,7 @@ function StakeForm({ token: selectedToken }: StakeFormProps) { }, [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 +188,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,17 +199,20 @@ 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 jlpGroup = mangoStore.getState().group.jlpGroup + const lstGroup = mangoStore.getState().group.lstGroup + const isJlpGroup = stakeBank.name === 'JLP' || stakeBank.name === 'USDC' + const group = isJlpGroup ? jlpGroup : lstGroup 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 + if (!group) return console.log(mangoAccounts) set((state) => { state.submittingBoost = true @@ -279,7 +290,10 @@ function StakeForm({ token: selectedToken }: StakeFormProps) { } useEffect(() => { - const group = mangoStore.getState().group + const jlpGroup = mangoStore.getState().group.jlpGroup + const lstGroup = mangoStore.getState().group.lstGroup + const isJlpGroup = selectedToken === 'JLP' || selectedToken === 'USDC' + const group = isJlpGroup ? jlpGroup : lstGroup set((state) => { state.swap.outputBank = group?.banksMapByName.get(selectedToken)?.[0] }) diff --git a/components/TokenButton.tsx b/components/TokenButton.tsx index 1fff81a..3f7c092 100644 --- a/components/TokenButton.tsx +++ b/components/TokenButton.tsx @@ -4,15 +4,14 @@ 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 groupLoaded = mangoStore((s) => s.groupLoaded) @@ -36,54 +35,41 @@ const TokenButton = ({ return ( ) diff --git a/components/TokenSelect.tsx b/components/TokenSelect.tsx new file mode 100644 index 0000000..d77ae68 --- /dev/null +++ b/components/TokenSelect.tsx @@ -0,0 +1,71 @@ +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' + +const TokenSelect = ({ + onClick, + tokenName, +}: { + tokenName: string + onClick: () => void +}) => { + const leverage = useLeverageMax(tokenName) * 0.9 + const groupLoaded = mangoStore((s) => s.groupLoaded) + + const { stakeBankDepositRate, financialMetrics } = useBankRates( + tokenName, + leverage, + ) + + const { financialMetrics: estimatedNetAPYFor1xLev } = useBankRates( + tokenName, + 1, + ) + + const APY_Daily_Compound = + Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1 + const UiRate = + tokenName === 'USDC' + ? APY_Daily_Compound * 100 + : Math.max(estimatedNetAPYFor1xLev.APY, financialMetrics.APY) + + return ( + + ) +} + +export default TokenSelect diff --git a/components/UnstakeForm.tsx b/components/UnstakeForm.tsx index 0e836a8..e7e558d 100644 --- a/components/UnstakeForm.tsx +++ b/components/UnstakeForm.tsx @@ -9,7 +9,7 @@ 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 { formatTokenSymbol } from '../utils/tokens' // import ActionTokenList from './account/ActionTokenList' import Label from './forms/Label' import Button, { IconButton } from './shared/Button' @@ -39,6 +39,7 @@ import { Disclosure } from '@headlessui/react' import { sleep } from 'utils' import useIpAddress from 'hooks/useIpAddress' import { AnchorProvider } from '@project-serum/anchor' +import { JLP_BORROW_TOKEN, LST_BORROW_TOKEN } from 'utils/constants' const set = mangoStore.getState().set @@ -46,27 +47,6 @@ 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 UnstakeForm({ token: selectedToken }: UnstakeFormProps) { const { t } = useTranslation(['common', 'account']) const [inputAmount, setInputAmount] = useState('') @@ -79,17 +59,28 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) { 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 [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 borrowBank = useMemo(() => { - return group?.banksMapByName.get('USDC')?.[0] - }, [group]) + // 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 @@ -176,16 +167,18 @@ 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 jlpGroup = mangoStore.getState().group.jlpGroup + const lstGroup = mangoStore.getState().group.lstGroup + const isJlpGroup = stakeBank.name === 'JLP' || stakeBank.name === 'USDC' + const group = isJlpGroup ? jlpGroup : lstGroup 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 { @@ -265,10 +258,16 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) { } }, [ipAllowed, stakeBank, borrowBank, publicKey, inputAmount, leverage]) - const maxWithdraw = - group && mangoAccount && stakeBank - ? mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, stakeBank.mint) - : 0 + const maxWithdraw = useMemo(() => { + if (!mangoAccount || !stakeBank) return 0 + const isJlpGroup = stakeBank.name === 'JLP' || stakeBank.name === 'USDC' + const group = isJlpGroup ? jlpGroup : lstGroup + if (!group) return 0 + return mangoAccount.getMaxWithdrawWithBorrowForTokenUi( + group, + stakeBank.mint, + ) + }, [jlpGroup, lstGroup, mangoAccount, stakeBank]) const showInsufficientBalance = tokenMax.maxAmount < Number(inputAmount) || @@ -280,7 +279,10 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) { Number(inputAmount) > maxWithdraw useEffect(() => { - const group = mangoStore.getState().group + const jlpGroup = mangoStore.getState().group.jlpGroup + const lstGroup = mangoStore.getState().group.lstGroup + const isJlpGroup = selectedToken === 'JLP' || selectedToken === 'USDC' + const group = isJlpGroup ? jlpGroup : lstGroup set((state) => { state.swap.outputBank = group?.banksMapByName.get(selectedToken)?.[0] }) diff --git a/components/WithdrawForm.tsx b/components/WithdrawForm.tsx deleted file mode 100644 index bb9b793..0000000 --- a/components/WithdrawForm.tsx +++ /dev/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 ( - <> -
-
- -
-
-
-
-
- { - setInputAmount( - !Number.isNaN(Number(e.value)) ? e.value : '', - ) - }} - isAllowed={withValueLimit} - /> -
- -
-
- - {formatTokenSymbol(selectedToken)} - -
-
-
-
- handleSizePercentage(p)} - values={['10', '25', '50', '75', '100']} - unit="%" - /> -
-
- {stakeBank && borrowBank ? ( -
- - {({ open }) => ( - <> - -
-

Staked Amount

-
- - - - -
-
-
- -
-

Staked Amount

- - - -
-
-

USDC borrowed

- {borrowBank ? ( - 0.001 - ? 'text-th-fgd-1' - : 'text-th-bkg-4' - }`} - > - - - ) : null} -
-
- - )} -
-
- ) : null} -
- {connected ? ( - - ) : ( - - )} - {tokenPositionsFull ? ( - - {t('error-token-positions-full')}{' '} - - {t('manage')} - - - } - /> - ) : null} -
- - ) -} - -export default WithdrawForm diff --git a/components/shared/TokenLogo.tsx b/components/shared/TokenLogo.tsx index 1911de5..7756aac 100644 --- a/components/shared/TokenLogo.tsx +++ b/components/shared/TokenLogo.tsx @@ -13,7 +13,6 @@ const TokenLogo = ({ size?: number }) => { const { mangoTokens } = useJupiterMints() - const logoUri = useMemo(() => { if (!bank) return '' const tokenSymbol = bank.name.toLowerCase() diff --git a/components/stats/HistoricalStats.tsx b/components/stats/HistoricalStats.tsx index 6328fe4..4a57483 100644 --- a/components/stats/HistoricalStats.tsx +++ b/components/stats/HistoricalStats.tsx @@ -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, }, ) diff --git a/components/stats/StatsPage.tsx b/components/stats/StatsPage.tsx index 3613390..182e2db 100644 --- a/components/stats/StatsPage.tsx +++ b/components/stats/StatsPage.tsx @@ -11,21 +11,23 @@ import { formatTokenSymbol } from 'utils/tokens' import HistoricalStats from './HistoricalStats' const StatsPage = () => { - const { group } = useMangoGroup() + const { jlpGroup, lstGroup } = useMangoGroup() const { t } = useTranslation('common') const { isMobile } = useViewport() const banks = useMemo(() => { - if (!group) return [] - const positionBanks = [] + const statsBanks = [] 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) + statsBanks.push(bank) } } - return positionBanks - }, [group]) + return statsBanks + }, [jlpGroup, lstGroup]) return (
diff --git a/hooks/useAccountHistory.ts b/hooks/useAccountHistory.ts index a1731e2..0eea3cd 100644 --- a/hooks/useAccountHistory.ts +++ b/hooks/useAccountHistory.ts @@ -40,16 +40,18 @@ 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( @@ -64,7 +66,7 @@ export default function useAccountHistory() { return mangoAccountPda.toString() }) return x - }, [group, wallet]) + }, [jlpGroup, lstGroup, wallet]) const activeStakeAccts = stakeAccounts?.map((acc) => acc.publicKey.toString()) ?? [] diff --git a/hooks/useBankRates.ts b/hooks/useBankRates.ts index 416dce8..24929ce 100644 --- a/hooks/useBankRates.ts +++ b/hooks/useBankRates.ts @@ -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 diff --git a/hooks/useJupiterMints.ts b/hooks/useJupiterMints.ts index 4c812bf..e869eaf 100644 --- a/hooks/useJupiterMints.ts +++ b/hooks/useJupiterMints.ts @@ -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 { jlpGroup, lstGroup } = mangoStore.getState().group + if (!jlpGroup || !lstGroup) 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(jlpGroup.banksMapByName.values()).map((b) => b[0].mint.toString(), ) - const mangoTokens = data.filter((t) => bankMints.includes(t.address)) + const lstBankMints = Array.from(lstGroup.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 || [], diff --git a/hooks/useLeverageMax.ts b/hooks/useLeverageMax.ts index 78041c1..f9fcd62 100644 --- a/hooks/useLeverageMax.ts +++ b/hooks/useLeverageMax.ts @@ -1,24 +1,28 @@ import { useMemo } from 'react' import useMangoGroup from './useMangoGroup' import { floorToDecimal } from 'utils/numbers' +import { JLP_BORROW_TOKEN, LST_BORROW_TOKEN } from 'utils/constants' 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 x = stakeInitAssetWeight.toNumber() / borrowInitLiabWeight.toNumber() diff --git a/hooks/useMangoGroup.ts b/hooks/useMangoGroup.ts index 502b5ca..f103186 100644 --- a/hooks/useMangoGroup.ts +++ b/hooks/useMangoGroup.ts @@ -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.jlpGroup) + const lstGroup = mangoStore((s) => s.group.lstGroup) - return { group } + return { jlpGroup, lstGroup } } diff --git a/hooks/usePositions.ts b/hooks/usePositions.ts index c80d559..80f338e 100644 --- a/hooks/usePositions.ts +++ b/hooks/usePositions.ts @@ -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 } } diff --git a/hooks/useSelectedMarket.ts b/hooks/useSelectedMarket.ts deleted file mode 100644 index c89e275..0000000 --- a/hooks/useSelectedMarket.ts +++ /dev/null @@ -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, - } -} diff --git a/hooks/useStakeRates.ts b/hooks/useStakeRates.ts index 4998071..ca0499c 100644 --- a/hooks/useStakeRates.ts +++ b/hooks/useStakeRates.ts @@ -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 = {} - 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.jito = + (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 {} diff --git a/store/mangoStore.ts b/store/mangoStore.ts index 1fa40d6..8b6489b 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -35,12 +35,9 @@ import { BOOST_ACCOUNT_PREFIX, BOOST_DATA_API_URL, CONNECTION_COMMITMENT, - DEFAULT_MARKET_NAME, FALLBACK_ORACLES, - INPUT_TOKEN_DEFAULT, MANGO_DATA_API_URL, MAX_PRIORITY_FEE_KEYS, - OUTPUT_TOKEN_DEFAULT, PAGINATION_PAGE_LENGTH, RPC_PROVIDER_KEY, STAKEABLE_TOKENS, @@ -66,9 +63,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, @@ -84,7 +79,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 = [ { @@ -162,7 +159,7 @@ export type MangoStore = { } connected: boolean connection: Connection - group: Group | undefined + group: { jlpGroup: Group | undefined; lstGroup: Group | undefined } groupLoaded: boolean client: MangoClient showUserSetup: boolean @@ -327,7 +324,7 @@ const mangoStore = create()( }, connected: false, connection, - group: undefined, + group: { jlpGroup: undefined, lstGroup: undefined }, groupLoaded: false, client, showUserSetup: false, @@ -531,61 +528,17 @@ const mangoStore = create()( 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 jlpGroup = await client.getGroup(GROUP_JLP) + const lstGroup = await client.getGroup(GROUP_V1) set((state) => { - state.group = group + state.group.jlpGroup = jlpGroup + state.group.lstGroup = 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) => { @@ -632,20 +585,33 @@ const mangoStore = create()( }, fetchMangoAccounts: async (ownerPk: PublicKey) => { const set = get().set - // const actions = get().actions try { - const group = get().group + const jlpGroup = get().group.jlpGroup + const lstGroup = get().group.lstGroup const client = get().client const selectedMangoAccount = get().mangoAccount.current const selectedToken = get().selectedToken - if (!group) throw new Error('Group not loaded') + 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.getMangoAccountsForOwner(jlpGroup, ownerPk), + client.getMangoAccountsForOwner(lstGroup, ownerPk), + client.getMangoAccountsForDelegate(jlpGroup, ownerPk), + client.getMangoAccountsForDelegate(lstGroup, ownerPk), ]) - const mangoAccounts = [...ownerMangoAccounts, ...delegateAccounts] + const mangoAccounts = [ + ...jlpOwnerMangoAccounts, + ...lstOwnerMangoAccounts, + ...jlpDelegateAccounts, + ...lstDelegateAccounts, + ] console.log('mango accounts: ', mangoAccounts) const selectedAccountIsNotInAccountsList = mangoAccounts.find( (x) => @@ -675,16 +641,10 @@ const mangoStore = create()( } 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 @@ -902,14 +862,4 @@ const mangoStore = create()( }), ) -mangoStore.subscribe((state) => state.mangoAccount.current, spotBalancesUpdater) -mangoStore.subscribe( - (state) => state.mangoAccount.openOrderAccounts, - spotBalancesUpdater, -) -mangoStore.subscribe( - (state) => state.mangoAccount.current, - perpPositionsUpdater, -) - export default mangoStore diff --git a/store/perpPositionsUpdater.ts b/store/perpPositionsUpdater.ts deleted file mode 100644 index 2add065..0000000 --- a/store/perpPositionsUpdater.ts +++ /dev/null @@ -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 diff --git a/store/spotBalancesUpdater.ts b/store/spotBalancesUpdater.ts deleted file mode 100644 index d29c672..0000000 --- a/store/spotBalancesUpdater.ts +++ /dev/null @@ -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 diff --git a/utils/constants.ts b/utils/constants.ts index d023231..15fc358 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,11 +1,40 @@ 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' }, + { + name: 'JLP', + id: 1, + active: true, + mint_address: '27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4', + }, + { + name: 'USDC', + id: 0, + active: true, + mint_address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + }, + { + name: 'MSOL', + id: 521, + active: true, + mint_address: 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So', + }, + { + name: 'JitoSOL', + id: 621, + active: true, + mint_address: 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn', + }, + { + name: 'bSOL', + id: 721, + active: true, + mint_address: 'bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1', + }, ] export const STAKEABLE_TOKENS = STAKEABLE_TOKENS_DATA.filter( (d) => d.active, @@ -27,6 +56,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 +182,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = { 'eth (portal)': true, hnt: true, jitosol: true, + jlp: true, kin: true, ldo: true, mngo: true, diff --git a/utils/orderbook.ts b/utils/orderbook.ts deleted file mode 100644 index 60c8a1f..0000000 --- a/utils/orderbook.ts +++ /dev/null @@ -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, - 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 = {} - 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 -}