diff --git a/components/EditLeverageForm.tsx b/components/EditLeverageForm.tsx new file mode 100644 index 0000000..1cb6224 --- /dev/null +++ b/components/EditLeverageForm.tsx @@ -0,0 +1,596 @@ +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 mangoStore from '@store/mangoStore' +import { notify } from '../utils/notifications' +import { TokenAccount, formatTokenSymbol } from '../utils/tokens' +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 { isMangoError } from 'types' +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 { 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 LeverageSlider from './shared/LeverageSlider' +import useMangoAccount from 'hooks/useMangoAccount' +import { + ChevronDownIcon, +} from '@heroicons/react/20/solid' +import { useEffect } from 'react' +import { + formatNumericValue, +} from 'utils/numbers' +import FormatNumericValue from './shared/FormatNumericValue' +import { stakeAndCreate } from 'utils/transactions' +// import { MangoAccount } from '@blockworks-foundation/mango-v4' +import useBankRates from 'hooks/useBankRates' +import { Disclosure } from '@headlessui/react' +import useLeverageMax from 'hooks/useLeverageMax' +import { toNativeI80F48, toUiDecimals, toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' +import { token } from '@project-serum/anchor/dist/cjs/utils' +import { simpleSwap } from 'utils/transactions' + +const set = mangoStore.getState().set + +export const NUMBERFORMAT_CLASSES = + 'inner-shadow-top-sm w-full rounded-xl border border-th-bkg-3 bg-th-input-bkg p-3 pl-12 pr-4 text-left font-bold text-xl 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' + +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 ( +// accounts +// .map((a) => a.accountNum) +// .reduce((a, b) => Math.max(a, b), -Infinity) + 1 +// ) +// } else if (accounts.length === 1) { +// return accounts[0].accountNum + 1 +// } +// return 0 +// } + +function EditLeverageForm({ token: selectedToken }: StakeFormProps) { + 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 { 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 stakeBankAmount = + mangoAccount && stakeBank && mangoAccount?.getTokenBalance(stakeBank) + + const borrowAmount = + 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)); + return Math.sign(lev) !== -1 ? lev : 1 + } + return 1 + } catch (e) { + console.log(e) + return 1 + } + }, [stakeBankAmount, borrowAmount, stakeBank]) + + const [leverage, setLeverage] = useState(current_leverage) + const { financialMetrics, borrowBankBorrowRate } = useBankRates( + selectedToken, + leverage, + ) + + const liquidationPrice = useMemo(() => { + const price = Number(stakeBank?.uiPrice) + const borrowMaintLiabWeight = Number(borrowBank?.maintLiabWeight) + const stakeMaintAssetWeight = Number(stakeBank?.maintAssetWeight) + const loanOriginationFee = Number(borrowBank?.loanOriginationFeeRate) + const liqPrice = + price * + ((borrowMaintLiabWeight * (1 + loanOriginationFee)) / + stakeMaintAssetWeight) * + (1 - 1 / leverage) + return liqPrice.toFixed(3) + }, [stakeBank, borrowBank, leverage]) + + 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) / current_leverage, + maxDecimals: stakeBank.mintDecimals, + } + }, [stakeBank, mangoAccount, current_leverage]) + + const amountToBorrow = useMemo(() => { + const borrowPrice = borrowBank?.uiPrice + const stakePrice = stakeBank?.uiPrice + if (!borrowPrice || !stakePrice || !Number(tokenMax.maxAmount)) return 0 + const borrowAmount = + stakeBank?.uiPrice * Number(tokenMax.maxAmount) * (leverage - 1) + return borrowAmount + }, [leverage, borrowBank, stakeBank, tokenMax]) + + const availableVaultBalance = useMemo(() => { + if (!borrowBank || !group) return 0 + const maxUtilization = 1 - borrowBank.minVaultToDepositsRatio + const vaultBorrows = borrowBank.uiBorrows() + const vaultDeposits = borrowBank.uiDeposits() + const loanOriginationFeeFactor = + 1 - borrowBank.loanOriginationFeeRate.toNumber() - 1e-6 + const available = + (maxUtilization * vaultDeposits - vaultBorrows) * loanOriginationFeeFactor + return available + }, [borrowBank, group]) + + const changeInJLP = Number(((leverage * tokenMax?.maxAmount) - toUiDecimals(stakeBankAmount, stakeBank?.mintDecimals)).toFixed(2)) + const changeInUSDC = Number((- amountToBorrow - toUiDecimals(borrowAmount, borrowBank?.mintDecimals)).toFixed(2)) + + const handleChangeLeverage = useCallback(async () => { + if (!ipAllowed) { + console.log('IP NOT PERMITTED') + return + } + + const client = mangoStore.getState().client + const group = mangoStore.getState().group + const actions = mangoStore.getState().actions + const mangoAccount = mangoStore.getState().mangoAccount.current + const mangoAccounts = mangoStore.getState().mangoAccounts + + if (!group || !stakeBank || !publicKey || !mangoAccount) return + console.log(mangoAccounts) + set((state) => { + state.submittingBoost = true + }) + try { + + notify({ + title: 'Building transaction. This may take a moment.', + type: 'info', + }) + + console.log(-changeInUSDC, changeInJLP) + if (changeInJLP > 0){ + console.log('Swapping From USDC to JLP') + const { signature: tx, slot } = await simpleSwap( + client, + group, + mangoAccount, + borrowBank?.mint, + stakeBank?.mint, + - changeInUSDC, + ) + notify({ + title: 'Transaction confirmed', + type: 'success', + txid: tx, + }) + } + else{ + console.log('Swapping From JLP to USDC') + const { signature: tx, slot } = await simpleSwap( + client, + group, + mangoAccount, + stakeBank?.mint, + borrowBank?.mint, + - changeInJLP, + ) + notify({ + title: 'Transaction confirmed', + type: 'success', + txid: tx, + }) + } + + + set((state) => { + state.submittingBoost = false + }) + await sleep(500) + if (!mangoAccount) { + await actions.fetchMangoAccounts( + (client.program.provider as AnchorProvider).wallet.publicKey, + ) + } + await actions.reloadMangoAccount(slot) + await actions.fetchWalletTokens(publicKey) + } catch (e) { + console.error('Error depositing:', e) + set((state) => { + state.submittingBoost = false + }) + if (!isMangoError(e)) return + notify({ + title: 'Transaction failed', + description: e.message, + txid: e?.txid, + type: 'error', + }) + } + }, [ipAllowed, stakeBank, publicKey, amountToBorrow]) + + const tokenDepositLimitLeft = stakeBank?.getRemainingDepositLimit() + const tokenDepositLimitLeftUi = + stakeBank && tokenDepositLimitLeft + ? toUiDecimals(tokenDepositLimitLeft, stakeBank?.mintDecimals) + : 0 + + const depositLimitExceeded = + tokenDepositLimitLeftUi !== null + ? Number(tokenMax.maxAmount) > tokenDepositLimitLeftUi + : false + + const changeLeverage = (v: number) => { + setLeverage(v * 1) + if (Math.round(v) != storedLeverage) { + set((state) => { + state.leverage = Math.round(v) + }) + } + } + + useEffect(() => { + const group = mangoStore.getState().group + set((state) => { + state.swap.outputBank = group?.banksMapByName.get(selectedToken)?.[0] + }) + }, [selectedToken]) + + + return ( + <> +
+
+ {availableVaultBalance < amountToBorrow && borrowBank && ( +
+ + The available {borrowBank?.name} vault balance is low and + impacting the maximum amount you can borrow +
+ } + /> +
+ )} +
+ {depositLimitExceeded ? ( +
+ +
+ ) : null} +
+
+
+
+ +
+ {stakeBank && borrowBank ? ( +
+ + {({ open }) => ( + <> + +
+

Est. Net APY

+
+ 0.001 + ? 'text-th-success' + : 'text-th-error' + }`} + > + {financialMetrics.APY >= 0 + ? '+' + : financialMetrics.APY === 0 + ? '' + : ''} + + % + + +
+
+
+ +

+ Position +

+
+
+

Size

+
+ 0.001 + ? 'text-th-fgd-1' + : 'text-th-bkg-4' + }`} + > + + ( + ) + + {' '} + {stakeBank.name}{' '} + + +

+ {' '} + {borrowBank.name} +

+
+
+
+

{`${borrowBank.name} Borrowed`}

+ 0.001 + ? 'text-th-fgd-1' + : 'text-th-bkg-4' + }`} + > + + ( + ) + +
+
+

{`Est. Liquidation Price`}

+ 0.001 + ? 'text-th-fgd-1' + : 'text-th-bkg-4' + }`} + > + $ + + +
+
+

