diff --git a/apps/marginfi-v2-ui/package.json b/apps/marginfi-v2-ui/package.json index d9ce2b2268..c5395122f0 100644 --- a/apps/marginfi-v2-ui/package.json +++ b/apps/marginfi-v2-ui/package.json @@ -41,6 +41,7 @@ "firebase": "^9.22.1", "firebase-admin": "^11.9.0", "jsbi": "^4.3.0", + "lodash.debounce": "^4.0.8", "next": "13.4.19", "react": "18.2.0", "react-dom": "18.2.0", @@ -58,6 +59,7 @@ "devDependencies": { "@mrgnlabs/eslint-config-custom": "*", "@mrgnlabs/tsconfig": "*", + "@types/lodash.debounce": "^4.0.7", "@types/node": "18.11.18", "@types/numeral": "^2.0.2", "@types/react": "18.0.26", diff --git a/apps/marginfi-v2-ui/src/components/Staking/StakingCard/LstDepositToggle.tsx b/apps/marginfi-v2-ui/src/components/Staking/StakingCard/LstDepositToggle.tsx new file mode 100644 index 0000000000..25f032bf92 --- /dev/null +++ b/apps/marginfi-v2-ui/src/components/Staking/StakingCard/LstDepositToggle.tsx @@ -0,0 +1,88 @@ +import { styled, Switch, SwitchProps } from "@mui/material"; +import { Dispatch, SetStateAction } from "react"; + +interface LstDepositToggleProps extends SwitchProps { + checked: boolean; + setChecked: Dispatch>; +} + +const LstDepositToggle = styled(({ checked, setChecked, ...switchProps }: LstDepositToggleProps) => { + const handleChange = () => { + setChecked((prev) => !prev); + }; + + return ( + + ); +})(({ disabled }) => ({ + width: "100%", + height: "100%", + ...(disabled ? { cursor: "not-allowed" } : {}), + padding: 0, + backgroundColor: "rgba(0,0,0,1)", // @todo currently transparency is at 1 to hide the center thing that i can't make disappear + borderRadius: 5, + display: "flex", + justifyContent: "center", + alignItems: "center", + fontFamily: "Aeonik Pro", + fontWeight: 400, + "&:before": { + content: '"Tokens"', + width: "50%", + height: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + zIndex: 10, + pointerEvents: "none", + }, + "&:after": { + content: '"Native Stake"', + width: "50%", + height: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + zIndex: 10, + pointerEvents: "none", + }, + "& .MuiSwitch-switchBase": { + padding: "0.25rem", + width: "50%", + height: "100%", + display: "flex", + justifyContent: "center", + transitionDuration: "300ms", + transform: "translateX(0%)", + "& + .MuiSwitch-track": { + opacity: 0, + width: 0, + height: "100%", + }, + "&.Mui-disabled + .MuiSwitch-track": { + opacity: 0.5, + }, + "&.Mui-checked": { + transform: "translateX(100%)", + }, + }, + "&:hover .MuiSwitch-thumb": { + backgroundColor: "#394147", + }, + "& .MuiSwitch-thumb": { + boxSizing: "border-box", + width: "100%", + height: "100%", + backgroundColor: "#2F373D", + borderRadius: 4, + }, +})); + +export { LstDepositToggle }; diff --git a/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingCard.tsx b/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingCard.tsx index 0a429a4509..aa5472eb88 100644 --- a/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingCard.tsx +++ b/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingCard.tsx @@ -1,19 +1,19 @@ -import { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; -import { TextField, Typography, CircularProgress } from "@mui/material"; +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { TextField, Typography } from "@mui/material"; import * as solanaStakePool from "@solana/spl-stake-pool"; import { WalletIcon } from "./WalletIcon"; import { PrimaryButton } from "./PrimaryButton"; import { useLstStore } from "~/pages/stake"; import { useWalletContext } from "~/components/useWalletContext"; import { - ACCOUNT_SIZE, TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, createCloseAccountInstruction, - createInitializeAccountInstruction, getAssociatedTokenAddressSync, + nativeToUi, numeralFormatter, percentFormatter, + uiToNative, } from "@mrgnlabs/mrgn-common"; import { ArrowDropDown } from "@mui/icons-material"; import { StakingModal } from "./StakingModal"; @@ -26,91 +26,140 @@ import { PublicKey, SYSVAR_RENT_PUBKEY, Signer, + StakeAuthorizationLayout, + StakeProgram, SystemProgram, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; import { useConnection } from "@solana/wallet-adapter-react"; -import { SwapMode, useJupiter } from "@jup-ag/react-hook"; +import { JupiterProvider, SwapMode, useJupiter } from "@jup-ag/react-hook"; import JSBI from "jsbi"; import { usePrevious } from "~/utils"; -import { createJupiterApiClient, instanceOfSwapInstructionsResponse, Instruction } from "@jup-ag/api"; +import { + createJupiterApiClient, + Instruction, +} from "@jup-ag/api"; import { toast } from "react-toastify"; import { SettingsModal } from "./SettingsModal"; import { SettingsIcon } from "./SettingsIcon"; -import { SOL_MINT, TokenData, TokenDataMap } from "~/store/lstStore"; +import { LST_MINT, SOL_MINT, TokenData, TokenDataMap } from "~/store/lstStore"; import { RefreshIcon } from "./RefreshIcon"; import { StakePoolProxyProgram } from "~/utils/stakePoolProxy"; import { Spinner } from "~/components/Spinner"; +import { StakeData } from "~/utils/stakeAcounts"; +import BN from "bn.js"; +import debounce from "lodash.debounce"; const QUOTE_EXPIRY_MS = 30_000; +const DEFAULT_DEPOSIT_OPTION: DepositOption = { type: "native", amount: new BN(0), maxAmount: new BN(0) }; type OngoingAction = "swapping" | "minting"; +export type DepositOption = + | { + type: "native"; + amount: BN; + maxAmount: BN; + } + | { + type: "token"; + tokenData: TokenData; + amount: BN; + } + | { + type: "stake"; + stakeData: StakeData; + }; + export const StakingCard: FC = () => { const { connection } = useConnection(); const { connected, wallet, walletAddress, openWalletSelector } = useWalletContext(); - const [lstData, tokenDataMap, fetchLstState, slippagePct, setSlippagePct, stakePoolProxyProgram] = useLstStore( - (state) => [ - state.lstData, - state.tokenDataMap, - state.fetchLstState, - state.slippagePct, - state.setSlippagePct, - state.stakePoolProxyProgram, - ] - ); + const [ + lstData, + userDataFetched, + tokenDataMap, + stakeAccounts, + fetchLstState, + slippagePct, + setSlippagePct, + availableLamports, + solUsdValue, + ] = useLstStore((state) => [ + state.lstData, + state.userDataFetched, + state.tokenDataMap, + state.stakeAccounts, + state.fetchLstState, + state.slippagePct, + state.setSlippagePct, + state.availableLamports, + state.solUsdValue, + ]); const jupiterApiClient = createJupiterApiClient(); const [ongoingAction, setOngoingAction] = useState(null); const [refreshingQuotes, setRefreshingQuotes] = useState(false); - const [depositAmount, setDepositAmount] = useState(null); - const [selectedMint, setSelectedMint] = useState(SOL_MINT); + const [depositOption, setDepositOption] = useState(DEFAULT_DEPOSIT_OPTION); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); - const { slippage, slippageBps } = useMemo( - () => ({ slippage: slippagePct / 100, slippageBps: slippagePct * 100 }), + const slippageBps = useMemo( + () => slippagePct * 100, [slippagePct] ); - const prevSelectedMint = usePrevious(selectedMint); - useEffect(() => { - if (selectedMint?.toBase58() !== prevSelectedMint?.toBase58()) { - setDepositAmount(0); - } - }, [selectedMint, prevSelectedMint]); - const prevWalletAddress = usePrevious(walletAddress); useEffect(() => { if ((!walletAddress && prevWalletAddress) || (walletAddress && !prevWalletAddress)) { - setDepositAmount(0); + setDepositOption(DEFAULT_DEPOSIT_OPTION); } }, [walletAddress, prevWalletAddress]); - const selectedMintInfo: TokenData | null = useMemo(() => { - if (tokenDataMap === null) return null; - return tokenDataMap.get(selectedMint.toString()) ?? null; - }, [tokenDataMap, selectedMint]); + useEffect(() => { + setDepositOption((currentDepositOption) => { + if (currentDepositOption.type === "native") { + return { + ...currentDepositOption, + maxAmount: availableLamports ?? new BN(0), + }; + } else if (currentDepositOption.type === "token") { + if (!tokenDataMap) return currentDepositOption; + return { + ...currentDepositOption, + maxAmount: tokenDataMap.get(currentDepositOption.tokenData.address)?.balance ?? 0, + }; + } else { + return currentDepositOption; + } + }); + }, [availableLamports, tokenDataMap]); - const rawDepositAmount = useMemo( - () => Math.trunc(Math.pow(10, selectedMintInfo?.decimals ?? 0) * (depositAmount ?? 0)), - [depositAmount, selectedMintInfo] - ); + const depositAmountUi = useMemo(() => { + return depositOption.type === "native" + ? nativeToUi(depositOption.amount, 9) + : depositOption.type === "token" + ? nativeToUi(depositOption.amount, depositOption.tokenData.decimals) + : nativeToUi(depositOption.stakeData.lamports, 9); + }, [depositOption]); const { quoteResponseMeta, loading: loadingQuotes, refresh, lastRefreshTimestamp, + error, } = useJupiter({ - amount: selectedMint.equals(SOL_MINT) ? JSBI.BigInt(0) : JSBI.BigInt(rawDepositAmount), // amountIn trick to avoid SOL -> SOL quote calls - inputMint: selectedMint, - outputMint: SOL_MINT, + amount: depositOption.type === "stake" || depositOption.type === "native" ? JSBI.BigInt(0) : JSBI.BigInt(depositOption.amount), // amountIn trick to avoid Jupiter calls when depositing stake or native SOL + inputMint: + depositOption.type === "token" + ? new PublicKey(depositOption.tokenData.address) + : undefined, + outputMint: LST_MINT, swapMode: SwapMode.ExactIn, slippageBps, - debounceTime: 250, // debounce ms time before consecutive refresh calls + debounceTime: 250, }); const priceImpactPct: number | null = useMemo(() => { @@ -121,50 +170,90 @@ export const StakingCard: FC = () => { const refreshQuoteIfNeeded = useCallback( (force: boolean = false) => { const hasExpired = Date.now() - lastRefreshTimestamp > QUOTE_EXPIRY_MS; - if (!selectedMint.equals(SOL_MINT) && (depositAmount ?? 0 > 0) && (hasExpired || force)) { + if (depositOption.type === "token" && depositOption.amount.gtn(0) && (hasExpired || force)) { setRefreshingQuotes(true); refresh(); } }, - [selectedMint, depositAmount, refresh, lastRefreshTimestamp] + [depositOption, refresh, lastRefreshTimestamp] ); + const showErrotToast = useRef(debounce(() => toast.error("Failed to find route"), 250)); + + const prevError = usePrevious(error); useEffect(() => { - refreshQuoteIfNeeded(); - const id = setInterval(refreshQuoteIfNeeded, 1_000); - return () => clearInterval(id); - }, [refreshQuoteIfNeeded]); + if (prevError === undefined && error !== undefined) { + setDepositOption((currentDepositOption) => { + if (currentDepositOption.type === "token" && currentDepositOption.amount.gtn(0)) { + showErrotToast.current(); + return { + ...currentDepositOption, + amount: new BN(0), + }; + } else { + return currentDepositOption; + } + }); + } + }, [error, prevError]); useEffect(() => { if (!loadingQuotes) { - setTimeout(() => setRefreshingQuotes(false), 500); - } - }, [loadingQuotes]); + setTimeout(() => setRefreshingQuotes(false), 500); + } +}, [loadingQuotes]); - const lstOutAmount: number | null = useMemo(() => { - if (depositAmount === null) return null; - if (!selectedMint || !lstData?.lstSolValue) return 0; + const lstOutAmount: number = useMemo(() => { + if (!depositOption || !lstData?.lstSolValue) return 0; - if (selectedMint.equals(SOL_MINT)) { - return depositAmount / lstData.lstSolValue; + if (depositOption.type === "native") { + return nativeToUi(depositOption.amount, 9) / lstData.lstSolValue; + } else if (depositOption.type === "stake") { + return nativeToUi(depositOption.stakeData.lamports, 9) / lstData.lstSolValue; } else { if (quoteResponseMeta?.quoteResponse?.outAmount) { - const outAmount = JSBI.toNumber(quoteResponseMeta?.quoteResponse?.outAmount) / 1e9; - return outAmount / lstData.lstSolValue; + return JSBI.toNumber(quoteResponseMeta?.quoteResponse?.outAmount) / 1e9; } else { return 0; } } - }, [depositAmount, selectedMint, lstData?.lstSolValue, quoteResponseMeta?.quoteResponse?.outAmount]); + }, [depositOption, lstData?.lstSolValue, quoteResponseMeta?.quoteResponse?.outAmount]); const onChange = useCallback( - (event: NumberFormatValues) => setDepositAmount(event.floatValue ?? null), - [setDepositAmount] + (event: NumberFormatValues) => { + if (depositOption.type === "stake") return; + + setDepositOption((currentDepositOption) => { + const updatedAmount = + currentDepositOption.type === "native" + ? uiToNative(event.floatValue ?? 0, 9) + : currentDepositOption.type === "token" + ? uiToNative(event.floatValue ?? 0, currentDepositOption.tokenData.decimals) + : currentDepositOption.stakeData.lamports; + + return { + ...currentDepositOption, + amount: updatedAmount, + }; + }); + }, + [depositOption.type] ); + const maxDepositString = useMemo(() => { + if (!userDataFetched) return "-"; + if (depositOption.type === "token") { + const maxUi = nativeToUi(depositOption.tokenData.balance, depositOption.tokenData.decimals); + return maxUi < 0.01 ? "< 0.01" : numeralFormatter(maxUi); + } else if (depositOption.type === "native") { + const maxUi = nativeToUi(depositOption.maxAmount, 9); + return maxUi < 0.01 ? "< 0.01" : numeralFormatter(maxUi); + } + return "-"; + }, [userDataFetched, depositOption]); + const onMint = useCallback(async () => { - if (!lstData || !wallet || !walletAddress || !depositAmount || !stakePoolProxyProgram) return; - console.log("depositing", depositAmount, selectedMint); + if (!lstData || !wallet || !walletAddress) return; let sigs = []; @@ -172,17 +261,16 @@ export const StakingCard: FC = () => { value: { blockhash, lastValidBlockHeight }, } = await connection.getLatestBlockhashAndContext(); - try { - if (selectedMint.equals(SOL_MINT)) { - setOngoingAction("minting"); - const _depositAmount = depositAmount * 1e9; + setOngoingAction("minting"); - const { instructions, signers } = await makeDepositSolToStakePoolIx( + try { + if (depositOption.type === "stake") { + const { instructions, signers } = await makeDepositStakeToStakePoolIx( lstData.accountData, lstData.poolAddress, walletAddress, - _depositAmount, - undefined + depositOption.stakeData.validatorVoteAddress, + depositOption.stakeData.address ); const depositMessage = new TransactionMessage({ @@ -198,98 +286,54 @@ export const StakingCard: FC = () => { const depositSig = await connection.sendTransaction(signedTransaction); sigs.push(depositSig); - } else { - setOngoingAction("swapping"); + } else if (depositOption.type === "token") { const quote = quoteResponseMeta?.original; if (!quote) { - throw new Error("Route not calculated yet"); + console.error("Route not calculated yet"); + return; } - const destinationTokenAccountKeypair = Keypair.generate(); - - const minimumRentExemptionForTokenAccount = await connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE); - const createWSolAccountIx = SystemProgram.createAccount({ - fromPubkey: walletAddress, - newAccountPubkey: destinationTokenAccountKeypair.publicKey, - lamports: minimumRentExemptionForTokenAccount, - space: ACCOUNT_SIZE, - programId: TOKEN_PROGRAM_ID, - }); - const initWSolAccountIx = createInitializeAccountInstruction( - destinationTokenAccountKeypair.publicKey, - SOL_MINT, - walletAddress - ); - - // Craft swap tx - const swapInstructionsResult = await jupiterApiClient.swapInstructionsPost({ + const { swapTransaction: swapTransactionEncoded, lastValidBlockHeight } = await jupiterApiClient.swapPost({ swapRequest: { quoteResponse: quote, userPublicKey: walletAddress.toBase58(), wrapAndUnwrapSol: false, - destinationTokenAccount: destinationTokenAccountKeypair.publicKey.toBase58(), }, }); - if (!instanceOfSwapInstructionsResponse(swapInstructionsResult)) throw new Error("Invalid swap response"); - - const addressLookupTableAccounts: AddressLookupTableAccount[] = []; - - const swapInstructions = [ - ...swapInstructionsResult.computeBudgetInstructions.map(jupIxToSolanaIx), - createWSolAccountIx, - initWSolAccountIx, - ...swapInstructionsResult.setupInstructions.map(jupIxToSolanaIx), - jupIxToSolanaIx(swapInstructionsResult.swapInstruction), - ]; - if (swapInstructionsResult.cleanupInstruction) { - swapInstructions.push(jupIxToSolanaIx(swapInstructionsResult.cleanupInstruction)); - } + const swapTransactionBuffer = Buffer.from(swapTransactionEncoded, "base64"); + const swapTransaction = VersionedTransaction.deserialize(swapTransactionBuffer); - addressLookupTableAccounts.push( - ...(await getAdressLookupTableAccounts(connection, swapInstructionsResult.addressLookupTableAddresses)) + const signedSwapTransaction = await wallet.signTransaction(swapTransaction); + const swapSig = await connection.sendTransaction(signedSwapTransaction); + await connection.confirmTransaction( + { + blockhash, + lastValidBlockHeight, + signature: swapSig, + }, + "confirmed" ); + } else if (depositOption.type === "native") { - const swapTransactionMessage = new TransactionMessage({ - instructions: swapInstructions, - payerKey: walletAddress, - recentBlockhash: blockhash, - }).compileToV0Message(addressLookupTableAccounts); - const swapTransaction = new VersionedTransaction(swapTransactionMessage); - swapTransaction.sign([destinationTokenAccountKeypair]); - - // Craft stake pool deposit tx - const { instructions, signers } = await makeDepositWSolToStakePoolIx( + const { instructions, signers } = await makeDepositSolToStakePoolIx( lstData.accountData, lstData.poolAddress, - wallet.publicKey, - destinationTokenAccountKeypair.publicKey, - stakePoolProxyProgram, - minimumRentExemptionForTokenAccount + walletAddress, + depositOption.amount, + undefined ); + const depositMessage = new TransactionMessage({ instructions: instructions, payerKey: walletAddress, recentBlockhash: blockhash, }); + const depositTransaction = new VersionedTransaction(depositMessage.compileToV0Message([])); depositTransaction.sign(signers); - // Send txs - const signedSwapTransaction = await wallet.signTransaction(swapTransaction); - const swapSig = await connection.sendTransaction(signedSwapTransaction); - await connection.confirmTransaction( - { - blockhash, - lastValidBlockHeight, - signature: swapSig, - }, - "confirmed" - ); // TODO: explicitly warn if second tx fails - - setOngoingAction("minting"); - - const signedDepositTransaction = await wallet.signTransaction(depositTransaction); - const depositSig = await connection.sendTransaction(signedDepositTransaction); + const signedTransaction = await wallet.signTransaction(depositTransaction); + const depositSig = await connection.sendTransaction(signedTransaction); await connection.confirmTransaction( { blockhash, @@ -298,6 +342,8 @@ export const StakingCard: FC = () => { }, "confirmed" ); + } else { + throw new Error("Invalid deposit option"); } toast.success("Minting complete"); @@ -314,22 +360,12 @@ export const StakingCard: FC = () => { toast.error(errorMsg); } finally { await Promise.all([refresh(), fetchLstState()]); - setDepositAmount(0); + setDepositOption((currentDepositOption) => + currentDepositOption.type === "stake" ? currentDepositOption : { ...currentDepositOption, amount: new BN(0) } + ); setOngoingAction(null); } - }, [ - lstData, - wallet, - walletAddress, - depositAmount, - stakePoolProxyProgram, - selectedMint, - connection, - quoteResponseMeta?.original, - jupiterApiClient, - refresh, - fetchLstState, - ]); + }, [lstData, wallet, walletAddress, depositOption, connection, quoteResponseMeta?.original, jupiterApiClient, refresh, fetchLstState]); return ( <> @@ -337,46 +373,50 @@ export const StakingCard: FC = () => {
Deposit - {selectedMintInfo && ( + {depositOption.type === "token" && (
- {selectedMintInfo.symbol !== "SOL" && ( - <> -
refreshQuoteIfNeeded(true)} - > - -
-
setIsSettingsModalOpen(true)} - > - - - {isNaN(slippagePct) ? "0" : slippagePct}% - -
- - )} +
refreshQuoteIfNeeded(true)} + > + +
+
setIsSettingsModalOpen(true)} + > + + + {isNaN(slippagePct) ? "0" : slippagePct}% + +
)}
- {connected && selectedMintInfo && ( + {connected && (depositOption.type === "native" || depositOption.type === "token") && (
- - {selectedMintInfo.balance - ? selectedMintInfo.balance < 0.01 - ? "< 0.01" - : numeralFormatter(selectedMintInfo.balance) - : "-"} - + {maxDepositString} setDepositAmount(selectedMintInfo.balance)} + className={`p-2 ml-1 h-5 flex flex-row items-center justify-center text-sm border rounded-full border-white/10 bg-black/10 text-secondary fill-current cursor-pointer hover:bg-black/20 hover:border-[#DCE85D]/70 hover:shadow-[#DCE85D]/70 transition-all duration-200 ease-in-out`} + onClick={() => + setDepositOption((currentDepositOption) => { + const updatedAmount = + currentDepositOption.type === "native" + ? currentDepositOption.maxAmount + : currentDepositOption.type === "token" + ? currentDepositOption.tokenData.balance + : new BN(currentDepositOption.stakeData.lamports); + + return { + ...currentDepositOption, + amount: updatedAmount, + }; + }) + } > MAX @@ -386,12 +426,13 @@ export const StakingCard: FC = () => { { - if (e.key === "Enter" && connected && depositAmount !== 0 && !refreshingQuotes && !ongoingAction) { + if (e.key === "Enter" && connected && depositAmountUi !== 0 && !refreshingQuotes && !ongoingAction) { onMint(); } }} @@ -400,33 +441,42 @@ export const StakingCard: FC = () => { size="small" isAllowed={(values) => { const { floatValue } = values; - if (!connected || selectedMintInfo === null) { + if (!connected || depositOption.type === "stake") { return true; } - if (selectedMintInfo.balance === 0) { - return false; - } - return floatValue ? floatValue < selectedMintInfo.balance : true; + const decimals = depositOption.type === "token" ? depositOption.tokenData.decimals : 9; + const depositAmount = uiToNative(floatValue ?? 0, decimals); + const maxDepositAmount = + depositOption.type === "token" ? depositOption.tokenData.balance : depositOption.maxAmount; + return floatValue ? depositAmount.lt(maxDepositAmount) : true; }} sx={{ input: { textAlign: "right", MozAppearance: "textfield" }, "input::-webkit-inner-spin-button": { WebkitAppearance: "none", margin: 0 }, + "input::-webkit-text-fill-color": "red", "& .MuiOutlinedInput-root": { "&.Mui-focused fieldset": { borderWidth: "0px", }, }, + "& .MuiInputBase-input.Mui-disabled": { + WebkitTextFillColor: "#e1e1e1", + cursor: "not-allowed", + }, }} className="bg-[#0F1111] p-2 rounded-xl" InputProps={ - tokenDataMap + tokenDataMap && solUsdValue ? { className: "font-aeonik text-[#e1e1e1] p-0 m-0", startAdornment: ( ), } @@ -437,7 +487,7 @@ export const StakingCard: FC = () => {
You will receive - {lstOutAmount !== null && selectedMintInfo + {lstOutAmount !== null ? lstOutAmount < 0.01 && lstOutAmount > 0 ? "< 0.01" : numeralFormatter(lstOutAmount) @@ -449,8 +499,7 @@ export const StakingCard: FC = () => { { loading={connected && !!ongoingAction} onClick={connected ? onMint : openWalletSelector} > - {!connected ? ( - "connect" - ) : ongoingAction ? ( - `${ongoingAction}...` - ) : refreshingQuotes ? ( - - ) : ( - "mint" - )} + {!connected ? "connect" : ongoingAction ? `${ongoingAction}...` : refreshingQuotes ? : "mint"}
@@ -504,14 +545,42 @@ export const StakingCard: FC = () => { }; interface DropDownButtonProps { + availableLamports: BN; + solUsdPrice: number; tokenDataMap: TokenDataMap; - selectedMintInfo: TokenData | null; - setSelectedMint: Dispatch>; + stakeAccounts: StakeData[]; + depositOption: DepositOption; + setDepositOption: Dispatch>; disabled?: boolean; } -const DropDownButton: FC = ({ tokenDataMap, selectedMintInfo, setSelectedMint, disabled }) => { +const DropDownButton: FC = ({ + availableLamports, + solUsdPrice, + tokenDataMap, + stakeAccounts, + depositOption, + setDepositOption, + disabled, +}) => { const [isModalOpen, setIsModalOpen] = useState(false); + + const [iconUrl, optionName] = useMemo(() => { + if (depositOption.type === "native") { + return [ + "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", + "SOL", + ]; + } else if (depositOption.type === "stake") { + return [ + "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", + "Stake", + ]; + } else { + return [depositOption.tokenData.iconUrl, depositOption.tokenData.symbol]; + } + }, [depositOption]); + return ( <>
= ({ tokenDataMap, selectedMintInf }`} >
- token logo + token logo
- {selectedMintInfo?.symbol ?? "SOL"} + {optionName}
setIsModalOpen(false)} + availableLamports={availableLamports} + solUsdPrice={solUsdPrice} tokenDataMap={tokenDataMap} - setSelectedMint={setSelectedMint} + stakeAccounts={stakeAccounts} + depositOption={depositOption} + setDepositOption={setDepositOption} /> ); @@ -552,7 +625,7 @@ async function makeDepositSolToStakePoolIx( stakePool: solanaStakePool.StakePool, stakePoolAddress: PublicKey, from: PublicKey, - lamports: number, + lamports: BN, destinationTokenAccount?: PublicKey, referrerTokenAccount?: PublicKey, depositAuthority?: PublicKey @@ -567,7 +640,7 @@ async function makeDepositSolToStakePoolIx( SystemProgram.transfer({ fromPubkey: from, toPubkey: userSolTransfer.publicKey, - lamports, + lamports: lamports.toNumber(), }) ); @@ -594,7 +667,7 @@ async function makeDepositSolToStakePoolIx( managerFeeAccount: stakePool.managerFeeAccount, referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount, poolMint: stakePool.poolMint, - lamports, + lamports: lamports.toNumber(), withdrawAuthority, depositAuthority, }) @@ -606,6 +679,77 @@ async function makeDepositSolToStakePoolIx( }; } +/** + * Creates instructions required to deposit stake to stake pool. + */ +export async function makeDepositStakeToStakePoolIx( + stakePool: solanaStakePool.StakePool, + stakePoolAddress: PublicKey, + walletAddress: PublicKey, + validatorVote: PublicKey, + depositStake: PublicKey +) { + const withdrawAuthority = findWithdrawAuthorityProgramAddress( + solanaStakePool.STAKE_POOL_PROGRAM_ID, + stakePoolAddress + ); + + const validatorStake = findStakeProgramAddress( + solanaStakePool.STAKE_POOL_PROGRAM_ID, + validatorVote, + stakePoolAddress + ); + + const instructions: TransactionInstruction[] = []; + const signers: Signer[] = []; + + const poolMint = stakePool.poolMint; + + const poolTokenReceiverAccount = getAssociatedTokenAddressSync(poolMint, walletAddress); + instructions.push( + createAssociatedTokenAccountIdempotentInstruction(walletAddress, poolTokenReceiverAccount, walletAddress, poolMint) + ); + + instructions.push( + ...StakeProgram.authorize({ + stakePubkey: depositStake, + authorizedPubkey: walletAddress, + newAuthorizedPubkey: stakePool.stakeDepositAuthority, + stakeAuthorizationType: StakeAuthorizationLayout.Staker, + }).instructions + ); + + instructions.push( + ...StakeProgram.authorize({ + stakePubkey: depositStake, + authorizedPubkey: walletAddress, + newAuthorizedPubkey: stakePool.stakeDepositAuthority, + stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer, + }).instructions + ); + + instructions.push( + solanaStakePool.StakePoolInstruction.depositStake({ + stakePool: stakePoolAddress, + validatorList: stakePool.validatorList, + depositAuthority: stakePool.stakeDepositAuthority, + reserveStake: stakePool.reserveStake, + managerFeeAccount: stakePool.managerFeeAccount, + referralPoolAccount: poolTokenReceiverAccount, + destinationPoolAccount: poolTokenReceiverAccount, + withdrawAuthority, + depositStake, + validatorStake, + poolMint, + }) + ); + + return { + instructions, + signers, + }; +} + /** * Creates instructions required to deposit the whole balance of a wsol account to stake pool. */ @@ -679,6 +823,17 @@ function findWithdrawAuthorityProgramAddress(programId: PublicKey, stakePoolAddr return publicKey; } +/** + * Generates the stake program address for a validator's vote account + */ +function findStakeProgramAddress(programId: PublicKey, voteAccountAddress: PublicKey, stakePoolAddress: PublicKey) { + const [publicKey] = PublicKey.findProgramAddressSync( + [voteAccountAddress.toBuffer(), stakePoolAddress.toBuffer()], + programId + ); + return publicKey; +} + function jupIxToSolanaIx(ix: Instruction): TransactionInstruction { return new TransactionInstruction({ programId: new PublicKey(ix.programId), diff --git a/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingModal.tsx b/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingModal.tsx index 1a2c9aac64..d1dd47873e 100644 --- a/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingModal.tsx +++ b/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingModal.tsx @@ -1,19 +1,40 @@ import { Typography, Modal } from "@mui/material"; -import { Dispatch, FC, SetStateAction } from "react"; -import { Close } from "@mui/icons-material"; +import { Dispatch, FC, SetStateAction, useState } from "react"; +import { Close, Launch } from "@mui/icons-material"; import Image from "next/image"; +import { nativeToUi, numeralFormatter, shortenAddress } from "@mrgnlabs/mrgn-common"; +import { SOL_MINT, TokenDataMap } from "~/store/lstStore"; +import { LstDepositToggle } from "./LstDepositToggle"; +import { StakeData } from "~/utils/stakeAcounts"; +import { DepositOption } from "./StakingCard"; +import BN from "bn.js"; import { PublicKey } from "@solana/web3.js"; -import { numeralFormatter } from "@mrgnlabs/mrgn-common"; -import { TokenDataMap } from "~/store/lstStore"; +import { MrgnTooltip } from "~/components/Tooltip"; +import InfoIcon from "@mui/icons-material/Info"; interface StakingModalProps { isOpen: boolean; handleClose: () => void; - setSelectedMint: Dispatch>; + depositOption: DepositOption; + setDepositOption: Dispatch>; + availableLamports: BN; + solUsdPrice: number; tokenDataMap: TokenDataMap; + stakeAccounts: StakeData[]; } -export const StakingModal: FC = ({ isOpen, handleClose, tokenDataMap, setSelectedMint }) => { +export const StakingModal: FC = ({ + isOpen, + handleClose, + depositOption, + setDepositOption, + availableLamports, + solUsdPrice, + tokenDataMap, + stakeAccounts, +}) => { + const [isStakeAccountMode, setIsStakeAccountMode] = useState(depositOption.type === "stake"); + return ( = ({ isOpen, handleClose, token className="border-none" >
-
- Select token -
- +
+
+ +
+
+
-
- {[...tokenDataMap.values()] - .sort((a, b) => Number(b.balance) * Number(b.price) - Number(a.balance) * Number(a.price)) - .map((token) => { - const usdValue = token.balance * token.price; - return ( - { - setSelectedMint(new PublicKey(token.address)); - handleClose(); - }} - > -
-
- token logo - {token.symbol} -
-
- {token.balance > 0 && ( - <> - {token.balance < 0.01 ? "< 0.01" : numeralFormatter(token.balance)} - - ({usdValue < 0.01 ? "< $0.01" : `$${numeralFormatter(usdValue)}`}) - - - )} -
-
-
- ); - })} +
+ + {isStakeAccountMode ? "Select stake account" : "Select token"} + +
+ + Convert your native SOL stake into $LST instantly + + ) : ( + + Convert your tokens to $LST effortlessly. Powered by Jupiter. + + ) + } + placement="top" + > + + +
+
+
+ {isStakeAccountMode ? ( + stakeAccounts.length > 0 ? ( + + ) : ( +
+ No eligible stake accounts found
+ ) + ) : ( + + )}
); }; + +const TokenList: FC<{ + availableLamports: BN; + solUsdPrice: number; + tokenDataMap: TokenDataMap; + depositOption: DepositOption; + setDepositOption: Dispatch>; + handleClose: () => void; +}> = ({ availableLamports, solUsdPrice, tokenDataMap, depositOption, setDepositOption, handleClose }) => { + const availableLamportsUi = nativeToUi(availableLamports, 9); + const lamportsUsdValue = availableLamportsUi * solUsdPrice; + return ( +
+
{ + setDepositOption({ type: "native", amount: new BN(0), maxAmount: availableLamports }); + handleClose(); + }} + className={`flex flex-row w-full justify-between font-aeonik font-[400] text-base items-center gap-4 p-2 rounded-lg ${ + depositOption.type === "native" && "bg-[#DCE85D88]" + } hover:text-white hover:bg-gray-700 cursor-pointer`} + > +
+ token logo + +
+ {"SOL (native)"} + +
+
+
+ {availableLamportsUi > 0 && ( +
+ + {availableLamportsUi < 0.01 ? "< 0.01" : numeralFormatter(availableLamportsUi)} + + + {lamportsUsdValue < 0.01 ? "< $0.01" : `$${numeralFormatter(lamportsUsdValue)}`} + +
+ )} +
+
+ + {[...tokenDataMap.values()] + .filter((token) => token.balance.gtn(0)) + .sort( + (a, b) => + nativeToUi(b.balance, b.decimals) * Number(b.price) - nativeToUi(a.balance, a.decimals) * Number(a.price) + ) + .map((token) => { + const balanceUi = nativeToUi(token.balance, token.decimals); + const usdValue = balanceUi * token.price; + return ( +
{ + setDepositOption({ type: "token", tokenData: token, amount: new BN(0) }); + handleClose(); + }} + className={`flex flex-row w-full justify-between font-aeonik font-[400] text-base items-center gap-4 p-2 rounded-lg hover:text-white hover:bg-gray-700 cursor-pointer ${ + depositOption.type === "token" && + depositOption.tokenData.address === token.address && + "text-black bg-[#DCE85D88]" + }`} + > +
+ token logo +
+ + {token.address === SOL_MINT.toBase58() ? "SOL (wrapped)" : token.symbol} + + +
+
+
+ {token.balance.gtn(0) && ( +
+ + {balanceUi < 0.01 ? "< 0.01" : numeralFormatter(balanceUi)} + + + {usdValue < 0.01 ? "< $0.01" : `$${numeralFormatter(usdValue)}`} + +
+ )} +
+
+ ); + })} +
+ ); +}; + +const StakeAccountList: FC<{ + depositOption: DepositOption; + stakeAccounts: StakeData[]; + setSelectedStakeAccount: Dispatch>; + handleClose: () => void; +}> = ({ depositOption, stakeAccounts, setSelectedStakeAccount, handleClose }) => { + return ( +
+ {stakeAccounts + .sort((a, b) => b.lamports.sub(a.lamports).toNumber()) + .map((stakeData) => { + return ( + { + setSelectedStakeAccount({ type: "stake", stakeData }); + handleClose(); + }} + > +
+
+ token logo + +
+
+ {nativeToUi(stakeData.lamports, 9)} SOL +
+
+
+ ); + })} +
+ ); +}; + +type AccountType = "token" | "stake"; + +const AccountBadge: FC<{ account: PublicKey | string; type: AccountType }> = ({ account, type }) => ( + event.stopPropagation()} + > + {shortenAddress(account)} + + +); diff --git a/apps/marginfi-v2-ui/src/pages/stake.tsx b/apps/marginfi-v2-ui/src/pages/stake.tsx index 4d79420985..7b3b2cea4e 100644 --- a/apps/marginfi-v2-ui/src/pages/stake.tsx +++ b/apps/marginfi-v2-ui/src/pages/stake.tsx @@ -4,6 +4,7 @@ import { Typography } from "@mui/material"; import { useConnection } from "@solana/wallet-adapter-react"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; +import { OverlaySpinner } from "~/components/OverlaySpinner"; import { PageHeader } from "~/components/PageHeader"; import { StakingStats } from "~/components/Staking"; import { StakingCard } from "~/components/Staking/StakingCard/StakingCard"; @@ -30,12 +31,15 @@ const StakePage = () => { } }, [router]); - const [fetchLstState, setIsRefreshingStore, userDataFetched, resetUserData] = useLstStore((state) => [ - state.fetchLstState, - state.setIsRefreshingStore, - state.userDataFetched, - state.resetUserData, - ]); + const [initialized, isRefreshingStore, fetchLstState, setIsRefreshingStore, userDataFetched, resetUserData] = + useLstStore((state) => [ + state.initialized, + state.isRefreshingStore, + state.fetchLstState, + state.setIsRefreshingStore, + state.userDataFetched, + state.resetUserData, + ]); useEffect(() => { setIsRefreshingStore(true); @@ -65,27 +69,28 @@ const StakePage = () => { if (!mounted) return null; return ( - + stake
-
- +
+ $LST, by mrgn - + Introducing the best way to get exposure to SOL. $LST is built on mrgn's validator network and Jito's MEV rewards. For the first time,{" "} $LST holders can get the best staking yield available on Solana, combined with the biggest MEV rewards from Solana's trader network. - - $LST has 0% commission. The yield goes to you. Stop - paying middlemen. Stop using underperforming validators. Stop missing out on MEV rewards. + + $LST has 0% commission. The yield goes to you. Stop paying + middlemen. Stop using underperforming validators. Stop missing out on MEV rewards.
+ ); }; diff --git a/apps/marginfi-v2-ui/src/store/lstStore.ts b/apps/marginfi-v2-ui/src/store/lstStore.ts index 0277bf25eb..e68262e35e 100644 --- a/apps/marginfi-v2-ui/src/store/lstStore.ts +++ b/apps/marginfi-v2-ui/src/store/lstStore.ts @@ -1,6 +1,6 @@ import { AnchorProvider } from "@coral-xyz/anchor"; import { vendor } from "@mrgnlabs/marginfi-client-v2"; -import { ACCOUNT_SIZE, TOKEN_PROGRAM_ID, Wallet, aprToApy } from "@mrgnlabs/mrgn-common"; +import { ACCOUNT_SIZE, TOKEN_PROGRAM_ID, Wallet, aprToApy, uiToNative } from "@mrgnlabs/mrgn-common"; import { Connection, PublicKey } from "@solana/web3.js"; import { create, StateCreator } from "zustand"; import * as solanaStakePool from "@solana/spl-stake-pool"; @@ -9,18 +9,18 @@ import { TokenInfo, TokenInfoMap, TokenListContainer } from "@solana/spl-token-r import { TokenAccount, TokenAccountMap, fetchBirdeyePrices } from "@mrgnlabs/marginfi-v2-ui-state"; import { persist } from "zustand/middleware"; import { StakePoolProxyProgram, getStakePoolProxyProgram } from "~/utils/stakePoolProxy"; +import { StakeData, fetchStakeAccounts } from "~/utils/stakeAcounts"; +import BN from "bn.js"; const STAKEVIEW_APP_URL = "https://stakeview.app/apy/prev3.json"; const BASELINE_VALIDATOR_ID = "FugJZepeGfh1Ruunhep19JC4F3Hr2FL3oKUMezoK8ajp"; export const SOL_MINT = new PublicKey("So11111111111111111111111111111111111111112"); +export const LST_MINT = new PublicKey("LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp"); const NETWORK_FEE_LAMPORTS = 15000; // network fee + some for potential account creation const SOL_USD_PYTH_ORACLE = new PublicKey("H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG"); const STAKE_POOL_ID = new PublicKey("DqhH94PjkZsjAqEze2BEkWhFQJ6EyU6MdtMphMgnXqeK"); -// const STAKE_POOL_ID = new PublicKey("stk9ApL5HeVAwPLr3TLhDXdZS8ptVu7zp6ov8HFDuMi"); // blaze -// const STAKE_POOL_ID = new PublicKey("Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb"); // jito - const SUPPORTED_TOKENS = [ "7kbnvuGBxxj8AG9qp8Scn56muWGaRaFqxg1FsRp3PaFT", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", @@ -32,7 +32,7 @@ const SUPPORTED_TOKENS = [ "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", ]; -export type TokenData = Omit & { price: number; balance: number; iconUrl: string }; +export type TokenData = Omit & { price: number; balance: BN; iconUrl: string }; export type TokenDataMap = Map; export type SupportedSlippagePercent = 0.1 | 0.5 | 1.0 | 5.0; @@ -45,7 +45,9 @@ interface LstState { connection: Connection | null; wallet: Wallet | null; lstData: LstData | null; + availableLamports: BN | null; tokenDataMap: TokenDataMap | null; + stakeAccounts: StakeData[]; solUsdValue: number | null; slippagePct: SupportedSlippagePercent; stakePoolProxyProgram: StakePoolProxyProgram | null; @@ -70,13 +72,14 @@ function createLstStore() { ); } -interface LstData { +export interface LstData { poolAddress: PublicKey; tvl: number; projectedApy: number; lstSolValue: number; solDepositFee: number; accountData: solanaStakePool.StakePool; + validatorList: PublicKey[]; } const stateCreator: StateCreator = (set, get) => ({ @@ -87,7 +90,9 @@ const stateCreator: StateCreator = (set, get) => ({ connection: null, wallet: null, lstData: null, + availableLamports: null, tokenDataMap: null, + stakeAccounts: [], solUsdValue: null, slippagePct: 1, stakePoolProxyProgram: null, @@ -109,24 +114,30 @@ const stateCreator: StateCreator = (set, get) => ({ const stakePoolProxyProgram = getStakePoolProxyProgram(provider); let lstData: LstData | null = null; + let availableLamports: BN | null = null; let tokenDataMap: TokenDataMap | null = null; let solUsdValue: number | null = null; + let stakeAccounts: StakeData[] = []; if (wallet?.publicKey) { - const [accountsAiList, minimumRentExemption, _lstData, jupiterTokenInfo, userTokenAccounts] = await Promise.all( - [ + const [accountsAiList, minimumRentExemption, _lstData, jupiterTokenInfo, userTokenAccounts, _stakeAccounts] = + await Promise.all([ connection.getMultipleAccountsInfo([wallet.publicKey, SOL_USD_PYTH_ORACLE]), connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE), fetchLstData(connection), fetchJupiterTokenInfo(), fetchUserTokenAccounts(connection, wallet.publicKey), - ] - ); + fetchStakeAccounts(connection, wallet.publicKey), + ]); lstData = _lstData; const [walletAi, solUsdPythFeedAi] = accountsAiList; const nativeSolBalance = walletAi?.lamports ? walletAi.lamports : 0; - const availableSolBalance = (nativeSolBalance - minimumRentExemption - NETWORK_FEE_LAMPORTS) / 1e9; + availableLamports = new BN(nativeSolBalance - minimumRentExemption - NETWORK_FEE_LAMPORTS); solUsdValue = vendor.parsePriceData(solUsdPythFeedAi!.data).emaPrice.value; + stakeAccounts = _stakeAccounts.filter( + (stakeAccount) => + stakeAccount.isActive && _lstData.validatorList.find((v) => v.equals(stakeAccount.validatorVoteAddress)) + ); const tokenPrices = await fetchTokenPrices( [...jupiterTokenInfo.values()].map((tokenInfo) => new PublicKey(tokenInfo.address)) @@ -136,13 +147,9 @@ const stateCreator: StateCreator = (set, get) => ({ const price = tokenPrices.get(tokenInfo.address); const { logoURI, ..._tokenInfo } = tokenInfo; - let walletBalance: number = 0; - if (tokenMint === SOL_MINT.toBase58()) { - walletBalance = availableSolBalance; - } else { - const tokenAccount = userTokenAccounts?.get(tokenMint); - walletBalance = tokenAccount?.balance ?? 0; - } + let walletBalance: BN = new BN(0); + const tokenAccount = userTokenAccounts?.get(tokenMint); + walletBalance = uiToNative(tokenAccount?.balance ?? 0, tokenInfo.decimals); return [ tokenMint, @@ -168,7 +175,7 @@ const stateCreator: StateCreator = (set, get) => ({ const { logoURI, ..._tokenInfo } = tokenInfo; return [ tokenMint, - { ..._tokenInfo, iconUrl: logoURI ?? "/info_icon.png", price: price ? price : 0, balance: 0 }, + { ..._tokenInfo, iconUrl: logoURI ?? "/info_icon.png", price: price ? price : 0, balance: new BN(0) }, ]; }) ); @@ -185,7 +192,9 @@ const stateCreator: StateCreator = (set, get) => ({ connection, wallet, lstData, + availableLamports, tokenDataMap, + stakeAccounts, solUsdValue, stakePoolProxyProgram, }); @@ -200,7 +209,7 @@ const stateCreator: StateCreator = (set, get) => ({ if (tokenDataMap) { tokenDataMap = new Map( [...tokenDataMap?.entries()].map( - ([tokenMint, tokenData]) => [tokenMint, { ...tokenData, balance: 0 }] as [string, TokenData] + ([tokenMint, tokenData]) => [tokenMint, { ...tokenData, balance: new BN(0) }] as [string, TokenData] ) ); } @@ -253,6 +262,7 @@ async function fetchLstData(connection: Connection): Promise { lstSolValue, solDepositFee, accountData: stakePool, + validatorList: stakePoolInfo.validatorList.map((v) => new PublicKey(v.voteAccountAddress)), }; } diff --git a/apps/marginfi-v2-ui/src/utils/stakeAcounts.ts b/apps/marginfi-v2-ui/src/utils/stakeAcounts.ts new file mode 100644 index 0000000000..46e3a98f3b --- /dev/null +++ b/apps/marginfi-v2-ui/src/utils/stakeAcounts.ts @@ -0,0 +1,48 @@ +import { PublicKey, Connection, StakeProgram, AccountInfo, ParsedAccountData } from "@solana/web3.js"; +import BN from "bn.js"; + +export interface StakeData { + address: PublicKey; + lamports: BN; + isActive: boolean; + validatorVoteAddress: PublicKey; +} + +export async function fetchStakeAccounts( + connection: Connection, + walletAddress: PublicKey +): Promise { + const [parsedAccounts, currentEpoch] = await Promise.all([ + connection.getParsedProgramAccounts(StakeProgram.programId, { + filters: [ + { dataSize: 200 }, + { + memcmp: { + offset: 12, + bytes: walletAddress.toBase58(), + }, + }, + ], + }), + connection.getEpochInfo(), + ]); + + let newStakeAccountMetas = parsedAccounts + .map(({ pubkey, account }) => { + const parsedAccount = account as AccountInfo; + + const activationEpoch = Number(parsedAccount.data.parsed.info.stake.delegation.activationEpoch); + const deactivationEpoch = Number(parsedAccount.data.parsed.info.stake.delegation.deactivationEpoch); + let isActive = parsedAccount.data.parsed.type === "delegated" && currentEpoch.epoch >= activationEpoch + 1 && deactivationEpoch > currentEpoch.epoch; + + return { + address: pubkey, + lamports: new BN(account.lamports), + isActive, + validatorVoteAddress: new PublicKey(parsedAccount.data.parsed.info.stake.delegation.voter), + } as StakeData; + }) + .filter((stakeAccountMeta) => stakeAccountMeta !== undefined); + + return newStakeAccountMetas; +} diff --git a/yarn.lock b/yarn.lock index 92c83c264c..b8b323aa59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7702,6 +7702,18 @@ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.3.tgz#15a0712296c5041733c79efe233ba17ae5a7587b" integrity sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g== +"@types/lodash.debounce@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f" + integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.199" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.199.tgz#c3edb5650149d847a277a8961a7ad360c474e9bf" + integrity sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg== + "@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"