Skip to content

Commit

Permalink
feat: deploy staking page (#66)
Browse files Browse the repository at this point in the history
Signed-off-by: jonbray.eth <jon@xeon-protocol.io>
  • Loading branch information
heyJonBray authored Sep 13, 2024
1 parent 91a6dd2 commit bc5b22f
Show file tree
Hide file tree
Showing 5 changed files with 652 additions and 15 deletions.
258 changes: 258 additions & 0 deletions src/app/stake/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
'use client';

import { useEffect, useState, useMemo } from 'react';
import { Image } from '@chakra-ui/react';
import Lottie from 'react-lottie-player';
import lottieJson from '@/assets/animations/PE2.json';
import XeonStakingPoolABI from '@/abi/XeonStakingPool.abi.json';
import { Constants } from '@/abi/constants';
import Header from '@/components/Header';
import UserAssets from '@/components/staking/UserAssets';
import { ethers } from 'ethers';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Spinner,
useDisclosure,
} from '@chakra-ui/react';
import BookmarkAdded from '@/components/BookmarkAdded';

function Page() {
const [voteValue, setVoteValue] = useState(5); // state for user buyback vote value
// todo: for mainnet, ensure currentPercentage is proper default
const [currentPercentage, setCurrentPercentage] = useState(5); // state for current buyback percentage
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const { isOpen, onOpen, onClose } = useDisclosure();

// init provider and signer
useEffect(() => {
const initializeProvider = async () => {
if (typeof window !== 'undefined' && window.ethereum) {
const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = web3Provider.getSigner();
setProvider(web3Provider);
setSigner(signer);
}
};

initializeProvider();
}, []);

// memoize the XeonStakingPool contract instance to avoid re-creating it on every render
const XeonStakingPool = useMemo(() => {
if (!provider || !signer) return null;
return new ethers.Contract(
Constants.testnet.XeonStakingPool,
XeonStakingPoolABI,
signer
);
}, [provider, signer]);

useEffect(() => {
// fetch current buyback percentage from contract
const fetchBuybackPercentage = async () => {
if (XeonStakingPool) {
try {
// todo: for mainnet, ensure value is formatted correctly (N/10000)
const percentage = await XeonStakingPool.buyBackPercentage(); // assume integer value from contract
setCurrentPercentage(percentage.toNumber()); // update state with value
} catch (error) {
console.error('Error fetching buyback percentage:', error);
}
}
};

fetchBuybackPercentage();
}, [XeonStakingPool]);

// handle increment and decrement of vote value
// todo: for mainnet, ensure vote value is clamped to contract min/max
const handleIncrement = () => {
setVoteValue((prevValue) => Math.min(prevValue + 1, 100));
};

const handleDecrement = () => {
setVoteValue((prevValue) => Math.max(prevValue - 1, 1));
};

const handleVote = async () => {
if (!XeonStakingPool || voteValue < 1 || voteValue > 100) {
setMessage('Please enter a value between 1 and 100');
return;
}

setLoading(true);
onOpen();

try {
const tx = await XeonStakingPool.voteForBuybackPercentage(voteValue);
await tx.wait();
setLoading(false);
setMessage(`Vote successful for ${voteValue}% buyback`);
} catch (error) {
console.error('Vote failed', error);
setLoading(false);
setMessage('Vote failed, please try again.');
}
};

// handle vote value change
const handleVoteChange = (e) => {
const value = parseInt(e.target.value);
if (Number.isNaN(value)) {
setVoteValue(1);
} else {
setVoteValue(Math.min(Math.max(value, 1), 100));
}
};

return (
<div className="bg-[#000] lg:min-h-[100vh] 2xl:min-h-[50vh] px-8 pt-8 max-w-screen-2xl mx-auto relative">
<Header />
<div className="flex flex-col md:gap-12 md:flex-row justify-between 2xl:mt-[20%] mt-[18%]">
<div className="md:w-[40%] lg:w-auto md:px-0 lg:px-18 flex items-center md:block">
<h1 className="text-grey text-3xl md:text-5xl lg:text-7xl">Stake</h1>
<h1 className="text-floral text-3xl md:text-5xl lg:text-7xl ml-1 md:ml-10">
Xeon
</h1>

<Image
src="/dotted.webp"
alt="container"
className="md:absolute top-[10%] w-[40%] left-[-10%] hidden lg:block"
/>
<Lottie
className="w-[40%] md:absolute top-[-55px] 2xl:right-[47%] right-[46%] hidden lg:block"
loop
animationData={lottieJson}
play
/>
</div>
<div className="relative">
<p className="text-grey text-lg w-[85%] mt-4">
Stake your XEON tokens in just two simple steps.
</p>
<div className="md:absolute md:top-24 lg:top-20 md:left-[30px] lg:left-8 w-full h-full">
<p className="text-grey md:text-justify text-lg md:w-[80%]">
Stake XEON tokens to be eligible for revenue sharing. The staking
window opens for 3 days at the end of each epoch, at which time
XEON can be staked or unstaked. Protocol revenue is deposited is
deposited into the staking pool.
</p>
</div>
<Image
src="/card-109.svg"
h={{
base: '150px',
md: '200px',
lg: '185px',
}}
alt="container"
className="relative hidden md:block ml-[-20px]"
/>
</div>
</div>
<div className="mt-20">
<UserAssets />
</div>
<div className="md:flex justify-between gap-8 px-8 pb-20">
<div className="w-full md:w-1/2 p-5">
<div className="text-grey text-lg mt-4 border-2 p-2 rounded-md">
<h3 className="text-grey text-3xl md:text-5xl lg:text-7xl">
Settle
</h3>
<p className="text-left mt-2">
Close expired positions and collect fess into the staking pool
</p>
<div className="flex">
<button className="m-auto text-white bg-floral mx-auto mt-10 mb-12 px-8 p-2 rounded-full border-t-none border-b-[1px] border-r-[1px] border-l-[1px] border-button-gradient hover:bg-purple hover:border-lime">
Settle
</button>
</div>
</div>
</div>
<div className="w-full md:w-1/2 p-5">
<div className="text-grey text-lg mt-4 border-2 p-2 rounded-md">
<h3 className="text-grey text-3xl md:text-5xl lg:text-7xl">
$XEON Buyback
</h3>
<p className="text-left mt-2">
What percentage of protocol revenue should be used to buyback
$XEON token?
</p>
<div className="flex items-center mt-5">
<button
className="text-white bg-floral px-4 p-2 rounded-full border-[1px] border-button-gradient hover:bg-purple hover:border-lime"
onClick={handleDecrement}
>
-
</button>

<input
type="number"
value={voteValue}
onChange={handleVoteChange}
min={1}
max={100}
className="border-[1px] text-center bg-[#71637f4d] mx-3 rounded-xl border-grey p-2 focus:outline-lime w-[40%]"
/>

<button
className="text-white bg-floral px-4 p-2 rounded-full border-[1px] border-button-gradient hover:bg-purple hover:border-lime"
onClick={handleIncrement}
>
+
</button>
<button
className="m-auto text-white bg-floral mx-auto px-8 p-2 rounded-full border-t-none border-b-[1px] border-r-[1px] border-l-[1px] border-button-gradient hover:bg-purple hover:border-lime"
onClick={handleVote}
>
Vote
</button>
</div>
<p className="mt-3">
Current Buyback Percentage: {currentPercentage}%
</p>
</div>
</div>
</div>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent bg={'#000'}>
<ModalHeader bg={'#000'} color={'white'}>
Vote Feedback
</ModalHeader>
<ModalBody bg={'#000'}>
{loading ? (
<Spinner />
) : (
<BookmarkAdded
message={message}
status={loading ? 'loading' : 'success'}
/>
)}
</ModalBody>
<ModalFooter>
<button
className="
bg-gradient-button text-white px-4 py-2 rounded mt-4
"
onClick={onClose}
>
Close
</button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
);
}

