From c1a0d547e708555bbb598766565eea218da9af09 Mon Sep 17 00:00:00 2001 From: man0s <95379755+losman0s@users.noreply.github.com> Date: Wed, 13 Sep 2023 20:58:44 +0800 Subject: [PATCH] checkpoint --- .../Staking/StakingCard/StakingCard.tsx | 133 +++++++++--------- apps/marginfi-v2-ui/src/store/lstStore.ts | 92 ++++++++---- 2 files changed, 131 insertions(+), 94 deletions(-) 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 cab2216c8d..34e48bceab 100644 --- a/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingCard.tsx +++ b/apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingCard.tsx @@ -3,9 +3,9 @@ import { TextField, Typography } from "@mui/material"; import * as solanaStakePool from "@solana/spl-stake-pool"; import { WalletIcon } from "./WalletIcon"; import { PrimaryButton } from "./PrimaryButton"; -import { useJupiterStore, useLstStore } from "~/pages/stake"; +import { useLstStore } from "~/pages/stake"; import { useWalletContext } from "~/components/useWalletContext"; -import { Wallet, numeralFormatter, processTransaction, shortenAddress } from "@mrgnlabs/mrgn-common"; +import { Wallet, floor, numeralFormatter, processTransaction, shortenAddress } from "@mrgnlabs/mrgn-common"; import { ArrowDropDown } from "@mui/icons-material"; import { StakingModal } from "./StakingModal"; import Image from "next/image"; @@ -18,39 +18,30 @@ import { usePrevious } from "~/utils"; const SOL_MINT = new PublicKey("So11111111111111111111111111111111111111112"); -const SUPPORTED_TOKENS = [ - new PublicKey("So11111111111111111111111111111111111111112"), - new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), - new PublicKey("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"), -]; - export interface TokenData { mint: PublicKey; symbol: string; + decimals: number; iconUrl: string; balance: number; } export const StakingCard: FC = () => { const { connection } = useConnection(); - const { connected, wallet, walletAddress, openWalletSelector } = useWalletContext(); - const [lstData, userData] = useLstStore((state) => [state.lstData, state.userData]); - const [tokenMap, tokenAccountMap] = useJupiterStore((state) => [state.tokenMap, state.tokenAccountMap]); + const { connected, wallet, walletAddress, openWalletSelector } = useWalletContext(); + const [lstData, userData, jupiterTokenInfo, userTokenAccounts] = useLstStore((state) => [ + state.lstData, + state.userData, + state.jupiterTokenInfo, + state.userTokenAccounts, + ]); - const [depositAmount, setDepositAmount] = useState(0); + const [depositAmount, setDepositAmount] = useState(null); const [selectedMint, setSelectedMint] = useState(SOL_MINT); - const jupiter = useJupiter({ - amount: JSBI.BigInt(1 * 10 ** 6), // raw input amount of tokens - inputMint: selectedMint, - outputMint: SOL_MINT, - slippageBps: 1, // 0.1% slippage - debounceTime: 250, // debounce ms time before refresh - }); - const prevWalletAddress = usePrevious(walletAddress); useEffect(() => { - if (!walletAddress && prevWalletAddress || walletAddress && !prevWalletAddress) { + if ((!walletAddress && prevWalletAddress) || (walletAddress && !prevWalletAddress)) { setDepositAmount(0); setSelectedMint(SOL_MINT); } @@ -59,60 +50,64 @@ export const StakingCard: FC = () => { const maxDeposit: number | null = useMemo(() => userData?.nativeSolBalance ?? null, [userData]); const selectedMintInfo: TokenData | undefined = useMemo(() => { - if (!userData || tokenMap.size === 0) return undefined; + if (jupiterTokenInfo === null) return undefined; - const tokenInfo = tokenMap.get(selectedMint.toString()); + const tokenInfo = jupiterTokenInfo.get(selectedMint.toString()); if (!tokenInfo) throw new Error(`Token ${selectedMint.toBase58()} not found`); - let walletBalance: number; + let walletBalance: number = 0; if (selectedMint.equals(SOL_MINT)) { - walletBalance = userData.nativeSolBalance; + walletBalance = userData?.nativeSolBalance ?? 0; } else { - const tokenAccount = tokenAccountMap.get(selectedMint.toString()); - if (!tokenAccount) throw new Error(`Token account ${selectedMint.toBase58()} not found`); - walletBalance = tokenAccount.balance; + const tokenAccount = userTokenAccounts?.get(selectedMint.toString()); + walletBalance = tokenAccount?.balance ?? 0; } return { mint: selectedMint, symbol: tokenInfo.symbol, iconUrl: tokenInfo.logoURI ?? "/info_icon.png", + decimals: tokenInfo.decimals, balance: walletBalance, }; - }, [selectedMint, userData, tokenMap, tokenAccountMap]); + }, [jupiterTokenInfo, selectedMint, userData, userTokenAccounts]); - const supportedTokensForUser: TokenData[] = useMemo(() => { - if (!userData || !tokenAccountMap) return []; - const ownedMints = SUPPORTED_TOKENS.filter((mint) => tokenAccountMap.has(mint.toString())); - return ownedMints.map((mint) => { - const tokenInfo = tokenMap.get(mint.toString()); - if (!tokenInfo) throw new Error(`Token ${mint.toBase58()} not found`); + const { quoteResponseMeta } = useJupiter({ + amount: JSBI.BigInt( + selectedMintInfo ? floor(depositAmount ?? 0, selectedMintInfo.decimals) * 10 ** selectedMintInfo.decimals : 0 + ), // raw input amount of tokens + inputMint: selectedMint, + outputMint: SOL_MINT, + slippageBps: 1, // 0.1% slippage + debounceTime: 250, + }); - let walletBalance: number; - if (selectedMint.equals(SOL_MINT)) { - walletBalance = userData.nativeSolBalance; - } else { - const tokenAccount = tokenAccountMap.get(selectedMint.toString()); - if (!tokenAccount) throw new Error(`Token account ${selectedMint.toBase58()} not found`); - walletBalance = tokenAccount.balance; - } + const solDepositAmount: number | null = useMemo(() => { + if (!selectedMintInfo) return null; + if (selectedMintInfo.mint.equals(SOL_MINT)) return depositAmount; + const swapOutAmount = quoteResponseMeta?.quoteResponse.outAmount; + const swapOutAmountUi = swapOutAmount ? JSBI.toNumber(swapOutAmount) / 1e9 : null; + return swapOutAmountUi; + }, [depositAmount, selectedMintInfo, quoteResponseMeta]); - return { - mint, - symbol: tokenInfo.symbol, - iconUrl: tokenInfo.logoURI ?? "/info_icon.png", - balance: walletBalance, - }; - }); - }, [userData, tokenAccountMap, tokenMap, selectedMint]); + const availableTokens: TokenData[] = useMemo(() => { + if (!jupiterTokenInfo) return []; + return [...jupiterTokenInfo.values()].map((token) => ({ + mint: new PublicKey(token.address), + symbol: token.symbol, + iconUrl: token.logoURI ?? "/info_icon.png", + decimals: token.decimals, + balance: 0, + })); + }, [jupiterTokenInfo]); const onChange = useCallback( - (event: NumberFormatValues) => setDepositAmount(event.floatValue ?? 0), + (event: NumberFormatValues) => setDepositAmount(event.floatValue ?? null), [setDepositAmount] ); const onMint = useCallback(async () => { - if (!lstData || !wallet) return; + if (!lstData || !wallet || !depositAmount) return; console.log("depositing", depositAmount, selectedMint); try { await depositToken(lstData.poolAddress, depositAmount, selectedMint, connection, wallet); @@ -172,14 +167,12 @@ export const StakingCard: FC = () => { }} className="bg-[#0F1111] p-2 rounded-xl" InputProps={ - tokenMap.size > 0 + jupiterTokenInfo ? { className: "font-aeonik text-[#e1e1e1] p-0 m-0", startAdornment: ( {
You will receive - {lstData ? numeralFormatter(depositAmount / lstData.lstSolValue) : "-"} $LST + {lstData ? numeralFormatter(solDepositAmount ?? 0 / lstData.lstSolValue) : "-"} $LST
- + {connected ? "Mint" : "Connect"}
@@ -223,8 +219,6 @@ export const StakingCard: FC = () => { interface DropDownButtonProps { supportedTokens: TokenData[]; - depositAmount: number; - setDepositAmount: Dispatch>; selectedMintInfo: { mint: PublicKey; symbol: string; iconUrl: string }; setSelectedMint: Dispatch>; disabled?: boolean; @@ -244,7 +238,7 @@ const DropDownButton: FC = ({ supportedTokens, selectedMint token logo {selectedMintInfo.symbol} - + Promise; @@ -47,6 +58,8 @@ const stateCreator: StateCreator = (set, get) => ({ wallet: null, lstData: null, userData: null, + jupiterTokenInfo: null, + userTokenAccounts: null, // Actions fetchLstState: async (args?: { connection?: Connection; wallet?: Wallet }) => { @@ -60,13 +73,19 @@ const stateCreator: StateCreator = (set, get) => ({ let lstData: LstData | null = null; let userData: UserData | null = null; + let jupiterTokenInfo: TokenInfoMap | null = null; + let userTokenAccounts: TokenAccountMap | null = null; if (wallet?.publicKey) { - const [accountsAiList, minimumRentExemption, _lstData] = await Promise.all([ + const [accountsAiList, minimumRentExemption, _lstData, _jupiterTokenInfo, _userTokenAccounts] = await Promise.all([ connection.getMultipleAccountsInfo([wallet.publicKey]), connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE), fetchLstData(connection), + fetchJupiterTokenInfo(), + fetchUserTokenAccounts(connection, wallet.publicKey), ]); lstData = _lstData; + jupiterTokenInfo = _jupiterTokenInfo; + userTokenAccounts = _userTokenAccounts; const [walletAi] = accountsAiList; const nativeSolBalance = walletAi?.lamports ? walletAi.lamports : 0; const availableSolBalance = (nativeSolBalance - minimumRentExemption - NETWORK_FEE_LAMPORTS) / 1e9; @@ -74,7 +93,9 @@ const stateCreator: StateCreator = (set, get) => ({ userData = { nativeSolBalance: availableSolBalance }; userDataFetched = true; } else { - lstData = await fetchLstData(connection); + const [_lstData, _jupiterTokenInfo] = await Promise.all([fetchLstData(connection), fetchJupiterTokenInfo()]); + lstData = _lstData; + jupiterTokenInfo = _jupiterTokenInfo; } set({ @@ -85,6 +106,8 @@ const stateCreator: StateCreator = (set, get) => ({ wallet, lstData, userData, + jupiterTokenInfo, + userTokenAccounts, }); } catch (err) { console.error("error refreshing state: ", err); @@ -93,8 +116,9 @@ const stateCreator: StateCreator = (set, get) => ({ }, setIsRefreshingStore: (isRefreshingStore: boolean) => set({ isRefreshingStore }), resetUserData: () => { - console.log("resetting user data") - set({ userDataFetched: false, userData: null })}, + console.log("resetting user data"); + set({ userDataFetched: false, userData: null }); + }, }); async function fetchLstData(connection: Connection): Promise { @@ -124,31 +148,41 @@ async function fetchLstData(connection: Connection): Promise { }; } -async function fetchTokenData(connection: Connection): Promise { - const [stakePoolInfo] = await Promise.all([ - solanaStakePool.stakePoolInfo(connection, new PublicKey("5TnTqbrucx4GxLxEqtUUAr3cggE6CKV7nBDuT2bL9Gux")), - ]); - const poolTokenSupply = Number(stakePoolInfo.poolTokenSupply); - const totalLamports = Number(stakePoolInfo.totalLamports); - const lastPoolTokenSupply = Number(stakePoolInfo.lastEpochPoolTokenSupply); - const lastTotalLamports = Number(stakePoolInfo.lastEpochTotalLamports); - - const solDepositFee = stakePoolInfo.solDepositFee.denominator.eqn(0) - ? 0 - : stakePoolInfo.solDepositFee.numerator.toNumber() / stakePoolInfo.solDepositFee.denominator.toNumber(); - const lstSolValue = poolTokenSupply > 0 ? totalLamports / poolTokenSupply : 1; - const lastLstSolValue = lastPoolTokenSupply > 0 ? lastTotalLamports / lastPoolTokenSupply : 1; - const epochRate = lstSolValue / lastLstSolValue - 1; - const apr = epochRate * EPOCHS_PER_YEAR; - const projectedApy = aprToApy(apr, EPOCHS_PER_YEAR); +async function fetchJupiterTokenInfo(): Promise { + const preferredTokenListMode: any = "strict"; + const tokens = await (preferredTokenListMode === "strict" + ? await fetch("https://token.jup.ag/strict") + : await fetch("https://token.jup.ag/all") + ).json(); + const res = new TokenListContainer(tokens); + const list = res.filterByChainId(101).getList(); + const tokenMap = list.filter(tokenInfo => SUPPORTED_TOKENS.includes(tokenInfo.address)).reduce((acc, item) => { + acc.set(item.address, item); + return acc; + }, new Map()); + + return tokenMap; +} - return { - poolAddress: new PublicKey(stakePoolInfo.address), - tvl: totalLamports / 1e9, - projectedApy, - lstSolValue, - solDepositFee, - }; +async function fetchUserTokenAccounts(connection: Connection, walletAddress: PublicKey): Promise { + const response = await connection.getParsedTokenAccountsByOwner( + walletAddress, + { programId: TOKEN_PROGRAM_ID }, + "confirmed" + ); + + const reducedResult = response.value.map((item: any) => { + return { + created: true, + mint: new PublicKey(item.account.data.parsed.info.mint), + balance: nativeToUi(new BN(item.account.data.parsed.info.tokenAmount.uiAmount), 0), + } as TokenAccount; + }); + + const userTokenAccounts = new Map( + reducedResult.map((tokenAccount: any) => [tokenAccount.mint.toString(), tokenAccount]) + ); + return userTokenAccounts; } export { createLstStore };