+ Rates and Fees +

+
+
+

+ {formatTokenSymbol(selectedToken)} Yield APY +

+ + {financialMetrics.collectedReturnsAPY > 0.01 + ? '+' + : ''} + + % + +
+
+

+ {formatTokenSymbol(selectedToken)} Collateral Fee + APY +

+ 0.01 + ? 'text-th-error' + : 'text-th-bkg-4' + }`} + > + {financialMetrics?.collateralFeeAPY > 0.01 + ? '-' + : ''} + + % + +
+ {borrowBank ? ( + <> +
+

{`${borrowBank?.name} Borrow APY`}

+ 0.01 + ? 'text-th-error' + : 'text-th-bkg-4' + }`} + > + - + + % + +
+
+

+ Loan Origination Fee +

+ + {amountToBorrow ? ( + + + + ) : ( + + 0 + + )} + + {' '} + {borrowBank.name}{' '} + + +
+ + ) : null} +
+
+ + )} +
+
+ ) : !groupLoaded ? ( +
+ +
+ +
+ ) : null} +
+ {connected ? ( + + ) : ( + + )} + {tokenPositionsFull ? ( + + {t('error-token-positions-full')}{' '} + + {t('manage')} + + + } + /> + ) : null} +
+ + ) +} + +export default EditLeverageForm diff --git a/components/Positions.tsx b/components/Positions.tsx index 7114d7a..ab2df87 100644 --- a/components/Positions.tsx +++ b/components/Positions.tsx @@ -1,5 +1,5 @@ import useMangoGroup from 'hooks/useMangoGroup' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { SHOW_INACTIVE_POSITIONS_KEY } from 'utils/constants' import TokenLogo from './shared/TokenLogo' import Button from './shared/Button' @@ -15,6 +15,8 @@ import { } from '@blockworks-foundation/mango-v4' import useBankRates from 'hooks/useBankRates' import usePositions from 'hooks/usePositions' +import { PencilIcon } from '@heroicons/react/20/solid' +import EditLeverageModal from './modals/EditLeverageModal' const set = mangoStore.getState().set @@ -55,9 +57,8 @@ const Positions = ({ return ( <>
-

{`You have ${numberOfPositions} active position${ - numberOfPositions !== 1 ? 's' : '' - }`}

+

{`You have ${numberOfPositions} active position${numberOfPositions !== 1 ? 's' : '' + }`}

setShowInactivePositions(checked)} @@ -105,6 +106,7 @@ const PositionItem = ({ state.selectedToken = token }) } + const [showEditLeverageModal, setShowEditLeverageModal] = useState(false) const leverage = useMemo(() => { if (!group || !acct) return 1 @@ -160,7 +162,7 @@ const PositionItem = ({
@@ -194,13 +196,12 @@ const PositionItem = ({

Total Earned

= 0 + className={`text-xl font-bold ${!stakeBalance + ? 'text-th-fgd-4' + : pnl >= 0 ? 'text-th-success' : 'text-th-error' - }`} + }`} > {stakeBalance || pnl ? ( @@ -213,9 +214,14 @@ const PositionItem = ({ <>

Leverage

- - {leverage ? leverage.toFixed(2) : 0.0}x - +
+ + {leverage ? leverage.toFixed(2) : 0.0}x + + +

Est. Liquidation Price

@@ -235,6 +241,14 @@ const PositionItem = ({ )}
+ {showEditLeverageModal ? ( + setShowEditLeverageModal(false)} + /> + ) : null}
) } diff --git a/components/modals/EditLeverageModal.tsx b/components/modals/EditLeverageModal.tsx new file mode 100644 index 0000000..1642421 --- /dev/null +++ b/components/modals/EditLeverageModal.tsx @@ -0,0 +1,31 @@ +import { ModalProps } from '../../types/modal' +import Modal from '../shared/Modal' +import EditLeverageForm from '@components/EditLeverageForm' + +interface DepositWithdrawModalProps { + action: 'deposit' | 'withdraw' + token?: string | undefined +} +type ModalCombinedProps = DepositWithdrawModalProps & ModalProps + +const EditLeverageModal = ({ + isOpen, + onClose, + token, +}: ModalCombinedProps) => { + return ( + <> + +
+ <> +
+ +
+ +
+
+ + ) +} + +export default EditLeverageModal diff --git a/utils/transactions.ts b/utils/transactions.ts index 54328e2..df97608 100644 --- a/utils/transactions.ts +++ b/utils/transactions.ts @@ -191,6 +191,82 @@ export const unstakeAndSwap = async ( }) } +export const simpleSwap = async ( + client: MangoClient, + group: Group, + mangoAccount: MangoAccount, + inputMintPk: PublicKey, + outputMintPk: PublicKey, + amount: number, // Amount of input token to swap + slippage = 1 // Slippage tolerance in percentage +) => { + console.log('Performing simple swap'); + const instructions: TransactionInstruction[] = [] + + // Fetching the input and output banks from the group + const inputBank = group?.banksMapByMint.get(inputMintPk.toString())?.[0]; + const outputBank = group?.banksMapByMint.get(outputMintPk.toString())?.[0]; + + if (!inputBank || !outputBank) { + throw Error('Unable to find input bank or output bank'); + } + + console.log(amount) + + // Calculate the native amount for the swap + const nativeAmount = toNative(amount, inputBank.mintDecimals); + + // Step 1: Fetch the best swap route from Jupiter + const { bestRoute: selectedRoute } = await fetchJupiterRoutes( + inputMintPk.toString(), + outputMintPk.toString(), + nativeAmount.toNumber(), + slippage, + 'ExactIn' + ); + + if (!selectedRoute) { + throw Error('Unable to find a swap route'); + } + + // Step 2: Fetch Jupiter swap instructions + const payer = (client.program.provider as AnchorProvider).wallet.publicKey; + const [jupiterIxs, jupiterAlts] = await fetchJupiterTransaction( + client.program.provider.connection, + selectedRoute, + payer, + slippage, + inputMintPk, + outputMintPk, + ); + + const swapHealthRemainingAccounts: PublicKey[] = mangoAccount + ? client.buildHealthRemainingAccounts(group, [mangoAccount], [], [], []) + : [inputBank?.publicKey, outputBank?.oracle] + + let swapAlts: AddressLookupTableAccount[] = [] + const [swapIxs, alts] = await createSwapIxs({ + client: client, + group: group, + mangoAccountPk: mangoAccount.publicKey, + owner: payer, + inputMintPk: inputBank?.mint, + amountIn: toUiDecimals(selectedRoute.inAmount, inputBank?.mintDecimals), + outputMintPk: outputBank?.mint, + userDefinedInstructions: jupiterIxs, + userDefinedAlts: jupiterAlts, + flashLoanType: FlashLoanType.swap, + swapHealthRemainingAccounts, + }) + swapAlts = alts + instructions.push(...swapIxs) + + // Step 4: Send and confirm the transaction + return await client.sendAndConfirmTransactionForGroup(group, [...swapIxs], { + alts: [...group.addressLookupTablesList, ...swapAlts], + }); +}; + export const stakeAndCreate = async ( client: MangoClient, group: Group, @@ -246,12 +322,12 @@ export const stakeAndCreate = async ( const depositHealthRemainingAccounts: PublicKey[] = mangoAccount ? client.buildHealthRemainingAccounts( - group, - [mangoAccount], - [stakeBank], - [], - [], - ) + group, + [mangoAccount], + [stakeBank], + [], + [], + ) : [stakeBank.publicKey, stakeBank.oracle] const depositTokenIxs = await createDepositIx( client, @@ -280,6 +356,7 @@ export const stakeAndCreate = async ( slippage, ) + console.log(selectedRoute) if (!selectedRoute) { throw Error('Unable to find a swap route') } @@ -368,12 +445,12 @@ export const depositAndCreate = async ( const depositHealthRemainingAccounts: PublicKey[] = mangoAccount ? client.buildHealthRemainingAccounts( - group, - [mangoAccount], - [depositBank], - [], - [], - ) + group, + [mangoAccount], + [depositBank], + [], + [], + ) : [depositBank.publicKey, depositBank.oracle] const depositTokenIxs = await createDepositIx( @@ -510,11 +587,11 @@ const createSwapIxs = async ({ const healthRemainingAccounts: PublicKey[] = swapHealthRemainingAccounts ? swapHealthRemainingAccounts : [ - outputBank.publicKey, - inputBank.publicKey, - outputBank.oracle, - inputBank.oracle, - ] + outputBank.publicKey, + inputBank.publicKey, + outputBank.oracle, + inputBank.oracle, + ] // client.buildHealthRemainingAccounts( // group, // [], @@ -703,7 +780,7 @@ export const fetchJupiterTransaction = async ( const isDuplicateAta = (ix: TransactionInstruction): boolean => { return ( ix.programId.toString() === - 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' && + 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' && (ix.keys[3].pubkey.toString() === inputMint.toString() || ix.keys[3].pubkey.toString() === outputMint.toString()) )