export default Page;
19 changes: 9 additions & 10 deletions src/components/BookmarkAdded.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {Image} from "@chakra-ui/react";

import { Image } from '@chakra-ui/react';
const explorerUrls = {
0: "https://sepolia.basescan.org/tx/",
1: "https://etherscan.io/tx/",
56: "https://bscscan.com/tx/",
137: "https://polygonscan.com/tx/",
84532: "https://sepolia.basescan.org/tx/",
0: 'https://sepolia.basescan.org/tx/',
1: 'https://etherscan.io/tx/',
56: 'https://bscscan.com/tx/',
137: 'https://polygonscan.com/tx/',
84532: 'https://sepolia.basescan.org/tx/',
};

function BookmarkAdded({message, status, chainId, txHash}) {
function BookmarkAdded({ message, status, chainId, txHash }) {
const explorerUrl = explorerUrls[chainId]
? `${explorerUrls[chainId]}${txHash}`
: null;
Expand All @@ -17,14 +16,14 @@ function BookmarkAdded({message, status, chainId, txHash}) {
<div className="bg-black text-grey flex flex-col justify-center items-center">
<p className="text-lg mb-4 text-center">{status}</p>
<Image
src={status === "success" ? "/success.webp" : "/fail.webp"}
src={status === 'success' ? '/success.webp' : '/fail.webp'}
alt="transaction status"
className=""
/>

<h3 className="text-2xl mt-4 text-center">{message}</h3>

{status === "success" && explorerUrl && (
{status === 'success' && explorerUrl && (
<a
href={explorerUrl}
target="_blank"
Expand Down
6 changes: 3 additions & 3 deletions src/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function Header() {
<Tooltip label="Page under construction">
<div className="cursor-not-allowed">Analytics</div>
</Tooltip>
<Link href={"/guide"}>Guide</Link>
<Link href={"/stake"}>Stake</Link>
<Link href={"/"}>Claim</Link>
<Link
href={"https://docs.xeon-protocol.io/documentation"}
Expand Down Expand Up @@ -160,8 +160,8 @@ function Header() {
Analytics
</div>
</Tooltip>
<Link href="/guide">
<p className="hover:text-gray-400">Guide</p>
<Link href="/stake">
<p className="hover:text-gray-400">Stake</p>
</Link>
<Link href="/">
<p className="hover:text-gray-400">Claim</p>
Expand Down
Loading

0 comments on commit bc5b22f

Please sign in to comment.