Skip to content

Commit

Permalink
feat(mfi-v2-ui): functional LST staking
Browse files Browse the repository at this point in the history
  • Loading branch information
losman0s committed Sep 12, 2023
1 parent c2edf8d commit 1b0d27a
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 126 deletions.
1 change: 1 addition & 0 deletions apps/marginfi-v2-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@next/bundle-analyzer": "^13.4.19",
"@next/font": "13.1.1",
"@socialgouv/matomo-next": "^1.4.0",
"@solana/spl-stake-pool": "^0.6.5",
"@solana/wallet-adapter-base": "^0.9.20",
"@solana/wallet-adapter-react": "^0.15.28",
"@solana/wallet-adapter-react-ui": "^0.9.27",
Expand Down
190 changes: 170 additions & 20 deletions apps/marginfi-v2-ui/src/components/Staking/StakingCard/StakingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,195 @@
import { FC } from "react";
import { Typography } from "@mui/material";

import { StakingInput } from "./StakingInput";
import { Dispatch, FC, SetStateAction, use, useCallback, useMemo, 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 { Wallet, numeralFormatter, processTransaction } from "@mrgnlabs/mrgn-common";
import { ArrowDropDown } from "@mui/icons-material";
import { StakingModal } from "./StakingModal";
import Image from "next/image";
import { NumberFormatValues, NumericFormat } from "react-number-format";
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { useConnection } from "@solana/wallet-adapter-react";

type SupportedToken = "SOL";

const TOKEN_URL_MAP: Record<SupportedToken, string> = {
SOL: "info_icon.png",
};

interface StakingCardProps {}
export const StakingCard: FC = () => {
const { connection } = useConnection();
const { connected, wallet } = useWalletContext();
const [lstData, userData] = useLstStore((state) => [state.lstData, state.userData]);

const [depositAmount, setDepositAmount] = useState<number>(0);
const [selectedToken, setSelectedToken] = useState<SupportedToken>("SOL");

const maxDeposit = useMemo(() => userData?.availableSolBalance ?? 0, [userData]);

const onChange = useCallback(
(event: NumberFormatValues) => setDepositAmount(event.floatValue ?? 0),
[setDepositAmount]
);

const onMint = useCallback(async () => {
if (!lstData || !wallet) return;
console.log("depositing", depositAmount, selectedToken);
try {
await depositSol(lstData.poolAddress, depositAmount, selectedToken, connection, wallet);
} finally {
setDepositAmount(0);
}
}, [connection, depositAmount, lstData, selectedToken, wallet]);

export const StakingCard: FC<StakingCardProps> = ({}) => {
return (
<>
<div className="relative flex flex-col gap-2 rounded-xl bg-[#1C2023] px-8 py-6 max-w-[480px] w-full">
<div className="flex flex-row justify-between w-full">
<Typography className="font-aeonik font-[400] text-lg">Deposit</Typography>
<div className="flex flex-row gap-2 my-auto">
<div>
<WalletIcon />
{connected && (
<div className="flex flex-row gap-2 my-auto">
<div>
<WalletIcon />
</div>
<Typography className="font-aeonik font-[400] text-sm my-auto">
{userData ? numeralFormatter(userData.availableSolBalance) : "-"}
</Typography>
<a
className={`font-aeonik font-[700] text-base ${
!maxDeposit ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
}`}
onClick={() => setDepositAmount(maxDeposit)}
>
MAX
</a>
</div>
<Typography className="font-aeonik font-[400] text-sm my-auto">123</Typography>
<a className="font-aeonik font-[700] text-base cursor-pointer" onClick={() => {}}>
MAX
</a>
</div>
)}
</div>
<StakingInput />

<NumericFormat
placeholder="0"
value={depositAmount}
allowNegative={false}
decimalScale={9}
disabled={!connected || !maxDeposit}
onValueChange={onChange}
thousandSeparator=","
customInput={TextField}
size="small"
isAllowed={(values) => {
const { floatValue } = values;
if (!maxDeposit) return false;
return floatValue ? floatValue < maxDeposit : true;
}}
sx={{
input: { textAlign: "right", MozAppearance: "textfield" },
"input::-webkit-inner-spin-button": { WebkitAppearance: "none", margin: 0 },
"& .MuiOutlinedInput-root": {
"&.Mui-focused fieldset": {
borderWidth: "0px",
},
},
}}
className="text-white bg-[#0F1111] text-3xl p-2 rounded-xl"
InputProps={{
className: "font-aeonik text-[#e1e1e1] text-2xl p-0 m-0",
disabled: !connected || !maxDeposit,
startAdornment: (
<DropDownButton
depositAmount={depositAmount}
setDepositAmount={setDepositAmount}
selectedToken={selectedToken}
setSelectedToken={setSelectedToken}
disabled={!connected || !maxDeposit}
/>
),
}}
/>

<div className="flex flex-row justify-between w-full my-auto pt-2">
<Typography className="font-aeonik font-[400] text-lg">You will Receive</Typography>
<Typography className="font-aeonik font-[700] text-2xl text-[#75BA80]">314.27 $LST</Typography>
<Typography className="font-aeonik font-[400] text-lg">You will receive</Typography>
<Typography className="font-aeonik font-[700] text-2xl text-[#75BA80]">
{lstData ? numeralFormatter(depositAmount / lstData.lstSolValue) : "-"} $LST
</Typography>
</div>
<div className="py-7">
<PrimaryButton>Mint</PrimaryButton>
<PrimaryButton disabled={!maxDeposit || depositAmount == 0} onClick={onMint}>
Mint
</PrimaryButton>
</div>
<div className="flex flex-row justify-between w-full my-auto">
<Typography className="font-aeonik font-[400] text-base">Current price</Typography>
<Typography className="font-aeonik font-[700] text-lg">1 $LST = 1.13 SOL</Typography>
<Typography className="font-aeonik font-[700] text-lg">
1 $LST = {lstData ? lstData.lstSolValue : "-"} SOL
</Typography>
</div>
<div className="flex flex-row justify-between w-full my-auto">
<Typography className="font-aeonik font-[400] text-base">Fees</Typography>
<Typography className="font-aeonik font-[400] text-base">Deposit fee</Typography>
<Typography className="font-aeonik font-[700] text-lg">0%</Typography>
</div>
</div>
</>
);
};

interface DropDownButtonProps {
depositAmount: number;
setDepositAmount: Dispatch<SetStateAction<number>>;
selectedToken: SupportedToken;
setSelectedToken: Dispatch<SetStateAction<SupportedToken>>;
disabled: boolean;
}

const DropDownButton: FC<DropDownButtonProps> = ({ selectedToken, disabled }) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

return (
<>
<div
onClick={() => setIsModalOpen(true)}
className={`w-[250px] h-[45px] flex flex-row justify-between py-2 px-4 text-white bg-[#303030] rounded-lg ${
disabled ? "opacity-50" : "cursor-pointer"
}`}
>
<div className="flex flex-row gap-3">
<div className="m-auto">
<Image src={`/${TOKEN_URL_MAP[selectedToken]!}`} alt="token logo" height={24} width={24} />
</div>
<Typography className="font-aeonik font-[700] text-lg leading-none my-auto">{selectedToken}</Typography>
</div>
<ArrowDropDown />
</div>
<StakingModal isOpen={isModalOpen} handleClose={() => setIsModalOpen(false)} />
</>
);
};

async function depositSol(
stakePoolAddress: PublicKey,
depositAmount: number,
token: SupportedToken,
connection: Connection,
wallet: Wallet
) {
if (token !== "SOL") throw new Error("Only SOL is supported for now");
console.log("1 depositing", depositAmount, token);
const _depositAmount = depositAmount * 1e9;
console.log("2 depositing", _depositAmount, token);

const { instructions, signers } = await solanaStakePool.depositSol(
connection,
stakePoolAddress,
wallet.publicKey,
_depositAmount,
undefined
);

const tx = new Transaction().add(...instructions);

const sig = await processTransaction(connection, wallet, tx, signers, { dryRun: false });

console.log(`Staked ${depositAmount} ${token} with signature ${sig}`);
}

This file was deleted.

23 changes: 6 additions & 17 deletions apps/marginfi-v2-ui/src/components/Staking/StakingStats.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { FC } from "react";
import { Typography, Skeleton } from "@mui/material";

import { Typography } from "@mui/material";
import { numeralFormatter, percentFormatterDyn } from "@mrgnlabs/mrgn-common";
import { useLstStore } from "~/pages/stake";

interface StakingStatsProps {
tvl: number;
projectedApy: number;
}
export const StakingStats: FC = () => {
const [lstData] = useLstStore((state) => [state.lstData]);

export const StakingStats: FC<StakingStatsProps> = ({ tvl, projectedApy }) => {
return (
<div className="h-full rounded-xl font-[500] p-[10px]">
<div className="flex flex-col sm:flex-row justify-center gap-0 sm:gap-8 w-full min-w-1/2 mt-[20px] bg-[#171C1F] sm:bg-transparent rounded-xl">
Expand All @@ -22,11 +19,7 @@ export const StakingStats: FC<StakingStatsProps> = ({ tvl, projectedApy }) => {
TVL
</Typography>
<Typography color="#fff" className="font-aeonik font-[500] text-xl" component="div">
{tvl ? (
`$${numeralFormatter(tvl)}`
) : (
<Skeleton variant="rectangular" animation="wave" className="w-1/3 rounded-md top-[4px]" />
)}
{lstData ? `$${numeralFormatter(lstData.tvl)}` : "-"}
</Typography>
</div>

Expand All @@ -42,11 +35,7 @@ export const StakingStats: FC<StakingStatsProps> = ({ tvl, projectedApy }) => {
Projected APY
</Typography>
<Typography color="#fff" className="font-aeonik font-[500] text-xl" component="div">
{projectedApy ? (
percentFormatterDyn.format(projectedApy)
) : (
<Skeleton variant="rectangular" animation="wave" className="w-1/3 rounded-md top-[4px]" />
)}
{lstData ? percentFormatterDyn.format(lstData.projectedApy) : "-"}
</Typography>
</div>
</div>
Expand Down
30 changes: 29 additions & 1 deletion apps/marginfi-v2-ui/src/pages/stake.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
import { useConnection } from "@solana/wallet-adapter-react";
import { useEffect } from "react";
import { PageHeader } from "~/components/PageHeader";
import { StakingStats } from "~/components/Staking";
import { StakingCard } from "~/components/Staking/StakingCard/StakingCard";
import { useWalletContext } from "~/components/useWalletContext";
import { createLstStore } from "~/store/lstStore";

export const useLstStore = createLstStore();

const StakePage = () => {
const { wallet } = useWalletContext();
const { connection } = useConnection();

const [
fetchLstState,
setIsRefreshingStore,
] = useLstStore((state) => [
state.fetchLstState,
state.setIsRefreshingStore,
state.userDataFetched,
]);
useEffect(() => {
setIsRefreshingStore(true);
fetchLstState({ connection, wallet }).catch(console.error);
const id = setInterval(() => {
setIsRefreshingStore(true);
fetchLstState().catch(console.error);
}, 30_000);
return () => clearInterval(id);
}, [wallet]); // eslint-disable-line react-hooks/exhaustive-deps
// ^ crucial to omit both `connection` and `fetchMrgnlendState` from the dependency array
// TODO: fix...

return (
<>
<PageHeader text={"$LST"} />
<div className="flex flex-col h-full justify-center content-center pt-[64px] sm:pt-[16px] gap-4 mx-4">
<StakingStats tvl={1250} projectedApy={0.5215} />
<StakingStats />
<StakingCard />
</div>
</>
Expand Down
File renamed without changes.
Loading

0 comments on commit 1b0d27a

Please sign in to comment.