Skip to content

Commit

Permalink
feat: staking stepper
Browse files Browse the repository at this point in the history
  • Loading branch information
leduyhien152 committed Apr 17, 2024
1 parent 7eb6d1f commit 06a1915
Show file tree
Hide file tree
Showing 11 changed files with 1,269 additions and 1,221 deletions.
32 changes: 15 additions & 17 deletions components/overview/FlexibleStakingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
Badge,
Button,
Switch,
Tooltip,
Typography,
ValueChange,
ValueChangeIndicator,
Expand All @@ -20,7 +18,6 @@ import { Countdown } from "./Countdown";
import { retry } from "@/utils/retry";
import { formatUnits, parseUnits } from "ethers/lib/utils";
import { TokenImage } from "../token-image";
import { useLoginWidget } from "@mochi-web3/login-widget";

interface Props {
hidden: boolean;
Expand All @@ -45,7 +42,6 @@ export const FlexibleStakingCard = (props: Props) => {
updateValues,
initializeValues,
} = useFlexibleStaking();
const { wallets } = useLoginWidget();
const { isOpen: isBoosting, onOpen: onBoost } = useDisclosure();
const {
isOpen: isOpenFlexibleStakeModal,
Expand Down Expand Up @@ -149,19 +145,21 @@ export const FlexibleStakingCard = (props: Props) => {
Flexible
</Badge>,
]}
headerExtra={[
<Tooltip
key="auto-staking"
content="Auto-Staking automates the process of topping up your margin wallet, saving you from manually transferring funds before each trade. This is especially useful if you plan on making frequent trades."
className="max-w-xs text-center z-50"
arrow="bottom-center"
>
<Switch
checked={autoStaking}
onCheckedChange={(autoStaking) => setValues({ autoStaking })}
/>
</Tooltip>,
]}
headerExtra={
[
// <Tooltip
// key="auto-staking"
// content="Auto-Staking automates the process of topping up your margin wallet, saving you from manually transferring funds before each trade. This is especially useful if you plan on making frequent trades."
// className="max-w-xs text-center z-50"
// arrow="bottom-center"
// >
// <Switch
// checked={autoStaking}
// onCheckedChange={(autoStaking) => setValues({ autoStaking })}
// />
// </Tooltip>,
]
}
icon={<TokenImage symbol={stakingToken?.token_symbol} size={24} />}
title={stakingToken?.token_symbol}
description={stakingPool?.description}
Expand Down
33 changes: 11 additions & 22 deletions components/stake/flexible/flexible-stake-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,20 @@ import { useMemo, useState } from "react";
import { TokenAmount } from "@/utils/number";
import { StakeInput } from "../stake-input";
import { useFlexibleStaking } from "@/store/flexible-staking";
import { Spinner } from "@mochi-ui/icons";
import { LoginWidget } from "@mochi-web3/login-widget";
import { utils } from "@consolelabs/mochi-formatter";
import { formatUnits } from "ethers/lib/utils";
import { useWalletNetwork } from "@/hooks/useWalletNetwork";

interface Props {
onStake: (amount: number) => Promise<void>;
onApprove: (amount: number) => Promise<void>;
loading: string | null;
container: HTMLDivElement | null;
initializing: boolean;
onConfirm: (amount: number) => void;
}

export const FlexibleStakeContent = (props: Props) => {
const { onStake, onApprove, loading, container } = props;
const { balance, allowance, apr, stakingToken } = useFlexibleStaking();
const { container, initializing, onConfirm } = props;
const { balance, apr, stakingToken } = useFlexibleStaking();
const chain = useMemo(() => stakingToken?.token_chain_id, [stakingToken]);
const [amount, setAmount] = useState<TokenAmount>({
value: 0,
Expand All @@ -39,9 +37,6 @@ export const FlexibleStakeContent = (props: Props) => {
const convertedBalance = Number(
formatUnits(balance, stakingToken?.token_decimal)
);
const convertedAllowance = Number(
formatUnits(allowance, stakingToken?.token_decimal)
);

return (
<div className="flex flex-col">
Expand Down Expand Up @@ -72,22 +67,16 @@ export const FlexibleStakeContent = (props: Props) => {
<Button
size="lg"
disabled={
amount.value <= 0 || amount.value > convertedBalance || !!loading
amount.value <= 0 || amount.value > convertedBalance || initializing
}
className="mt-3"
onClick={
convertedAllowance >= amount.value
? () => onStake(amount.value)
: () => onApprove(amount.value)
}
onClick={() => onConfirm(amount.value)}
>
{!!loading && <Spinner className="w-4 h-4" />}
{loading ||
(amount.value > convertedBalance
? "Insufficient balance"
: convertedAllowance >= amount.value
? "Stake"
: "Approve Spending Cap")}
{initializing
? "Initializing"
: amount.value > convertedBalance
? "Insufficient balance"
: "Stake"}
</Button>
) : isConnected && !isCorrectNetwork ? (
<Button size="lg" className="mt-3" onClick={changeNetwork}>
Expand Down
131 changes: 33 additions & 98 deletions components/stake/flexible/flexible-stake-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { FlexibleStakeContent } from "./flexible-stake-content";
import { FlexibleStakeResponse } from "./flexible-stake-response";
import { ChainProvider, useLoginWidget } from "@mochi-web3/login-widget";
import { useFlexibleStaking } from "@/store/flexible-staking";
import { retry } from "@/utils/retry";
import { FlexibleStakePreview } from "./flexible-stake-preview";

interface Props {
open: boolean;
Expand All @@ -24,25 +24,25 @@ interface Props {
export const FlexibleStakeModal = (props: Props) => {
const { open, onOpenChange } = props;
const { wallets, getProviderByAddress } = useLoginWidget();
const {
poolContract,
stakingTokenContract,
allowance,
stakedAmount,
stakingToken,
initializeContract,
updateValues,
setValues,
} = useFlexibleStaking();
const { stakingToken, initializeContract, updateValues } =
useFlexibleStaking();
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [state, setState] = useState<"init" | "approved" | "success">("init");
const [loading, setLoading] = useState<
"Initializing" | "Approving" | "Staking" | null
>(null);
const [state, setState] = useState<"init" | "preview" | "success">("init");
const [initializing, setInitializing] = useState(false);
const [amount, setAmount] = useState(0);

const connected = wallets.find((w) => w.connectionStatus === "connected");
const address = connected?.address || "";

const onConfirm = (amount: number) => {
setAmount(amount);
setState("preview");
};

const onSuccess = () => {
setState("success");
};

useEffect(() => {
if (!address) return;
const init = async () => {
Expand All @@ -51,7 +51,7 @@ export const FlexibleStakeModal = (props: Props) => {
if (!provider) {
throw new Error("No provider connected.");
}
setLoading("Initializing");
setInitializing(true);
initializeContract(address, provider);
await updateValues();
} catch (err: any) {
Expand All @@ -64,88 +64,12 @@ export const FlexibleStakeModal = (props: Props) => {
: "Failed to initialize contract interaction",
});
} finally {
setLoading(null);
setInitializing(false);
}
};
init();
}, [address, getProviderByAddress, initializeContract, updateValues]);

const onStake = async (amount: number) => {
if (!poolContract) return;
try {
setLoading("Staking");
const txHash = await poolContract.stake(amount);
if (!txHash) {
throw new Error("Failed to stake");
}
setValues({ latestStaking: { txHash, amount } });
// FIXME: retry to get updated values
await retry(
async () => {
const newStakedAmount = await poolContract.getSenderStakedAmount();
if (!newStakedAmount || newStakedAmount.eq(stakedAmount)) {
throw new Error("Failed to stake");
}
return newStakedAmount;
},
3000,
100
);
await updateValues();
setState("success");
} catch (err: any) {
toast({
scheme: "danger",
title: "Error",
description:
typeof err.message === "string" ? err.message : "Failed to stake",
});
} finally {
setLoading(null);
}
};

const onApprove = async (amount: number) => {
if (!stakingTokenContract || !poolContract) return;
try {
setLoading("Approving");
const txHash = await stakingTokenContract.approveTokenAmount(
poolContract.getAddress(),
amount
);
if (!txHash) {
throw new Error("Failed to approve allowance");
}
// FIXME: retry to get updated values
await retry(
async () => {
const newAllowance = await stakingTokenContract.getAllowance(
poolContract.getAddress()
);
if (!newAllowance || newAllowance.eq(allowance)) {
throw new Error("Failed to approve allowance");
}
return newAllowance;
},
3000,
100
);
await updateValues();
setState("approved");
} catch (err: any) {
toast({
scheme: "danger",
title: "Error",
description:
typeof err.message === "string"
? err.message
: "Failed to approve allowance",
});
} finally {
setLoading(null);
}
};

return (
<Modal
open={open}
Expand All @@ -166,7 +90,7 @@ export const FlexibleStakeModal = (props: Props) => {
}
}}
>
{(state === "init" || state === "approved") && (
{state === "init" && (
<ModalTitle className="relative pb-3">
<Typography level="h6" fontWeight="lg" className="text-center">
Stake {stakingToken?.token_symbol}
Expand All @@ -176,10 +100,21 @@ export const FlexibleStakeModal = (props: Props) => {
</ModalClose>
</ModalTitle>
)}
{(state === "init" || state === "approved") && (
<FlexibleStakeContent
{...{ onApprove, onStake, loading, container }}
/>
{state === "preview" && (
<ModalTitle className="relative pb-3">
<Typography level="h6" fontWeight="lg">
Preview Stake
</Typography>
<ModalClose className="absolute inset-y-0 right-0 h-fit">
<CloseLgLine className="w-7 h-7" />
</ModalClose>
</ModalTitle>
)}
{state === "init" && (
<FlexibleStakeContent {...{ container, initializing, onConfirm }} />
)}
{state === "preview" && (
<FlexibleStakePreview {...{ amount, onSuccess }} />
)}
{state === "success" && (
<FlexibleStakeResponse
Expand Down
Loading

0 comments on commit 06a1915

Please sign in to comment.