Compare commits
8 Commits
4c455bd1f5
...
5bb5b7d7e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bb5b7d7e0 | |||
| e102d819a0 | |||
| be5b102522 | |||
| 73db807107 | |||
| 1663e82172 | |||
| c2d81bc229 | |||
| fe71618369 | |||
| 153749606f |
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ghost-dao-interface",
|
"name": "ghost-dao-interface",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.7",
|
"version": "0.7.15",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -121,7 +121,7 @@ function App() {
|
|||||||
const provider = usePublicClient();
|
const provider = usePublicClient();
|
||||||
const chainId = useChainId();
|
const chainId = useChainId();
|
||||||
|
|
||||||
const isSmallerScreen = useMediaQuery("(max-width: 1047px)");
|
const isSmallerScreen = useMediaQuery("(max-width: 1130px)");
|
||||||
const isSmallScreen = useMediaQuery("(max-width: 600px)");
|
const isSmallScreen = useMediaQuery("(max-width: 600px)");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
1
src/abi/GhostGatekeeper.json
Normal file
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg id="uuid-61b3c585-44d6-4fd8-a0e7-6a5e76478eb1" data-name="uuid-70ba15fb-c44e-4945-a02a-8d07f94a9abb" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 505 505">
|
<svg id="uuid-61b3c585-44d6-4fd8-a0e7-6a5e76478eb1" data-name="uuid-70ba15fb-c44e-4945-a02a-8d07f94a9abb" xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 513 513">
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.uuid-6437a6f0-42e0-4c9c-b0ef-b632253e989a {
|
.uuid-6437a6f0-42e0-4c9c-b0ef-b632253e989a {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg id="uuid-39ebc51d-db02-45d2-ad4b-e7ae31bb915b" data-name="Ethereum" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
<svg id="uuid-39ebc51d-db02-45d2-ad4b-e7ae31bb915b" data-name="Ethereum" xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 254 254">
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.uuid-e080659c-91a4-4965-9905-603394020b2c {
|
.uuid-e080659c-91a4-4965-9905-603394020b2c {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg id="uuid-cf283373-2eb4-44d0-88d2-93533a7f4b1d" data-name="eGHST" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
<svg id="uuid-cf283373-2eb4-44d0-88d2-93533a7f4b1d" data-name="eGHST" xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 254 254">
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.uuid-26b0e53c-73c9-4369-9725-1cee728298be {
|
.uuid-26b0e53c-73c9-4369-9725-1cee728298be {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 254.751 254.751">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 258.751 258.751">
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.uuid-67f42f03-098b-49dc-a7ed-dc13bd713387 {
|
.uuid-67f42f03-098b-49dc-a7ed-dc13bd713387 {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg id="uuid-0fc887e6-719b-4e51-bef8-e5c269508f6b" data-name="sGHST" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
|
<svg id="uuid-0fc887e6-719b-4e51-bef8-e5c269508f6b" data-name="sGHST" xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 254 254">
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.uuid-a205ebe2-7c8f-434c-9bde-29ac218716be {
|
.uuid-a205ebe2-7c8f-434c-9bde-29ac218716be {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@ -118,19 +118,19 @@ const NavItem = ({
|
|||||||
const match = currentLocation.pathname === to || currentLocation.pathname === `/${to}`;
|
const match = currentLocation.pathname === to || currentLocation.pathname === `/${to}`;
|
||||||
|
|
||||||
const linkProps = props.href
|
const linkProps = props.href
|
||||||
? {
|
? {
|
||||||
href: props.href,
|
href: props.href,
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
className: `external-site-link ${className}`,
|
className: `external-site-link ${className}`,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
component: NavLink,
|
component: NavLink,
|
||||||
to: to,
|
to: to,
|
||||||
className: `button-dapp-menu ${className}`,
|
className: `button-dapp-menu ${className}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LinkItem = () => (
|
const LinkItem = () => (
|
||||||
<Link {...linkProps} {...props} underline="hover">
|
<Link {...linkProps} {...props} underline="hover" onClick={(e) => e.stopPropagation()}>
|
||||||
<Box
|
<Box
|
||||||
sx={{ fontFamily: "Ubuntu" }}
|
sx={{ fontFamily: "Ubuntu" }}
|
||||||
display="flex"
|
display="flex"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|||||||
|
|
||||||
const PageTitle = ({ name, subtitle, noMargin }) => {
|
const PageTitle = ({ name, subtitle, noMargin }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const mobile = useMediaQuery(theme.breakpoints.down("700"));
|
const mobile = useMediaQuery(theme.breakpoints.down("900"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@ -136,7 +136,7 @@ const NavContent = ({ chainId, addressChainId }) => {
|
|||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<NavItem icon={StakeIcon} label={`(3, 3) Stake`} to={`/${chainName}/stake`} />
|
<NavItem icon={StakeIcon} label={`(3, 3) Stake`} to={`/${chainName}/stake`} />
|
||||||
<NavItem
|
<NavItem
|
||||||
defaultExpanded
|
defaultExpanded
|
||||||
icon={BondIcon}
|
icon={BondIcon}
|
||||||
@ -180,7 +180,7 @@ const NavContent = ({ chainId, addressChainId }) => {
|
|||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
|
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
|
||||||
{isGovernanceAvailable(chainId, addressChainId) && <NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />}
|
{isGovernanceAvailable(chainId, addressChainId) && <NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />}
|
||||||
<Box className="menu-divider">
|
<Box className="menu-divider">
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
58
src/components/TopBar/GhostChainSelect.jsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Box, Typography, SvgIcon, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
import { parseKnownToken } from "../../components/Token/Token";
|
||||||
|
import { useUnstableProvider } from "../../hooks/ghost";
|
||||||
|
import { PrimaryButton } from "../Button"
|
||||||
|
|
||||||
|
const GHOST_CONNECT = 'https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases';
|
||||||
|
|
||||||
|
function GhostChainSelect({ small }) {
|
||||||
|
const { providerDetail, isConnected } = useUnstableProvider();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (providerDetail) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
height="39px"
|
||||||
|
width={small ? "100px" : "155px"}
|
||||||
|
borderRadius="4px"
|
||||||
|
padding="0 14px"
|
||||||
|
border="1px solid #50759e"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection="row">
|
||||||
|
<SvgIcon component={parseKnownToken("CSPR")} inheritViewBox />
|
||||||
|
{!small && <Typography sx={{ paddingLeft: "6px"}}>CASPER</Typography>}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
width="21px"
|
||||||
|
height="21px"
|
||||||
|
backgroundColor={isConnected ? theme.colors.feedback.success : theme.colors.feedback.error}
|
||||||
|
borderRadius="50%"
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box height="39px" width={small ? "100px" : "155px"}>
|
||||||
|
<PrimaryButton
|
||||||
|
sx={{ margin: "0 !important", padding: "0 !important" }}
|
||||||
|
onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Get GHOST {small ? "" : "Connect"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GhostChainSelect;
|
||||||
@ -71,7 +71,7 @@ function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<FormControl sx={{ width: small ? "100px" : "155px" }}>
|
<FormControl sx={{ width: small ? "auto" : "155px" }}>
|
||||||
<Select
|
<Select
|
||||||
labelId="network-select-helper-label"
|
labelId="network-select-helper-label"
|
||||||
id="network-select-helper"
|
id="network-select-helper"
|
||||||
@ -92,7 +92,7 @@ function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, s
|
|||||||
return (
|
return (
|
||||||
<MenuItem key={chain.name} value={chain.id}>
|
<MenuItem key={chain.name} value={chain.id}>
|
||||||
<Box gap="10px" display="flex" flexDirection="row" alignItems="center">
|
<Box gap="10px" display="flex" flexDirection="row" alignItems="center">
|
||||||
<SvgIcon component={parseKnownToken(chain?.nativeCurrency?.symbol)} inheritViewBox />
|
<SvgIcon sx={{ width: 22 }} component={parseKnownToken(chain?.nativeCurrency?.symbol)} inheritViewBox />
|
||||||
{!small && <Typography>{chain.name}</Typography>}
|
{!small && <Typography>{chain.name}</Typography>}
|
||||||
</Box>
|
</Box>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Box, Button, SvgIcon, useMediaQuery, useTheme } from "@mui/material";
|
|||||||
import MenuIcon from "../../assets/icons/hamburger.svg?react";
|
import MenuIcon from "../../assets/icons/hamburger.svg?react";
|
||||||
import Wallet from "./Wallet"
|
import Wallet from "./Wallet"
|
||||||
import SelectNetwork from "./SelectNetwork";
|
import SelectNetwork from "./SelectNetwork";
|
||||||
|
import GhostChainSelect from "./GhostChainSelect";
|
||||||
|
|
||||||
const PREFIX = "TopBar";
|
const PREFIX = "TopBar";
|
||||||
|
|
||||||
@ -23,8 +23,8 @@ function TopBar({
|
|||||||
setWrongNetworkToastId
|
setWrongNetworkToastId
|
||||||
}) {
|
}) {
|
||||||
const themeColor = useTheme();
|
const themeColor = useTheme();
|
||||||
const desktop = useMediaQuery(themeColor.breakpoints.up(1048));
|
const desktop = useMediaQuery(themeColor.breakpoints.up(1130));
|
||||||
const small = useMediaQuery(themeColor.breakpoints.down(400));
|
const small = useMediaQuery(themeColor.breakpoints.down(600));
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
@ -34,7 +34,14 @@ function TopBar({
|
|||||||
marginRight={desktop ? "33px" : "0px"}
|
marginRight={desktop ? "33px" : "0px"}
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center">
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" width={small ? "calc(100vw - 78px)" : "320px"}>
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
width={small ? "calc(100vw - 78px)" : "500px"}
|
||||||
|
height="40px"
|
||||||
|
>
|
||||||
|
<GhostChainSelect small={small} />
|
||||||
<SelectNetwork
|
<SelectNetwork
|
||||||
wrongNetworkToastId={wrongNetworkToastId}
|
wrongNetworkToastId={wrongNetworkToastId}
|
||||||
setWrongNetworkToastId={setWrongNetworkToastId}
|
setWrongNetworkToastId={setWrongNetworkToastId}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const WalletButton = ({ openWallet, connect }) => {
|
|||||||
const { isConnected, chain } = useAccount();
|
const { isConnected, chain } = useAccount();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const onClick = isConnected ? openWallet : connect;
|
const onClick = isConnected ? openWallet : connect;
|
||||||
const label = isConnected ? "Open Wallet" : `Connect Wallet`;
|
const label = isConnected ? "Wallet" : `Connect`;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
id="fatso-menu-button"
|
id="fatso-menu-button"
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
CurrentIndex,
|
CurrentIndex,
|
||||||
} from "./components/Metric";
|
} from "./components/Metric";
|
||||||
import TokenInfo from "./components/TokenInfo";
|
import TokenInfo from "./components/TokenInfo";
|
||||||
|
import ProtocolDetails from "./components/ProtocolDetails";
|
||||||
|
|
||||||
import Paper from "../../components/Paper/Paper";
|
import Paper from "../../components/Paper/Paper";
|
||||||
import PageTitle from "../../components/PageTitle/PageTitle";
|
import PageTitle from "../../components/PageTitle/PageTitle";
|
||||||
@ -60,7 +61,7 @@ const MetricsDashboard = ({ chainId }) => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Paper style={{ padding: "0px" }} fullWidth={true} >
|
<Paper style={{ padding: "0px" }} fullWidth={true} >
|
||||||
<TokenInfo isMobileScreen={isMobileScreen} chainId={chainId} />
|
<ProtocolDetails theme={theme} isMobileScreen={isMobileScreen} chainId={chainId} />
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
137
src/containers/TreasuryDashboard/components/ProtocolDetails.jsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { Grid, Box, Typography } from "@mui/material";
|
||||||
|
import { useConfig } from "wagmi";
|
||||||
|
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { formatNumber } from "../../../helpers";
|
||||||
|
import { useEpoch, useGatekeeperApy } from "../../../hooks/staking";
|
||||||
|
import { useTokenSymbol, useBalance, useCirculatingSupply } from "../../../hooks/tokens";
|
||||||
|
import { SecondaryButton } from "../../../components/Button";
|
||||||
|
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||||
|
|
||||||
|
import { GATEKEEPER_ADDRESSES, EMPTY_ADDRESS } from "../../../constants/addresses";
|
||||||
|
|
||||||
|
const ProtocolDetail = ({ isMobileScreen, theme, name, sideName, url, urlParams, description }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<Box position="relative" width={`${isMobileScreen ? "100%" : "48%"}`}>
|
||||||
|
<Box
|
||||||
|
borderRadius="9px"
|
||||||
|
padding="12px"
|
||||||
|
sx={{ backgroundColor: theme.colors.paper.card }}
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Box display="flex" gap={"3px"} alignItems="center" justifyContent="space-between">
|
||||||
|
<Typography fontSize="20px" fontWeight="bold" lineHeight="33px">
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
<Typography color={theme.colors.primary[300]} fontSize="20px" fontWeight="bold" lineHeight="33px">
|
||||||
|
{sideName}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<>
|
||||||
|
<Box my="18px">
|
||||||
|
<Typography color={theme.colors.gray[40]} mt="9px">
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" justifyContent="center" width="100%">
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={() => urlParams
|
||||||
|
? navigate({
|
||||||
|
pathname: url,
|
||||||
|
search: urlParams.toString()
|
||||||
|
})
|
||||||
|
: navigate(url, { replace: true })
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</SecondaryButton>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProtocolDetails = ({ chainId, isMobileScreen, theme, }) => {
|
||||||
|
const config = useConfig();
|
||||||
|
const nativeSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
|
||||||
|
const networkName = config?.getClient()?.chain?.name;
|
||||||
|
|
||||||
|
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||||
|
const { symbol: csprSymbol } = useTokenSymbol(chainId, "CSPR");
|
||||||
|
const { contractAddress: ftsoAddress } = useBalance(chainId, "FTSO", EMPTY_ADDRESS);
|
||||||
|
|
||||||
|
const circulatingSupply = useCirculatingSupply(chainId, "STNK");
|
||||||
|
const gatekeepedApy = useGatekeeperApy(chainId);
|
||||||
|
const { epoch } = useEpoch(chainId);
|
||||||
|
|
||||||
|
const apyInner = useMemo(() => {
|
||||||
|
let apy = Infinity;
|
||||||
|
if (circulatingSupply._value > 0n) {
|
||||||
|
const value = epoch.distribute.div(circulatingSupply);
|
||||||
|
apy = 100 * (Math.pow(1 + parseFloat(value.toString()), 1095) - 1);
|
||||||
|
if (apy === 0) apy = Infinity;
|
||||||
|
}
|
||||||
|
return apy;
|
||||||
|
}, [circulatingSupply, epoch]);
|
||||||
|
|
||||||
|
const bridgeNumbers = useMemo(() => {
|
||||||
|
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
||||||
|
const number = 1 + connectedNetworks * 3;
|
||||||
|
return `(${number}, ${number})`;
|
||||||
|
}, [chainId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={0} justifyContent={"center"}>
|
||||||
|
<Box display="flex" flexWrap="wrap" justifyContent="space-between" mt="10px" gap="25px">
|
||||||
|
<ProtocolDetail
|
||||||
|
isMobileScreen={isMobileScreen}
|
||||||
|
theme={theme}
|
||||||
|
url={`/${networkName.toLowerCase()}/dex/uniswap`}
|
||||||
|
urlParams={createSearchParams({
|
||||||
|
from: EMPTY_ADDRESS,
|
||||||
|
to: `${ftsoAddress}`,
|
||||||
|
})}
|
||||||
|
name="(3, 3) Swap"
|
||||||
|
sideName="Unlock Magic"
|
||||||
|
description={`Buying strategy expands bond capacity triggering a positive loop to strengthen the Treasury and attract more stakers. Swap ${nativeSymbol} for ${ftsoSymbol} to unlock (3, 3) Stake.`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProtocolDetail
|
||||||
|
isMobileScreen={isMobileScreen}
|
||||||
|
theme={theme}
|
||||||
|
url={`/${networkName.toLowerCase()}/bonds`}
|
||||||
|
name="(1, 1) Bond"
|
||||||
|
sideName="Up to 40% Discount"
|
||||||
|
description={`Bonding strategy grows Treasury and builds Protocol Owned Liquidity (POL) by allowing usersto acquire ${csprSymbol} at a discount. Take advantage of the next available bond.`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProtocolDetail
|
||||||
|
isMobileScreen={isMobileScreen}
|
||||||
|
theme={theme}
|
||||||
|
url={`/${networkName.toLowerCase()}/stake`}
|
||||||
|
name="(3, 3) Stake"
|
||||||
|
sideName={`${formatNumber(apyInner, 0)}% APY`}
|
||||||
|
description={`Staking enables (3, 3) coordination by aligning long-term incentives, sustainably backed (1, 1) Bonds, LP fees, and Farming. Stake ${ftsoSymbol} to earn rewards and unlock ${bridgeNumbers} Stake\u00B2.`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProtocolDetail
|
||||||
|
isMobileScreen={isMobileScreen}
|
||||||
|
theme={theme}
|
||||||
|
url={`/${networkName.toLowerCase()}/bridge`}
|
||||||
|
name={`${bridgeNumbers} Stake\u00B2`}
|
||||||
|
sideName={`${formatNumber(apyInner * gatekeepedApy, 0)}% APY`}
|
||||||
|
description={`Staking\u00B2 strategy further deepens long-term incentives powered by sustainable cross-chain bridging revenue. ${bridgeNumbers} Stake\u00B2 your ${csprSymbol} to receive organic APY with no warmup and dillution.`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtocolDetails;
|
||||||
@ -1,14 +1,19 @@
|
|||||||
import { createContext, useContext, useState, useMemo } from "react"
|
import { createContext, useEffect, useContext, useState, useMemo, useCallback, useRef } from "react"
|
||||||
import { Unstable } from "@substrate/connect-discovery"
|
import { Unstable } from "@substrate/connect-discovery"
|
||||||
import { createClient } from "@polkadot-api/substrate-client"
|
import { createClient } from "@polkadot-api/substrate-client"
|
||||||
import { getObservableClient } from "@polkadot-api/observable-client"
|
import { getObservableClient } from "@polkadot-api/observable-client"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
|
const MAX_BLOCK_TIMEOUT = 15000
|
||||||
const DEFAULT_CHAIN_ID = "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3"
|
const DEFAULT_CHAIN_ID = "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3"
|
||||||
const UnstableProvider = createContext(null)
|
const UnstableProvider = createContext(null)
|
||||||
export const useUnstableProvider = () => useContext(UnstableProvider)
|
export const useUnstableProvider = () => useContext(UnstableProvider)
|
||||||
|
|
||||||
export const UnstableProviderProvider = ({ children }) => {
|
export const UnstableProviderProvider = ({ children }) => {
|
||||||
|
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [reconnectTicket, setReconnectTicket] = useState(0);
|
||||||
|
|
||||||
const { data: providerDetails } = useSWR("getGhostProviders", () =>
|
const { data: providerDetails } = useSWR("getGhostProviders", () =>
|
||||||
Unstable.getSubstrateConnectExtensionProviders()
|
Unstable.getSubstrateConnectExtensionProviders()
|
||||||
);
|
);
|
||||||
@ -19,36 +24,66 @@ export const UnstableProviderProvider = ({ children }) => {
|
|||||||
() => providerDetail ? providerDetail.provider : null
|
() => providerDetail ? providerDetail.provider : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
|
|
||||||
|
|
||||||
const client = useMemo(() => {
|
const client = useMemo(() => {
|
||||||
if (!provider || !chainId) return undefined;
|
if (!provider || !chainId) return undefined;
|
||||||
|
|
||||||
const chain = provider.getChains()[chainId];
|
const chain = provider.getChains()[chainId];
|
||||||
if (!chain) return undefined;
|
if (!chain) return undefined;
|
||||||
return createClient(chain.connect);
|
|
||||||
}, [provider, chainId]);
|
|
||||||
|
|
||||||
const observableClient = useMemo(() => {
|
return createClient(chain.connect)
|
||||||
return client ? getObservableClient(client) : undefined;
|
}, [provider, chainId, reconnectTicket]);
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
const chainHead$ = useMemo(() => {
|
const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
|
||||||
return observableClient ? observableClient.chainHead$() : undefined;
|
const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]);
|
||||||
}, [observableClient]);
|
|
||||||
|
const lastBlockNumber = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chainHead$) return;
|
||||||
|
|
||||||
|
lastBlockNumber.current = 0;
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
const sub = chainHead$.bestBlocks$.subscribe({
|
||||||
|
next: (blocks) => {
|
||||||
|
const currentHeight = blocks.at(0)?.number ?? -1;
|
||||||
|
|
||||||
|
if (currentHeight > lastBlockNumber.current) {
|
||||||
|
lastBlockNumber.current = currentHeight;
|
||||||
|
setIsConnected(true);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
setIsConnected(false);
|
||||||
|
setReconnectTicket(t => t + 1);
|
||||||
|
}, MAX_BLOCK_TIMEOUT);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
setIsConnected(false);
|
||||||
|
setTimeout(() => setReconnectTicket(t => t + 1), 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
sub.unsubscribe();
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [chainHead$]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
isConnected,
|
||||||
|
providerDetails,
|
||||||
|
providerDetail,
|
||||||
|
connectProviderDetail: setProviderDetail,
|
||||||
|
chainId,
|
||||||
|
client,
|
||||||
|
setChainId,
|
||||||
|
chainHead$
|
||||||
|
}), [isConnected, providerDetails, providerDetail, chainId, client, chainHead$]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnstableProvider.Provider
|
<UnstableProvider.Provider value={value}>
|
||||||
value={{
|
|
||||||
providerDetails,
|
|
||||||
providerDetail,
|
|
||||||
connectProviderDetail: setProviderDetail,
|
|
||||||
provider,
|
|
||||||
chainId,
|
|
||||||
client,
|
|
||||||
setChainId,
|
|
||||||
chainHead$
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</UnstableProvider.Provider>
|
</UnstableProvider.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,10 +7,50 @@ import { config } from "../../config";
|
|||||||
import { isNetworkLegacyType } from "../../constants";
|
import { isNetworkLegacyType } from "../../constants";
|
||||||
import { STAKING_ADDRESSES } from "../../constants/addresses";
|
import { STAKING_ADDRESSES } from "../../constants/addresses";
|
||||||
import { abi as StakingAbi } from "../../abi/GhostStaking.json";
|
import { abi as StakingAbi } from "../../abi/GhostStaking.json";
|
||||||
|
import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
|
||||||
|
|
||||||
import { shorten } from "../../helpers";
|
import { shorten } from "../../helpers";
|
||||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||||
|
|
||||||
|
export const useGatekeeperApy = (chainId) => {
|
||||||
|
const { data: gatekeeper, refetch } = useReadContract({
|
||||||
|
abi: StakingAbi,
|
||||||
|
address: STAKING_ADDRESSES[chainId],
|
||||||
|
functionName: "gatekeeper",
|
||||||
|
scopeKey: `gatekeeper-${chainId}`,
|
||||||
|
chainId: chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: metadata, error } = useReadContract({
|
||||||
|
abi: GatekeeperAbi,
|
||||||
|
address: gatekeeper,
|
||||||
|
functionName: "gatekeeperMetadata",
|
||||||
|
scopeKey: `gatekeeperMetadata-${chainId}-${gatekeeper}`,
|
||||||
|
chainId: chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const amountIn = new DecimalBigNumber(metadata?.amountIn ?? 0n, 18);
|
||||||
|
const amountOut = new DecimalBigNumber(metadata?.amountOut ?? 0n, 18);
|
||||||
|
|
||||||
|
const deployedAt = metadata?.deployedAt ?? 0;
|
||||||
|
const unixSeconds = Math.floor(Date.now() / 1000);
|
||||||
|
const power = 365 * 86400 / (unixSeconds - deployedAt);
|
||||||
|
|
||||||
|
const feeIn = new DecimalBigNumber(6900n, 2);
|
||||||
|
const feeOut = new DecimalBigNumber(6900n, 2);
|
||||||
|
const stakeRatio = new DecimalBigNumber(69000n, 3);
|
||||||
|
|
||||||
|
const numerator = amountIn.mul(feeIn).add(amountOut.mul(feeOut));
|
||||||
|
const denominator = amountIn.mul(stakeRatio).sub(amountOut.mul(stakeRatio));
|
||||||
|
|
||||||
|
if (denominator?.toString() === "0") {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Number(numerator.div(denominator).toString());
|
||||||
|
return Math.pow(1 + result, power);
|
||||||
|
}
|
||||||
|
|
||||||
export const useCurrentIndex = (chainId) => {
|
export const useCurrentIndex = (chainId) => {
|
||||||
const { data: index, refetch } = useReadContract({
|
const { data: index, refetch } = useReadContract({
|
||||||
abi: StakingAbi,
|
abi: StakingAbi,
|
||||||
|
|||||||