Compare commits

..

No commits in common. "main" and "dao-governance" have entirely different histories.

104 changed files with 1770 additions and 3959 deletions

View File

@ -1,7 +1,7 @@
{
"name": "ghost-dao-interface",
"private": true,
"version": "0.7.58",
"version": "0.5.20",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -5,27 +5,18 @@ import CssBaseline from "@mui/material/CssBaseline";
import { styled, ThemeProvider } from "@mui/material/styles";
import { lazy, Suspense, useCallback, useEffect, useState } from "react";
import toast, { Toaster } from "react-hot-toast";
import { Outlet, Navigate, Route, Routes, useLocation, useParams } from "react-router-dom";
import {
useAccount,
useConnect,
useChainId,
useConfig,
usePublicClient,
useSwitchChain,
injected
} from "wagmi";
import { watchChainId } from '@wagmi/core'
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import { useAccount, useConnect, useChainId, useConfig, usePublicClient, injected } from "wagmi";
import Messages from "./components/Messages/Messages";
import NavDrawer from "./components/Sidebar/NavDrawer";
import Sidebar from "./components/Sidebar/Sidebar";
import TopBar from "./components/TopBar/TopBar";
import BreakoutModal from "./containers/Breakout/BreakoutModal";
import { shouldTriggerSafetyCheck } from "./helpers";
import { isNetworkAvailable } from "./constants";
import { isNetworkAvailable, isNetworkLegacy, isGovernanceAvailable } from "./constants";
import useTheme from "./hooks/useTheme";
import { useUnstableProvider } from "./hooks/ghost";
import { dark as darkTheme } from "./themes/dark.js";
import { girth as gTheme } from "./themes/girth.js";
import { light as lightTheme } from "./themes/light.js";
@ -35,6 +26,8 @@ const Bonds = lazy(() => import("./containers/Bond/Bonds"));
const BondModalContainer = lazy(() => import("./containers/Bond/BondModal"));
const StakeContainer = lazy(() => import("./containers/Stake/StakeContainer"));
const TreasuryDashboard = lazy(() => import("./containers/TreasuryDashboard/TreasuryDashboard"));
const Faucet = lazy(() => import("./containers/Faucet/Faucet"));
const Wrapper = lazy(() => import("./containers/WethWrapper/WethWrapper"));
const Dex = lazy(() => import("./containers/Dex/Dex"));
const Bridge = lazy(() => import("./containers/Bridge/Bridge"));
const NotFound = lazy(() => import("./containers/NotFound/NotFound"));
@ -107,15 +100,16 @@ function App() {
const [theme, toggleTheme] = useTheme();
const config = useConfig();
const { chains } = useSwitchChain();
const { connect, error: errorMessage } = useConnect();
const tryConnectInjected = () => connect({ connector: injected() });
const bondIndexes = [];
const [wrongNetworkToastId, setWrongNetworkToastId] = useState(null);
const [isSidebarExpanded, setIsSidebarExpanded] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const { address = "", chainId: addressChainId, status, isConnected, isReconnecting } = useAccount();
const { address = "", chainId: addressChainId, isConnected, isReconnecting } = useAccount();
const provider = usePublicClient();
const chainId = useChainId();
@ -123,6 +117,24 @@ function App() {
const isSmallerScreen = useMediaQuery("(max-width: 1047px)");
const isSmallScreen = useMediaQuery("(max-width: 600px)");
const {
providerDetail,
providerDetails,
connectProviderDetail
} = useUnstableProvider()
useEffect(() => {
// TODO: make sure we are using correct extension
const maybeProvider = providerDetails?.find(obj => obj.info.rdns === "io.ghostchain.GhostWalletExtension")
if (maybeProvider && !providerDetail) {
try {
connectProviderDetail(maybeProvider)
} catch (e) {
console.log(e)
}
}
}, [providerDetail, providerDetails, connectProviderDetail])
useEffect(() => {
if (shouldTriggerSafetyCheck()) {
toast.success("Safety Check: Always verify you're on app.dao.ghostchain.io!", { duration: 5000 });
@ -131,17 +143,17 @@ function App() {
useEffect(() => {
if (isConnected && chainId !== addressChainId) {
if (wrongNetworkToastId === null) {
const toastId = toast.loading("You are connected to wrong network. Use one of the deployed networks please.", {
position: 'bottom-right'
});
setWrongNetworkToastId(toastId);
}
const toastId = toast.loading("You are connected to wrong network. Use one of the deployed networks please.", {
position: 'bottom-right'
});
setWrongNetworkToastId(toastId);
} else {
toast.dismiss(wrongNetworkToastId);
setWrongNetworkToastId(null);
if (wrongNetworkToastId) {
toast.dismiss(wrongNetworkToastId);
setWrongNetworkToastId(null);
}
}
}, [chainId, addressChainId, isConnected, wrongNetworkToastId])
}, [chainId, addressChainId, isConnected])
useEffect(() => {
if (errorMessage) {
@ -158,6 +170,7 @@ function App() {
};
const themeMode = theme === "light" ? lightTheme : theme === "dark" ? darkTheme : gTheme;
const chainExists = isNetworkAvailable(chainId, addressChainId);
useEffect(() => {
@ -174,7 +187,6 @@ function App() {
wrongNetworkToastId={wrongNetworkToastId}
setWrongNetworkToastId={setWrongNetworkToastId}
address={address}
status={status}
chainId={addressChainId ? addressChainId : chainId}
chainExists={chainExists}
handleDrawerToggle={handleDrawerToggle}
@ -190,21 +202,24 @@ function App() {
<div className={`${classes.content} ${isSmallerScreen && classes.contentShift}`}>
<Suspense fallback={<div></div>}>
<BreakoutModal chainId={chainId} address={address} />
<Routes>
<Route path="/" element={<Navigate to={chainExists ? `/${chains.at(0).name.toLowerCase()}/dashboard` : "/empty"} />} />
<Route path="/" element={<Navigate to={chainExists ? "/dashboard" : "/empty"} />} />
{chainExists &&
<Route path="/:network" element={<AvailableNetworkGuard allowedNetworks={chains.map(chain => chain.name.toLowerCase())} /> }>
<Route path="dashboard" element={<TreasuryDashboard chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bonds" element={<Bonds connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bonds/:id" element={<BondModalContainer config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="stake" element={<StakeContainer config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bridge" element={<Bridge config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="dex/:name" element={<Dex config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="governance" element={<Governance config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="governance/:id" element={<ProposalDetails config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="governance/create" element={<NewProposal config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
</Route>
<>
<Route path="/dashboard" element={<TreasuryDashboard chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="/bonds" element={<Bonds connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="/bonds/:id" element={<BondModalContainer connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="/stake" element={<StakeContainer connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId}/>} />
{isNetworkLegacy(chainId)
? <Route path="/faucet" element={<Faucet config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
: <Route path="/wrapper" element={<Wrapper config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
}
<Route path="/bridge" element={<Bridge config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="/dex/:name" element={<Dex connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
{isGovernanceAvailable(chainId, addressChainId) && <Route path="/governance" element={<Governance config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />}
{isGovernanceAvailable(chainId, addressChainId) && <Route path="/governance/:id" element={<ProposalDetails config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />}
{isGovernanceAvailable(chainId, addressChainId) && <Route path="/governance/create" element={<NewProposal config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />}
</>
}
<Route path="/empty" element={<NotFound
wrongNetworkToastId={wrongNetworkToastId}
@ -227,12 +242,4 @@ function App() {
);
}
const AvailableNetworkGuard = ({ allowedNetworks }) => {
const { network } = useParams();
if (!allowedNetworks.includes(network)) {
return <Navigate to="/empty" replace />;
}
return <Outlet />
}
export default App;

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"abi":[{"inputs":[{"internalType":"address","name":"_staking","type":"address"},{"internalType":"uint256","name":"_ghostedSupply","type":"uint256"},{"internalType":"uint256","name":"_existential","type":"uint256"},{"internalType":"uint48","name":"_deployedAt","type":"uint48"},{"internalType":"uint104","name":"_amountIn","type":"uint104"},{"internalType":"uint104","name":"_amountOut","type":"uint104"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AlreadyExecuted","type":"error"},{"inputs":[],"name":"NonExistentAmount","type":"error"},{"inputs":[],"name":"NotStaking","type":"error"},{"inputs":[],"name":"WrongSignature","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"receiver","type":"bytes32"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Ghosted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Materialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"aggregatedPublicKey","type":"uint256"}],"name":"Rotated","type":"event"},{"inputs":[],"name":"DIVISOR","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"existentialDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"receiver","type":"bytes32"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ghost","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"ghostedSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"rx","type":"uint256"},{"internalType":"uint256","name":"s","type":"uint256"}],"name":"materialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"metadata","outputs":[{"components":[{"internalType":"uint48","name":"deployedAt","type":"uint48"},{"internalType":"uint104","name":"amountIn","type":"uint104"},{"internalType":"uint104","name":"amountOut","type":"uint104"}],"internalType":"struct IGatekeeperMetadata.GatekeeperMetadata","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"aggregatedPublicKey","type":"uint256"},{"internalType":"uint256","name":"rx","type":"uint256"},{"internalType":"uint256","name":"s","type":"uint256"}],"name":"rotate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"staking","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
<?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="-4 -4 513 513">
<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">
<defs>
<style>
.uuid-6437a6f0-42e0-4c9c-b0ef-b632253e989a {

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,5 +1,5 @@
<?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="-2 -2 254 254">
<svg id="uuid-39ebc51d-db02-45d2-ad4b-e7ae31bb915b" data-name="Ethereum" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
<defs>
<style>
.uuid-e080659c-91a4-4965-9905-603394020b2c {

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,5 +1,5 @@
<?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="-2 -2 254 254">
<svg id="uuid-cf283373-2eb4-44d0-88d2-93533a7f4b1d" data-name="eGHST" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
<defs>
<style>
.uuid-26b0e53c-73c9-4369-9725-1cee728298be {
@ -21,4 +21,4 @@
<path class="uuid-26b0e53c-73c9-4369-9725-1cee728298be" d="m66.78,163.74c2.41,0,4.76,1.17,6.19,3.3,2.27,3.4,5.12,6.55,8.46,9.36,3.84,3.27,8.17,6.14,12.9,8.57,9.33,4.8,19.72,7.51,30.06,7.86,5.23.17,10.45-.28,15.5-1.35,5.01-1.07,9.96-2.8,14.69-5.14,4.55-2.25,9.03-5.12,13.31-8.55,3.83-3.17,7.58-6.89,11.4-11.33l.39-.41c2.02-1.87,5.18-1.9,7.34-.06,2.12,1.8,2.65,4.77,1.28,7.06l-.19.31-.23.28c-4.12,5.06-8.21,9.34-12.51,13.1-4.9,4.16-10.02,7.64-15.28,10.42-5.57,2.95-11.43,5.18-17.42,6.63-6.02,1.45-12.27,2.16-18.55,2.14-12.31-.08-24.77-2.97-36.04-8.37-5.79-2.77-11.14-6.12-15.91-9.96-4.46-3.56-8.33-7.62-11.48-12.06-1.21-1.7-1.65-3.84-1.22-5.88.43-2.03,1.7-3.81,3.49-4.87,1.19-.71,2.51-1.05,3.81-1.05Z"/>
<path class="uuid-26b0e53c-73c9-4369-9725-1cee728298be" d="m105.35,44.06c1.3,0,2.58.33,3.72.98,1.77,1.01,3.06,2.73,3.54,4.71.95,3.9-1.42,7.92-5.29,8.95-8.06,2.16-16.05,5.98-23.1,11.04-7.76,5.57-14.37,12.59-19.13,20.31-4.9,7.92-8.02,16.99-9.02,26.22-1.01,8.71-.13,18.21,2.63,28.27l.09.4c.51,2.71-1.12,5.41-3.78,6.28-2.64.87-5.47-.28-6.73-2.72l-.2-.4-.13-.43c-3.5-11.4-4.83-22.35-3.97-32.56.85-11.07,4.26-22.07,9.86-31.77,5.38-9.38,13-17.99,22.02-24.89,8.36-6.39,17.55-11.13,27.31-14.08.71-.21,1.44-.32,2.17-.32Z"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 258.751 258.751">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 254.751 254.751">
<defs>
<style>
.uuid-67f42f03-098b-49dc-a7ed-dc13bd713387 {

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,5 +1,5 @@
<?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="-2 -2 254 254">
<svg id="uuid-0fc887e6-719b-4e51-bef8-e5c269508f6b" data-name="sGHST" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250">
<defs>
<style>
.uuid-a205ebe2-7c8f-434c-9bde-29ac218716be {
@ -84,4 +84,4 @@
</g>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -9,12 +9,12 @@ const classes = {
const StyledMuiChip = styled(MuiChip, {
shouldForwardProp: prop => prop !== "template" && prop !== "strong",
})(({ theme, template, background, strong }) => {
})(({ theme, template, strong }) => {
return {
[`&.${classes.chip}`]: {
height: "21px",
borderRadius: "16px",
backgroundColor: background ? background : template
backgroundColor: template
? template === "purple"
? theme.colors.special["olyZaps"]
: template === "gray"
@ -42,8 +42,8 @@ const StyledMuiChip = styled(MuiChip, {
};
});
const Chip = ({ background, template, strong = false, ...props }) => {
return <StyledMuiChip className={classes.chip} background={background} template={template} strong={strong} {...props} />;
const Chip = ({ template, strong = false, ...props }) => {
return <StyledMuiChip className={classes.chip} template={template} strong={strong} {...props} />;
};
export default Chip;

View File

@ -84,20 +84,22 @@ const Modal = ({
// getModalStyle is not a pure function, we roll the style only on the first render
const [modalStyle] = useState(getModalStyle);
const closeButton = (
<GhostStyledIcon
<IconButton
aria-label="close"
component={CloseIcon}
sx={{ cursor: "pointer" }}
color="inherit"
size="small"
onClick={e => {
if (props.onClose) {
props.onClose(e, "escapeKeyDown");
}
}}
/>
>
<GhostStyledIcon component={CloseIcon} />
</IconButton>
);
//modal must have a close position. Close position and topLeft or topRight cant be used for the same position.
const topRightPos = topRight ? topRight : closeButton;
const topRightPos = closePosition === "right" ? closeButton : topRight;
const topLeftPos = closePosition === "left" ? closeButton : topLeft;
return (
@ -111,7 +113,7 @@ const Modal = ({
{...props}
minHeight={minHeight}
maxWidth={maxWidth}
>
>
<Paper
style={modalStyle}
className={classes.paper}

View File

@ -118,19 +118,19 @@ const NavItem = ({
const match = currentLocation.pathname === to || currentLocation.pathname === `/${to}`;
const linkProps = props.href
? {
href: props.href,
target: "_blank",
className: `external-site-link ${className}`,
}
: {
component: NavLink,
to: to,
className: `button-dapp-menu ${className}`,
};
? {
href: props.href,
target: "_blank",
className: `external-site-link ${className}`,
}
: {
component: NavLink,
to: to,
className: `button-dapp-menu ${className}`,
};
const LinkItem = () => (
<Link {...linkProps} {...props} underline="hover" onClick={(e) => e.stopPropagation()}>
<Link {...linkProps} {...props} underline="hover">
<Box
sx={{ fontFamily: "Ubuntu" }}
display="flex"

View File

@ -64,7 +64,7 @@ const Paper = ({
{(topLeft || headerText || topRight || headerContent) && (
<Grid item className="card-header">
<Box display="flex" justifyContent="space-between">
{topLeft && <Box className="top-left" display="flex" justifyContent="center" alignItems="center">{topLeft}</Box>}
{topLeft && <div className="top-left">{topLeft}</div>}
{headerText && !headerContent && (
<Box display="flex" flexDirection="row" alignItems="center">
<Typography fontSize="24px" className="header-text" fontWeight={700} lineHeight="33px">
@ -83,7 +83,7 @@ const Paper = ({
</Box>
)}
{headerContent}
<Box className="top-right" display="flex" justifyContent="center" alignItems="center">{topRight}</Box>
<div className="top-right">{topRight}</div>
</Box>
{subHeader && <Box display="flex">{subHeader}</Box>}
</Grid>

View File

@ -25,7 +25,7 @@ const LinearProgressBar = (props) => {
{props.target && <Box
sx={{
position: 'absolute',
left: `${Math.min(Math.max(props.target, 0), 100)}%`,
left: `${props.target}%`,
top: props.targetTop || 0,
bottom: props.targetBottom || 0,
width: props.targetWidth || "2px",

View File

@ -34,7 +34,6 @@ const Select = ({
inputWidth,
renderValue = null,
width = "100%",
maxWidth = "100%"
}) => {
const theme = useTheme();
return (
@ -42,7 +41,6 @@ const Select = ({
display="flex"
flexDirection="column"
width={width}
maxWidth={maxWidth}
sx={{ backgroundColor: theme.colors.gray[750] }}
borderRadius="12px"
padding="15px"
@ -61,15 +59,6 @@ const Select = ({
IconComponent={KeyboardArrowDownIcon}
renderValue={renderValue}
displayEmpty
MenuProps={{
PaperProps: {
sx: {
maxHeight: 160,
overflowY: 'auto',
},
},
disableScrollLock: true,
}}
>
{options.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>

View File

@ -1,4 +1,4 @@
import { useMemo, useEffect } from "react";
import React from "react";
import "./Sidebar.scss";
@ -16,7 +16,6 @@ import {
} from "@mui/material";
import { styled } from "@mui/material/styles";
import { NavLink } from "react-router-dom";
import { useSwitchChain } from "wagmi";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import GitHubIcon from '@mui/icons-material/GitHub';
@ -27,7 +26,7 @@ import HubIcon from '@mui/icons-material/Hub';
import PublicIcon from '@mui/icons-material/Public';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import GavelIcon from '@mui/icons-material/Gavel';
import CasinoIcon from '@mui/icons-material/Casino';
import ForumIcon from '@mui/icons-material/Forum';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import BookIcon from '@mui/icons-material/Book';
import CurrencyExchangeIcon from '@mui/icons-material/CurrencyExchange';
@ -40,20 +39,19 @@ import BondIcon from "../Icon/BondIcon";
import StakeIcon from "../Icon/StakeIcon";
import WrapIcon from "../Icon/WrapIcon";
import { isNetworkAvailable } from "../../constants";
import { isNetworkAvailable, isNetworkLegacy, isGovernanceAvailable } from "../../constants";
import { AVAILABLE_DEXES } from "../../constants/dexes";
import { GATEKEEPER_ADDRESSES } from "../../constants/addresses";
import { ECOSYSTEM } from "../../constants/ecosystem";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { sortBondsByDiscount, formatCurrency } from "../../helpers";
import BondDiscount from "../../containers/Bond/components/BondDiscount";
import Chip from "../Chip/Chip";
import DashboardIcon from '@mui/icons-material/Dashboard';
import ShowerIcon from '@mui/icons-material/Shower';
import WifiProtectedSetupIcon from '@mui/icons-material/WifiProtectedSetup';
import { useTokenSymbol } from "../../hooks/tokens";
import { useFtsoPrice, useGhstPrice, useGhostedSupplyPrice } from "../../hooks/prices";
import { useLiveBonds } from "../../hooks/bonds/index";
import pckg from "../../../package.json"
@ -70,22 +68,14 @@ const StyledBox = styled(Box)(({ theme }) => ({
}));
const NavContent = ({ chainId, addressChainId }) => {
const { chains } = useSwitchChain();
const chainName = useMemo(() => {
return chains.find(chain => chain.id === chainId).name.toLowerCase();
}, [chains, chainId, addressChainId])
const { liveBonds: ghostBonds } = useLiveBonds(chainId);
const ftsoPrice = useFtsoPrice(chainId);
const ghstPrice = useGhstPrice(chainId);
const ghostedSupplyPrice = useGhostedSupplyPrice(chainId);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const sortedGhostBonds = useMemo(() => sortBondsByDiscount(ghostBonds), [ghostBonds]);
const bridgeNumbers = useMemo(() => {
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
const number = 1 + connectedNetworks * 3;
return `(${number}, ${number})`;
}, [chainId]);
return (
<Paper className="dapp-sidebar">
<Box className="dapp-sidebar-inner" display="flex" justifyContent="space-between" flexDirection="column">
@ -102,6 +92,17 @@ const NavContent = ({ chainId, addressChainId }) => {
Version {pckg.version}
</Box>
</Link>
<Box display="flex" flexDirection="column" mt="10px">
<Box fontSize="12px" fontWeight="500" lineHeight={"15px"}>
{ftsoSymbol} Price: {formatCurrency(ftsoPrice, 2)}
</Box>
<Box fontSize="12px" fontWeight="500" lineHeight="15px">
{ghstSymbol} Price: {formatCurrency(ghstPrice, 2)}
</Box>
<Box fontSize="12px" fontWeight="500" lineHeight={"15px"}>
GHOSTed Supply: {formatCurrency(ghostedSupplyPrice, 2)}
</Box>
</Box>
</Box>
<Box className="menu-divider">
@ -112,18 +113,58 @@ const NavContent = ({ chainId, addressChainId }) => {
<div className="dapp-nav" id="navbarNav">
{isNetworkAvailable(chainId, addressChainId) &&
<>
<NavItem icon={DashboardIcon} label={`Dashboard`} to={`/${chainName}/dashboard`} />
<NavItem icon={DashboardIcon} label={`Dashboard`} to="/dashboard" />
{isNetworkLegacy(chainId)
? <NavItem icon={ShowerIcon} label={`Faucet`} to="/faucet" />
: <NavItem icon={WifiProtectedSetupIcon} label={`Wrapper`} to="/wrapper" />
}
<NavItem
defaultExpanded
icon={BondIcon}
label={`Bond`}
to="/bonds"
children={
<AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}>
{sortBondsByDiscount(ghostBonds).map((bond, index) => {
return (
<Link
component={NavLink}
to={`/bonds/${bond.id}`}
key={index}
style={{ textDecoration: "none" }}
>
<Box mb="10px" display="flex" justifyContent="end">
<Typography
style={{
width: "180px",
justifyContent: "space-between",
display: "flex",
gap: "10px"
}}
component="span"
variant="body2"
>
{bond.displayName}
<BondDiscount textOnly discount={bond.discount} />
</Typography>
</Box>
</Link>
)
})}
</AccordionDetails>
}
/>
<NavItem
icon={CurrencyExchangeIcon}
label={`(3, 3) Swap`}
to={`/${chainName}/dex/uniswap`}
label={`Dex`}
to={'/dex/uniswap'}
children={
AVAILABLE_DEXES[chainId].length > 1 && <AccordionDetails style={{ margin: "0 0 -10px", display: "flex", flexDirection: "column", gap: "10px" }}>
<AccordionDetails style={{ margin: "0 0 -10px", display: "flex", flexDirection: "column", gap: "10px" }}>
{AVAILABLE_DEXES[chainId].map((dex, index) => {
return (
<Link
component={NavLink}
to={`/${chainName}/dex/${dex.name.toLowerCase()}`}
to={`/dex/${dex.name.toLowerCase()}`}
key={index}
style={{ textDecoration: "none" }}
>
@ -137,54 +178,11 @@ const NavContent = ({ chainId, addressChainId }) => {
</AccordionDetails>
}
/>
<NavItem icon={StakeIcon} label={`(3, 3) Stake`} to={`/${chainName}/stake`} />
<NavItem
defaultExpanded
icon={BondIcon}
label={`(1, 1) Bond`}
to={`/${chainName}/bonds`}
children={
<AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}>
{sortedGhostBonds.length > 0 && <Box width="180px" mb="10px" ml="auto">
<Typography component="span" variant="body2">Bond Discounts</Typography>
</Box>}
{sortedGhostBonds.map((bond, index) => {
return (
<Link
component={NavLink}
to={`/${chainName}/bonds/${bond.id}`}
key={index}
style={{ textDecoration: "none" }}
>
<Box mb="10px" display="flex" justifyContent="end">
<Typography
style={{
width: "180px",
justifyContent: "space-between",
alignItems: "center",
display: "flex",
gap: "10px"
}}
component="span"
variant="body2"
>
{bond.displayName}
{bond.isSoldOut
? <Chip label="Sold Out" template="darkGray" />
: <BondDiscount discount={bond.discount} />
}
</Typography>
</Box>
</Link>
)
})}
</AccordionDetails>
}
/>
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
<NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />
<NavItem icon={StakeIcon} label={`Stake`} to="/stake" />
<NavItem icon={ForkRightIcon} label={`Bridge`} to="/bridge" />
{isGovernanceAvailable(chainId, addressChainId) && <NavItem icon={GavelIcon} label={`Governance`} to="/governance" />}
<Box className="menu-divider">
<Divider />
<Divider />
</Box>
</>
}
@ -193,9 +191,10 @@ const NavContent = ({ chainId, addressChainId }) => {
</div>
<Box>
<NavItem href="https://ghostchain.io/game-theory-3-3" icon={CasinoIcon} label={`${bridgeNumbers} Game Theory`} />
<NavItem href="http://ecosystem.ghostchain.io" icon={PublicIcon} label={`Ecosystem`} />
<NavItem href="https://docs.ghostchain.io/" icon={BookIcon} label={`Documentation`} />
<NavItem href="http://ghostchain.io/builders" icon={ForumIcon} label={`Forum`} />
<NavItem href="https://docs.ghostchain.io/" icon={BookIcon} label={`Docs`} />
<NavItem href="https://git.ghostchain.io/ghostchain/ghost-dao-contracts/issues" icon={ErrorOutlineIcon} label={`Git Issues`} />
<StyledBox display="flex" justifyContent="space-around" paddingY="24px">
<Link href="https://git.ghostchain.io/ghostchain/ghost-dao-contracts" target="_blank" rel="noopener noreferrer">
<GhostStyledIcon viewBox="0 0 24 24" component={GitHubIcon} className={classes.gray} />

View File

@ -44,7 +44,6 @@ const SwapCard = ({
tokenName,
inputWidth,
inputFontSize,
chainTokenName,
maxWidth = "416px",
...props
}) => {
@ -73,26 +72,12 @@ const SwapCard = ({
cursor: tokenOnClick ? "pointer" : "default",
}}
borderRadius="6px"
paddingX="7px"
paddingY="2px"
paddingX="9px"
paddingY="6.5px"
alignItems="center"
{...onClickProps}
>
{typeof token === "string"
? <Token
chainTokenName={chainTokenName}
name={token}
parentSx={{
"& svg:nth-of-type(2)": {
transform: "scale(0.52)",
}
}}
sx={{
transform: "scale(0.65)",
transformOrigin: "center"
}}
/>
: token}
{typeof token === "string" ? <Token name={token} sx={{ fontSize: "21px" }} /> : token}
{(typeof token === "string" || tokenName) && (
<Typography fontSize="15px" lineHeight="24px" marginLeft="9px">

View File

@ -69,28 +69,27 @@ export const parseKnownToken = (name) => {
return icon;
}
const Token = ({ chainTokenName, name, isNative, parentSx, viewBox = "0 0 260 260", fontSize = "large", ...props }) => {
const Token = ({ chainTokenName, name, viewBox = "0 0 260 260", fontSize = "large", ...props }) => {
return (
<Box display="flex" justifyContent="center" alignItems="center" position="relative" sx={{ ...parentSx }} >
<Box display="flex" justifyContent="center" alignItems="center" position="relative">
<StyledSvgIcon
inheritViewBox
fontSize={fontSize}
component={parseKnownToken(name)}
style={{ animation: `spinTokenIcon ${isNative ? 10 : 0}s linear infinite` }}
{...props}
></StyledSvgIcon>
{chainTokenName && (
<StyledSvgIcon
inheritViewBox
component={parseKnownToken(chainTokenName)}
sx={{
style={{
position: "absolute",
bottom: 0,
right: 0,
transform: "translate(15%, 15%) scale(0.8)",
borderRadius: "100%",
zIndex: 10,
marginLeft: "70%",
marginTop: "45%",
width: "65%",
height: "65%",
border: "1px solid #fff",
borderRadius: "100%"
}}
></StyledSvgIcon>
)}

View File

@ -58,7 +58,6 @@ export const TokenAllowanceGuard = ({
height = "auto",
spendAmount,
tokenName,
isNative,
owner,
spender,
decimals,
@ -89,7 +88,7 @@ export const TokenAllowanceGuard = ({
);
}
if (!isNative && allowance && spendAmount && allowance.lt(spendAmount))
if (allowance && spendAmount && allowance.lt(spendAmount))
return (
<Grid container alignItems="center">
<Grid item xs={12} sm={isVertical ? 12 : 8}>

View File

@ -1,12 +1,11 @@
import { Avatar, Box } from "@mui/material";
import Token from "../Token/Token";
const TokenStack = ({ chainTokenNames, tokens, style, images, network, ...props }) => {
const TokenStack = ({ tokens, style, images, network, ...props }) => {
const imageStyles = {
height: "27px",
width: "27px",
...style,
};
return (
<Box display="flex" flexDirection="row" marginTop={network ? "3px" : "0px"} marginLeft={network ? "3px" : "0px"}>
@ -21,25 +20,18 @@ const TokenStack = ({ chainTokenNames, tokens, style, images, network, ...props
<Avatar
src={image}
style={{
...(i !== 0 ? { marginLeft: -7, zIndex: 100 - i, ...imageStyles } : { zIndex: 100 - i, ...imageStyles }),
...(i !== 0 ? { marginLeft: -7, zIndex: 1, ...imageStyles } : { zIndex: 1, ...imageStyles }),
}}
/>
))}
{tokens?.map((token, i) => (
<Token
{...props}
key={i}
name={token}
style={{
...(i !== 0 ? { marginLeft: -12, zIndex: 100 - i, ...style } : { zIndex: 100 - i, ...style }),
...(i !== 0 ? { marginLeft: -12, zIndex: 1, ...style } : { zIndex: 2, ...style }),
}}
parentSx={{
"& svg:nth-of-type(2)": {
transform: "translate(22%, 22%) scale(0.6)",
zIndex: 100,
}
}}
chainTokenName={chainTokenNames?.at(i)}
{...props}
/>
))}
</Box>

View File

@ -1,58 +0,0 @@
import { Box, Typography, SvgIcon, useTheme } from "@mui/material";
import { parseKnownToken } from "../../components/Token/Token";
import { useUnstableProvider } from "../../hooks/ghost";
import { PrimaryButton } from "../Button"
import { GHOST_CONNECT } from "../../constants/ecosystem";
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;

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react";
import { useState, useEffect } from "react";
import { Box, Typography, SvgIcon, useTheme } from "@mui/material";
@ -12,62 +12,28 @@ import { isNetworkAvailable } from "../../constants";
import { parseKnownToken } from "../../components/Token/Token";
import { useSwitchChain } from 'wagmi';
import { useNavigate, useLocation } from 'react-router-dom';
import toast from "react-hot-toast";
function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, small, status }) {
function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, small }) {
const theme = useTheme();
const navigate = useNavigate();
const hasAttemptedSwitch = useRef(false);
const { chains, switchChain } = useSwitchChain();
const { pathname } = useLocation();
const [networkId, setNetworkId] = useState(chainId);
useEffect(() => {
if (status === 'reconnecting' || status === 'connecting') return;
if (chainId !== networkId) setNetworkId(chainId);
}, [chainId])
const parts = pathname.split('/');
if (!hasAttemptedSwitch.current && parts.length > 2) {
let targetChain = chains.at(0);
const chainName = parts[1].toLowerCase();
const chain = chains.find(chain => chain.name.toLowerCase() === chainName);
if (chain && targetChain.name !== chain.name) {
targetChain = chain;
}
hasAttemptedSwitch.current = true;
changeNetworkId(targetChain.name, targetChain.id);
}
}, [status, chains, pathname]);
const changeNetworkId = (chainName, newNetworkId) => {
const handleChange = (event) => {
const chainName = chains.find((chain) => chain.id === event.target.value).name;
toast.dismiss(wrongNetworkToastId);
const toastId = toast.loading(`Trying to connect to ${chainName} network... Wait please`, {
position: 'bottom-right'
});
setWrongNetworkToastId(toastId);
setNetworkId(newNetworkId);
switchChain({
chainId: newNetworkId
}, {
onSuccess: (data) => console.log("Network switched to", data.name),
onError: (error) => console.log("Error during network switching", error),
});
}
const handleChange = (event) => {
const chainName = chains.find((chain) => chain.id === event.target.value).name;
changeNetworkId(chainName, event.target.value);
const parts = pathname.split('/');
if (parts.length > 2) {
parts[1] = chainName.toLowerCase();
const newPath = parts.join("/");
navigate(newPath, { replace: true });
}
setNetworkId(event.target.value);
switchChain({ chainId: event.target.value });
}
return(
@ -76,7 +42,6 @@ function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, s
labelId="network-select-helper-label"
id="network-select-helper"
value={networkId}
disabled={!hasAttemptedSwitch.current}
onChange={handleChange}
sx={{
boxShadow: 'none',
@ -92,7 +57,7 @@ function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, s
return (
<MenuItem key={chain.name} value={chain.id}>
<Box gap="10px" display="flex" flexDirection="row" alignItems="center">
<SvgIcon sx={{ width: 22 }} component={parseKnownToken(chain?.nativeCurrency?.symbol)} inheritViewBox />
<SvgIcon component={parseKnownToken(chain?.nativeCurrency?.symbol)} inheritViewBox />
{!small && <Typography>{chain.name}</Typography>}
</Box>
</MenuItem>

View File

@ -3,6 +3,7 @@ import MenuIcon from "../../assets/icons/hamburger.svg?react";
import Wallet from "./Wallet"
import SelectNetwork from "./SelectNetwork";
const PREFIX = "TopBar";
const classes = {
@ -17,7 +18,6 @@ function TopBar({
address,
wrongNetworkToastId,
connect,
status,
handleDrawerToggle,
setWrongNetworkToastId
}) {
@ -33,19 +33,12 @@ function TopBar({
marginRight={desktop ? "33px" : "0px"}
>
<Box display="flex" alignItems="center">
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
width={small ? "calc(100vw - 78px)" : "320px"}
height="40px"
>
<Box display="flex" justifyContent="space-between" alignItems="center" width={small ? "calc(100vw - 78px)" : "320px"}>
<SelectNetwork
wrongNetworkToastId={wrongNetworkToastId}
setWrongNetworkToastId={setWrongNetworkToastId}
chainId={chainId}
small={small}
status={status}
/>
<Wallet address={address} connect={connect} chainId={chainId} />
</Box>

View File

@ -1,4 +1,3 @@
import { useMemo } from "react";
import {
Box,
Button,
@ -13,21 +12,19 @@ import {
import { styled } from '@mui/material/styles';
import CloseIcon from '@mui/icons-material/Close';
import LoopIcon from '@mui/icons-material/Loop';
import ReactGA from "react-ga4";
import { ReactElement, useState, useEffect } from "react";
import { useNavigate, useLocation, createSearchParams } from "react-router-dom";
import { useNavigate, createSearchParams } from "react-router-dom";
import GhostStyledIcon from "../../Icon/GhostIcon";
import { Tokens, NativeToken, useWallet } from "./Token";
import { Tokens, useWallet } from "./Token";
import { PrimaryButton, SecondaryButton } from "../../Button";
import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
import { formatCurrency, shorten } from "../../../helpers";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { RESERVE_ADDRESSES, FTSO_ADDRESSES } from "../../../constants/addresses";
import { useUnstableProvider, useSystemAccount, useChainSpec } from "../../../hooks/ghost"
import { useAccount, useDisconnect, useSwitchChain } from "wagmi";
import { useAccount, useDisconnect } from "wagmi";
const DisconnectButton = ({ onClose }) => {
const { disconnect } = useDisconnect();
@ -53,12 +50,12 @@ const CloseButton = styled(IconButton)(theme => ({
},
}));
const WalletTotalValue = ({ chainName, address, tokens, currency, setCurrency }) => {
const ghstPrice = tokens?.ghst ? tokens.ghst.price : tokens?.price;
const tokensObject = tokens?.ghst ? tokens : { tokens };
const WalletTotalValue = ({ address, tokens }) => {
const ghstPrice = tokens.ghst.price;
const [currency, setCurrency] = useState("USD");
const [topHovered, setTopHovered] = useState(false);
const walletTotalValueUSD = Object.values(tokensObject).reduce(
const walletTotalValueUSD = Object.values(tokens).reduce(
(totalValue, token) => totalValue.add(token.balance.mul(token.price)),
new DecimalBigNumber(0n, 0),
);
@ -78,16 +75,16 @@ const WalletTotalValue = ({ chainName, address, tokens, currency, setCurrency })
onMouseEnter={() => setTopHovered(true)}
onMouseLeave={() => setTopHovered(false)}
>
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Box width="120px" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography style={{ lineHeight: 1.1, fontWeight: 600, fontSize: "0.975rem" }} color="textSecondary">
{chainName?.toUpperCase()}
MY WALLET
</Typography>
<Typography style={{ fontSize: ".85rem", lineHeight: 1.1 }} variant="h3">
{shorten(address)}
</Typography>
</Box>
{tokens?.ghst && <Box pt="1px">
<Box pt="1px">
<GhostStyledIcon
viewBox="0 0 24 24"
component={LoopIcon}
@ -97,11 +94,12 @@ const WalletTotalValue = ({ chainName, address, tokens, currency, setCurrency })
transform: topHovered ? "rotateZ(-180deg)" : "rotateZ(0deg)",
}}
/>
</Box>}
</Box>
</Box>
<Typography style={{ fontWeight: 700 }} variant="h3">
{formatCurrency(walletValue[currency], currency === "USD" ? 2 : 5, currency === "USD" ? "USD" : "👻" )}
{formatCurrency(walletValue[currency], 2, currency === "USD" ? "USD" : "👻" )}
</Typography>
</Box>
);
@ -110,42 +108,11 @@ const WalletTotalValue = ({ chainName, address, tokens, currency, setCurrency })
function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
const theme = useTheme();
const navigate = useNavigate();
const location = useLocation();
const tokens = useWallet(chainId, address);
const [currency, setCurrency] = useState("USD");
const { account, isExtensionMissing } = useUnstableProvider();
const { chains } = useSwitchChain();
const chainName = useMemo(() => {
const chain = chains.find(chain => chain.id === chainId)
if (!chain) return null
return chain.name.toLowerCase()
}, [chains, chainId])
const chainSpec = useChainSpec();
const accountBalanceRaw = useSystemAccount({ account });
const accountBalance = useMemo(() => {
const decimals = chainSpec ? chainSpec.properties.tokenDecimals : 18;
if (!accountBalanceRaw) return new DecimalBigNumber(0n, decimals);
const balance = accountBalanceRaw.data.free
+ accountBalanceRaw.data.frozen
+ accountBalanceRaw.data.reserved;
return new DecimalBigNumber(balance, decimals);
}, [accountBalanceRaw, chainSpec])
const nativeGhst = useMemo(() => {
return {
...tokens.ghst,
balance: accountBalance,
}
}, [tokens, accountBalance])
const onBtnClick = (dexName, from, to) => {
navigate({
pathname: `${chains.find(chain => chain.id === chainId).name.toLowerCase()}/dex/` + dexName,
pathname: "/dex/" + dexName,
search: createSearchParams({
pool: true,
from: from,
@ -155,12 +122,6 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
onClose();
}
useEffect(() => {
if (isWalletOpen && chainName) {
ReactGA.send({ hitType: "pageview", page: `/${chainName.toLowerCase()}/sidebar` });
}
}, [isWalletOpen, chainName])
useEffect(() => {
if (isWalletOpen) {
Object.values(tokens ?? {}).forEach(token => token.refetch());
@ -171,13 +132,7 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
<Paper>
<Box sx={{ padding: theme.spacing(0, 3), display: "flex", flexDirection: "column", minHeight: "100vh" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", padding: theme.spacing(3, 0, 1) }}>
<WalletTotalValue
currency={currency}
setCurrency={setCurrency}
chainName={chainName}
address={address}
tokens={tokens}
/>
<WalletTotalValue address={address} tokens={tokens} />
<CloseButton size="small" onClick={onClose} aria-label="close wallet">
<GhostStyledIcon component={CloseIcon} />
</CloseButton>
@ -187,7 +142,7 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
<Divider />
</Box>
<Box sx={{ display: "flex", flexDirection: "column" }} >
<Box sx={{ display: "flex", flexDirection: "column" }} style={{ gap: theme.spacing(1) }}>
<Tokens address={address} tokens={tokens} onClose={onClose} />
</Box>
@ -195,45 +150,17 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
<Divider />
</Box>
{!isExtensionMissing
? <>
<Box sx={{ display: "flex", flexDirection: "column" }} style={{ gap: theme.spacing(2, 1) }}>
<WalletTotalValue
currency={currency}
setCurrency={setCurrency}
chainName={chainSpec?.chainName ?? ""}
address={account ?? "...wait"}
tokens={nativeGhst}
/>
<NativeToken {...nativeGhst} />
</Box>
<Box sx={{ margin: theme.spacing(2, -3, 3) }}>
<Divider />
</Box>
</>
: <Box
sx={{ borderRadius: "15px", background: theme.colors.paper.background }}
width="100%"
height="100%"
padding="20px 30px"
display="flex"
flexDirection="column"
justifyContent="space-around"
alignItems="center"
gap="10px"
<Box
sx={{ display: "flex", flexDirection: "column" }}
style={{ gap: theme.spacing(1.5) }}
>
<SecondaryButton
fullWidth
onClick={() => onBtnClick("uniswap", RESERVE_ADDRESSES[chainId], FTSO_ADDRESSES[chainId])}
>
<Typography sx={{ textAlign: "center" }} variant="subtitle1">GHOST Connect is not detected!</Typography>
<Typography sx={{ textAlign: "center" }} variant="body2">Download GHOST Connect for bounties, validator stats, and (10, 10) {"Stake\u00B2"} rewards.</Typography>
<PrimaryButton
fullWidth
onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}
>
Get GHOST Connect
</PrimaryButton>
</Box>
}
<Typography>{`${tokens?.ftso?.symbol}-${tokens?.reserve?.symbol} on Uniswap`}</Typography>
</SecondaryButton>
</Box>
<Box sx={{ width: "100%", marginTop: "auto", marginX: "auto", padding: theme.spacing(2, 0) }}>
<DisconnectButton onClose={onClose} />

View File

@ -9,17 +9,16 @@ import {
useTheme,
} from "@mui/material";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { ChangeEvent, useState, useMemo, useEffect } from "react";
import { ChangeEvent, useState, useEffect } from "react";
import { useNavigate, createSearchParams } from "react-router-dom";
import { useQuery } from "react-query";
import { formatCurrency, formatNumber } from "../../../helpers";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber"
import { tokenNameConverter } from "../../../helpers/tokenConverter";
import { EMPTY_ADDRESS } from "../../../constants/addresses";
import { isNetworkLegacy } from "../../../constants";
import GhostStyledIcon from "../../Icon/GhostIcon";
import TokenStack from "../../TokenStack/TokenStack";
import SingleToken from "../../Token/Token";
import { PrimaryButton, SecondaryButton } from "../../Button";
import { useBalance, useTokenSymbol } from "../../../hooks/tokens";
@ -27,12 +26,11 @@ import {
useNativePrice,
useReservePrice,
useFtsoPrice,
useStnkPrice,
useGhstPrice
} from "../../../hooks/prices";
import { useLpValuation } from "../../../hooks/treasury";
import { useUniswapV2PairReserves } from "../../../hooks/uniswapv2";
import { getTokenIcons } from "../../../hooks/helpers";
import { useAccount, useBalance as useNativeBalance, useConfig, useSwitchChain, useChainId } from "wagmi";
import { useAccount, useBalance as useNativeBalance, useConfig } from "wagmi";
const addTokenToWallet = async (token, userAddress) => {
if (!window.ethereum) return;
@ -69,65 +67,13 @@ const BalanceValue = ({
</Box>
);
export const NativeToken = (props) => {
const theme = useTheme();
const navigate = useNavigate();
const chainId = useChainId();
const { chains } = useSwitchChain();
const chainName = useMemo(() => {
return chains.find(chain => chain.id === chainId).name.toLowerCase();
}, [chains, chainId])
return (
<Accordion expanded={props.expanded} onChange={props.onChangeExpanded}>
<AccordionSummary expandIcon={<GhostStyledIcon component={ExpandMoreIcon} color="disabled" />} >
<Box sx={{ display: "flex", justifyContent: "space-between", width: "100%", marginRight: "10px" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<SingleToken
sx={{
width: "30px",
height: "30px",
}}
name="GHST"
isNative
/>
<Typography>{props.symbol}</Typography>
</Box>
<BalanceValue
balance={props.balance}
balanceValueUSD={props.balance.mul(props.price)}
/>
</Box>
</AccordionSummary>
<AccordionDetails style={{ margin: "auto", padding: theme.spacing(1, 0) }}>
<Box
sx={{ display: "flex", flexDirection: "column", flex: 1, mx: "32px", justifyContent: "center" }}
style={{ gap: theme.spacing(1) }}
>
<Box display="flex" flexDirection="column" className="ghst-pairs" style={{ width: "100%" }}>
<PrimaryButton
onClick={() =>{ navigate({ pathname: `${chainName}/bridge` }); onClose(); }}
fullWidth
>
<Typography>Get (10, 10) Stake</Typography>
</PrimaryButton>
</Box>
</Box>
</AccordionDetails>
</Accordion>
);
}
export const Token = (props) => {
const {
isNative,
symbol,
faucetPath,
icons,
address,
price,
price = 0,
balance,
onAddTokenToWallet,
expanded,
@ -138,20 +84,13 @@ export const Token = (props) => {
} = props;
const theme = useTheme();
const navigate = useNavigate();
const chainId = useChainId();
const { chains } = useSwitchChain();
const chainName = useMemo(() => {
return chains.find(chain => chain.id === chainId).name.toLowerCase();
}, [chains, chainId])
const useLink = (symbol, fromAddress, toAddress, isPool) => {
if (faucetPath) {
navigate({ pathname: faucetPath })
if (symbol.toUpperCase() === "RESERVE") {
navigate({ pathname: "/faucet" })
} else {
navigate({
pathname: `${chainName}/dex/uniswap`,
pathname: "/dex/uniswap",
search: isPool ?
createSearchParams({
pool: "true",
@ -182,13 +121,12 @@ export const Token = (props) => {
height: "28px",
}}
tokens={props.icons}
chainTokenNames={props.chainTokenNames}
/>
<Typography>{symbol}</Typography>
</Box>
<BalanceValue
balance={balance}
balanceValueUSD={balance?.mul(price)}
balanceValueUSD={balance.mul(price)}
/>
</Box>
</AccordionSummary>
@ -208,7 +146,7 @@ export const Token = (props) => {
onClick={() => useLink(symbol, reserveAddress, address, isPool)}
fullWidth
>
<Typography>Get {faucetPath ? "for Free" : "on DEX"}</Typography>
<Typography>Get on {symbol?.toUpperCase() === "RESERVE" ? "Faucet" : "Uniswap"}</Typography>
</SecondaryButton>
</Box>
</Box>
@ -236,6 +174,11 @@ export const useWallet = (chainId, userAddress) => {
refetch: ftsoRefetch,
contractAddress: ftsoAddress,
} = useBalance(chainId, "FTSO", userAddress);
const {
balance: stnkBalance,
refetch: stnkRefetch,
contractAddress: stnkAddress,
} = useBalance(chainId, "STNK", userAddress);
const {
balance: ghstBalance,
refetch: ghstRefetch,
@ -246,57 +189,40 @@ export const useWallet = (chainId, userAddress) => {
refetch: lpReserveFtsoRefetch,
contractAddress: lpReserveFtsoBalanceAddress,
} = useBalance(chainId, "RESERVE_FTSO", userAddress);
const {
tokens: lpReserveFtsoTokens,
} = useUniswapV2PairReserves(chainId, "RESERVE_FTSO");
const nativePrice = useNativePrice(chainId);
const reservePrice = useReservePrice(chainId);
const ftsoPrice = useFtsoPrice(chainId);
const stnkPrice = useStnkPrice(chainId);
const ghstPrice = useGhstPrice(chainId);
const lpReserveFtsoPrice = useLpValuation(chainId, "RESERVE_FTSO", 1000000000000000000n);
const config = useConfig();
const nativeSymbol = useMemo(() => {
return config?.getClient()?.chain?.nativeCurrency?.symbol;
}, [chainId, config]);
const nativeSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { symbol: lpReserveFtsoSymbol } = useTokenSymbol(chainId, "RESERVE_FTSO");
const lpReserveFtsoTokenNames = useMemo(() => {
const token0 = getTokenIcons(chainId, lpReserveFtsoTokens?.token0 ?? []);
const token1 = getTokenIcons(chainId, lpReserveFtsoTokens?.token1 ?? []);
let tokenAddresses = [lpReserveFtsoTokens?.token1, lpReserveFtsoTokens?.token0];
let tokenNames = [...token1, ...token0];
if (token0?.at(0) === reserveSymbol) {
tokenAddresses = [lpReserveFtsoTokens?.token0, lpReserveFtsoTokens?.token1];
tokenNames = [...token0, ...token1];
}
return { tokenAddresses, tokenNames }
}, [chainId, reserveSymbol, lpReserveFtsoTokens]);
const tokens = {
native: {
symbol: nativeSymbol,
icons: [nativeSymbol],
balance: nativeBalance,
price: nativePrice,
refetch: nativeBalanceRefetch,
refetch: nativeBalanceRefetch
},
reserve: {
symbol: reserveSymbol,
address: reserveAddress,
balance: reserveBalance,
price: reservePrice,
icons: [tokenNameConverter(chainId, reserveSymbol)],
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/11/6A-Classic-ETC-Token.svg",
icons: isNetworkLegacy(chainId) ? ["GDAI"] : [tokenNameConverter(chainId, reserveSymbol)],
externalUrl: isNetworkLegacy(chainId)
? "https://ghostchain.io/wp-content/uploads/2025/03/gDAI.svg"
: "https://ghostchain.io/wp-content/uploads/2025/11/6A-Classic-ETC-Token.svg",
refetch: reserveRefetch,
},
ftso: {
@ -304,32 +230,35 @@ export const useWallet = (chainId, userAddress) => {
address: ftsoAddress,
balance: ftsoBalance,
price: ftsoPrice,
chainTokenNames: [nativeSymbol],
icons: ["FTSO"],
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/eGHST.svg",
refetch: ftsoRefetch,
},
stnk: {
symbol: stnkSymbol,
address: stnkAddress,
balance: stnkBalance,
price: stnkPrice,
icons: ["STNK"],
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/sGHST.svg",
refetch: stnkRefetch,
},
ghst: {
symbol: ghstSymbol,
address: ghstAddress,
balance: ghstBalance,
price: ghstPrice,
icons: ["GHST"],
chainTokenNames: [nativeSymbol],
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/GHST.svg",
refetch: ghstRefetch,
},
reserveFtso: {
isPool: true,
symbol: lpReserveFtsoSymbol,
address: lpReserveFtsoTokenNames?.tokenAddresses.at(1) ?? "",
address: lpReserveFtsoBalanceAddress,
balance: lpReserveFtsoBalance,
price: lpReserveFtsoPrice,
icons: lpReserveFtsoTokenNames?.tokenNames,
chainTokenNames: [
lpReserveFtsoTokenNames?.tokenNames?.at(0) !== reserveSymbol && nativeSymbol,
lpReserveFtsoTokenNames?.tokenNames?.at(1) !== reserveSymbol && nativeSymbol,
],
icons: ["FTSO", isNetworkLegacy(chainId) ? "GDAI" : tokenNameConverter(chainId, reserveSymbol)],
externalUrl: "https://ghostchain.io/wp-content/uploads/2025/03/uni-v2.svg",
refetch: lpReserveFtsoRefetch,
}
@ -346,14 +275,14 @@ export const useWallet = (chainId, userAddress) => {
}, {});
};
export const Tokens = ({ address, tokens, isNative, onClose }) => {
export const Tokens = ({ address, tokens, onClose }) => {
const [expanded, setExpanded] = useState(null);
const alwaysShowTokens = [tokens.native, tokens.reserve, tokens.ftso, tokens.ghst, tokens.reserveFtso];
const alwaysShowTokens = [tokens.native, tokens.reserve, tokens.ftso, tokens.stnk, tokens.ghst, tokens.reserveFtso];
const tokenProps = (token) => ({
...token,
expanded: expanded === token.symbol,
reserveAddress: EMPTY_ADDRESS,
reserveAddress: tokens.reserve.address,
onChangeExpanded: (e, isExpanded) => setExpanded(isExpanded ? token.symbol : null),
onAddTokenToWallet: () => addTokenToWallet(token, address),
onClose: () => onClose(),
@ -362,7 +291,7 @@ export const Tokens = ({ address, tokens, isNative, onClose }) => {
return (
<>
{alwaysShowTokens.map((token, i) => (
<Token key={i} isNative={i == 0} {...tokenProps(token)} />
<Token key={i} isNative={i === 0} {...tokenProps(token)} />
))}
</>
);

View File

@ -11,7 +11,7 @@ const WalletButton = ({ openWallet, connect }) => {
const { isConnected, chain } = useAccount();
const theme = useTheme();
const onClick = isConnected ? openWallet : connect;
const label = `${isConnected ? "Open" : "Connect"} Wallet`;
const label = isConnected ? "Open Wallet" : `Connect Wallet`;
return (
<Button
id="fatso-menu-button"
@ -76,12 +76,7 @@ export function Wallet({ address, chainId, connect }) {
onOpen={openWallet}
onClose={closeWallet}
>
<InitialWalletView
isWalletOpen={isWalletOpen}
address={address}
chainId={chainId}
onClose={closeWallet}
/>
<InitialWalletView isWalletOpen={isWalletOpen} address={address} chainId={chainId} onClose={closeWallet} />
</StyledSwipeableDrawer>
</>
);

View File

@ -4,6 +4,19 @@ export enum NetworkId {
TESTNET_MORDOR = 63,
}
export const isGovernanceAvailable = (chainId, addressChainId) => {
chainId = addressChainId ? addressChainId : chainId;
let exists = false;
switch (chainId) {
case 11155111:
exists = true
break;
default:
break;
}
return exists;
}
export const isNetworkAvailable = (chainId, addressChainId) => {
chainId = addressChainId ? addressChainId : chainId;
let exists = false;
@ -24,30 +37,15 @@ export const isNetworkAvailable = (chainId, addressChainId) => {
}
export const isNetworkLegacy = (chainId) => {
let isLegacy = false;
let exists = false;
switch (chainId) {
case 11155111:
isLegacy = true
break;
case 63:
isLegacy = true
case 560048:
exists = true
break;
default:
break;
}
return isLegacy;
}
export const isNetworkLegacyType = (chainId) => {
let isLegacyType = false;
switch (chainId) {
case 63:
isLegacyType = true;
break;
default:
break;
}
return isLegacyType
return exists;
}
export const networkAvgBlockSpeed = (chainId) => {

View File

@ -1,40 +1,38 @@
import { NetworkId } from "../constants";
export const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000";
export const STAKING_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
[NetworkId.TESTNET_HOODI]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
[NetworkId.TESTNET_MORDOR]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
[NetworkId.TESTNET_HOODI]: "0x25F62eDc6C89FF84E957C22336A35d2dfc861a86",
[NetworkId.TESTNET_MORDOR]: "0xC25C9C56a89ebd6ef291b415d00ACfa7913c55e7",
};
export const BOND_DEPOSITORY_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
[NetworkId.TESTNET_HOODI]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
[NetworkId.TESTNET_MORDOR]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
[NetworkId.TESTNET_HOODI]: "0x6Ad50B1E293E68B2fC230c576220a93A9D311571",
[NetworkId.TESTNET_MORDOR]: "0x7C85cDEddBAd0f50453d373F7332BEa11ECa7BAf",
};
export const DAO_TREASURY_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",
[NetworkId.TESTNET_HOODI]: "0x05D797f9F34844594C956da58f1785997397f02E",
[NetworkId.TESTNET_MORDOR]: "0x05D797f9F34844594C956da58f1785997397f02E",
[NetworkId.TESTNET_HOODI]: "0x1a1b29b18f714fac9dDabEf530dFc4f85b56A6e8",
[NetworkId.TESTNET_MORDOR]: "0x5883C8e2259556B534036c7fDF4555E09dE9f243",
};
export const FTSO_DAI_LP_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D",
[NetworkId.TESTNET_HOODI]: "0x32388605b5E83Ea79CDdC479AA9939DBCF98f59D",
[NetworkId.TESTNET_MORDOR]: "0x53B13C4722081c405ce25c7A7629fC326A49a469",
[NetworkId.TESTNET_HOODI]: "0xf7B2d44209E70782d93A70F7D8eC50010dF7ae50",
[NetworkId.TESTNET_MORDOR]: "0xE6546D12665dB5B22Cb92FB9e0221aE51A57aeaa",
};
export const FTSO_STNK_LP_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x0000000000000000000000000000000000000000",
[NetworkId.TESTNET_SEPOLIA]: "0x0000000000000000000000000000000000000000", // TBD
[NetworkId.TESTNET_SEPOLIA]: "0x0000000000000000000000000000000000000000",
[NetworkId.TESTNET_MORDOR]: "0x0000000000000000000000000000000000000000",
}
export const RESERVE_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
[NetworkId.TESTNET_HOODI]: "0xE69a5c6dd88cA798b93c3C92fc50c51Fd5305eB4",
[NetworkId.TESTNET_HOODI]: "0x80c6676c334BCcE60b3CC852085B72143379CE58",
[NetworkId.TESTNET_MORDOR]: "0x6af91B3763b5d020E0985f85555EB50e5852d7AC",
};
@ -46,44 +44,41 @@ export const WETH_ADDRESSES = {
export const GHST_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
[NetworkId.TESTNET_HOODI]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
[NetworkId.TESTNET_MORDOR]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
[NetworkId.TESTNET_HOODI]: "0xE98f7426457E6533B206e91B7EcA97aa8A258B46",
[NetworkId.TESTNET_MORDOR]: "0x14b5787F8a1E62786F50A7998A9b14aa24298423",
};
export const STNK_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
[NetworkId.TESTNET_HOODI]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
[NetworkId.TESTNET_MORDOR]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
[NetworkId.TESTNET_HOODI]: "0xF07e9303A9f16Afd82f4f57Fd6fca68Aa0AB6D7F",
[NetworkId.TESTNET_MORDOR]: "0x137bA9403885D8ECEa95AaFBb8734F5a16121bAC",
};
export const FTSO_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
[NetworkId.TESTNET_HOODI]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
[NetworkId.TESTNET_MORDOR]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
[NetworkId.TESTNET_HOODI]: "0xb184e423811b644A1924334E63985c259F5D0033",
[NetworkId.TESTNET_MORDOR]: "0xeA170CC0faceC531a6a9e93a28C4330Ac50343a1",
};
export const DISTRIBUTOR_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
[NetworkId.TESTNET_HOODI]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
[NetworkId.TESTNET_MORDOR]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
[NetworkId.TESTNET_HOODI]: "0xdF49dC81c457c6f92e26cf6d686C7a8715255842",
[NetworkId.TESTNET_MORDOR]: "0xaf5e76706520db7fb01096E322940206bf3fce57",
};
export const GHOST_GOVERNANCE_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xaf7Ad1b83C47405BB9aa96868bCFbb6D65e4C2a1",
[NetworkId.TESTNET_HOODI]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
[NetworkId.TESTNET_MORDOR]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
[NetworkId.TESTNET_SEPOLIA]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
};
export const BONDING_CALCULATOR_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
[NetworkId.TESTNET_HOODI]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
[NetworkId.TESTNET_MORDOR]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
[NetworkId.TESTNET_HOODI]: "0x2635d526Ad24b98082563937f7b996075052c6Fd",
[NetworkId.TESTNET_MORDOR]: "0x0c4C7C49a173E2a3f9Eed93125F3F146D8e17bCb",
}
export const GATEKEEPER_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xd735cA07984a16911222c08411A80e24EB38869B",
[NetworkId.TESTNET_HOODI]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
[NetworkId.TESTNET_MORDOR]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
[NetworkId.TESTNET_SEPOLIA]: "0xc85129A097773B7F8970a7364c928C05f265E6A1",
[NetworkId.TESTNET_MORDOR]: "0xA59cB4ff90bE2206121aE61eEB68d0AeC7BA095f",
}
export const UNISWAP_V2_ROUTER = {
@ -118,10 +113,6 @@ export const CEX_TICKERS = {
"https://api.binance.com/api/v3/ticker/price?symbol=ETHUSDT",
"https://api.coinbase.com/v2/prices/ETH-USDT/spot",
],
[NetworkId.TESTNET_HOODI]: [
"https://api.binance.com/api/v3/ticker/price?symbol=ETHUSDT",
"https://api.coinbase.com/v2/prices/ETH-USDT/spot",
],
[NetworkId.TESTNET_MORDOR]: [
"https://api.binance.com/api/v3/ticker/price?symbol=ETCUSDT",
"https://api.coinbase.com/v2/prices/ETC-USDT/spot",

View File

@ -9,7 +9,6 @@ export const AVAILABLE_DEXES = {
name: "Uniswap",
icon: UniswapIcon,
viewBox: "0 0 195 230",
isFork: true,
},
],
[NetworkId.TESTNET_HOODI]: [
@ -17,7 +16,6 @@ export const AVAILABLE_DEXES = {
name: "Uniswap",
icon: UniswapIcon,
viewBox: "0 0 195 230",
isFork: true,
},
],
[NetworkId.TESTNET_MORDOR]: [
@ -25,7 +23,6 @@ export const AVAILABLE_DEXES = {
name: "Uniswap",
icon: UniswapIcon,
viewBox: "0 0 195 230",
isFork: true,
},
],
};

View File

@ -1,7 +1,5 @@
import { NetworkId } from "../constants";
export const GHOST_CONNECT = "https://connect.ghostchain.io/";
export const ECOSYSTEM = [
{
name: "GHOST chain",

View File

@ -1,30 +0,0 @@
import { NetworkId } from "../constants";
export const amountInHistory = {
[NetworkId.TESTNET_SEPOLIA]: 0n,
[NetworkId.TESTNET_HOODI]: 0n,
[NetworkId.TESTNET_MORDOR]: 0n,
}
export const amountOutHistory = {
[NetworkId.TESTNET_SEPOLIA]: 0n,
[NetworkId.TESTNET_HOODI]: 0n,
[NetworkId.TESTNET_MORDOR]: 0n,
}
// denominator is 1e5
export const feeIn = {
[NetworkId.TESTNET_SEPOLIA]: 6900n,
[NetworkId.TESTNET_HOODI]: 6900n,
[NetworkId.TESTNET_MORDOR]: 6900n,
}
// denominator is 1e5
export const feeOut = {
[NetworkId.TESTNET_SEPOLIA]: 0n,
[NetworkId.TESTNET_HOODI]: 0n,
[NetworkId.TESTNET_MORDOR]: 0n,
}
// denominator is 1e5
export const stakeRatio = 5000n;

View File

@ -1,9 +1,8 @@
import { ArrowBack } from "@mui/icons-material";
import { Box, Link, Skeleton, Typography } from "@mui/material";
import { useEffect, useState, useMemo, useCallback } from "react";
import { Link as RouterLink, useLocation, useParams, useNavigate } from "react-router-dom";
import { useAccount, useSwitchChain } from "wagmi";
import { isAddress } from "viem";
import { useEffect, useState } from "react";
import { Link as RouterLink, useLocation, useNavigate, useParams } from "react-router-dom";
import { useAccount, useChainId } from "wagmi";
import ReactGA from "react-ga4";
import PageTitle from "../../components/PageTitle/PageTitle";
@ -18,93 +17,64 @@ import BondSettingsModal from "./components/BondSettingsModal";
import NotFound from "../NotFound/NotFound";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { isNetworkLegacy } from "../../constants";
import { NetworkId } from "../../constants";
import { formatCurrency } from "../../helpers";
import { useLocalStorage } from "../../hooks/localstorage";
import { useLiveBonds } from "../../hooks/bonds";
import { useFtsoPrice } from "../../hooks/prices";
const BondModalContainer = ({ chainId, address, config, connect }) => {
const { id, network } = useParams();
const { liveBonds } = useLiveBonds(chainId, network);
const BondModalContainer = ({ chainId, address, connect }) => {
const { id } = useParams();
const { liveBonds } = useLiveBonds(chainId);
const bond = liveBonds.find(bond => bond.id === Number(id));
if (!bond) return <NotFound />;
return <BondModal config={config} chainId={chainId} bond={bond} address={address} connect={connect} />;
return <BondModal chainId={chainId} bond={bond} address={address} connect={connect} />;
};
export const BondModal = ({ bond, chainId, address, config, connect }) => {
export const BondModal = ({ bond, chainId, address, connect }) => {
const navigate = useNavigate();
const { network } = useParams();
const { pathname } = useLocation();
const [slippage, setSlippage] = useState(localStorage.getItem("bond-slippage") || "10");
const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("bond-decimals") || "5");
const [isSettingsOpen, setSettingsOpen] = useState(false);
const { getStorageValue, setStorageValue } = useLocalStorage();
const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "bond-slippage", "10"));
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "bond-decimals", "5"));
const [recipientAddress, setRecipientAddressInner] = useState(() => getStorageValue(chainId, address, "bond-recipient", address));
const [recipientAddress, setRecipientAddress] = useState(address);
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: pathname });
}, [pathname])
}, [])
const setRecipientAddress = useCallback((value) => {
setRecipientAddressInner(value);
if (isAddress(value)) {
setStorageValue(chainId, address, "bond-recipient", value);
}
}, [chainId, address]);
const setSlippageInner = useCallback((value) => {
const setSlippageInner = (value) => {
const maybeValue = parseFloat(value);
if (!maybeValue || parseFloat(value) <= 100) {
setSlippage(value);
setStorageValue(chainId, address, "bond-slippage", value);
localStorage.setItem("bond-slippage", value);
}
}, [chainId, address]);
}
const setFormatDecimalsInner = useCallback((value) => {
const setFormatDecimalsInner = (value) => {
if (Number(value) <= 17) {
setFormatDecimals(value);
setStorageValue(chainId, address, "bond-decimals", value);
localStorage.setItem("bond-decimals", value);
}
}, [chainId, address]);
}
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === "Escape") isSettingsOpen ? setSettingsOpen(false) : navigate(`/${network}/bonds`);
if (event.key === "Escape") isSettingsOpen ? setSettingsOpen(false) : navigate("/bonds");
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [network, navigate, isSettingsOpen]);
const chainSymbol = useMemo(() => {
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
if (chainSymbol) return chainSymbol;
return "WTF";
}, [config]);
const preparedQuoteToken = useMemo(() => {
if (isNetworkLegacy(chainId)) {
return {
address: bond.quoteToken.quoteTokenAddress,
chainNames: bond.quoteToken.chainNames,
icons: bond.quoteToken.icons,
name: bond.quoteToken.name,
}
}
return { address: undefined, icons: [chainSymbol], name: chainSymbol };
}, [bond, chainSymbol, chainId])
}, [navigate, isSettingsOpen]);
return (
<Box>
<PageTitle
name={
<Box display="flex" flexDirection="row" alignItems="center">
<Link component={RouterLink} to={`/${network}/bonds`}>
<Link component={RouterLink} to="/bonds">
<Box display="flex" flexDirection="row">
<ArrowBack />
<Typography fontWeight="500" marginLeft="9.5px" marginRight="18px">
@ -113,14 +83,10 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
</Box>
</Link>
<TokenStack
chainTokenNames={preparedQuoteToken.chainNames}
tokens={preparedQuoteToken.icons}
sx={{ fontSize: "27px" }}
/>
<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "27px" }} />
<Box display="flex" flexDirection="column" ml={1} justifyContent="center" alignItems="center">
<Typography variant="h4" fontWeight={500}>
{preparedQuoteToken.name}
{bond.quoteToken.name}
</Typography>
</Box>
</Box>
@ -166,12 +132,10 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
<BondInputArea
chainId={chainId}
bond={bond}
config={config}
connect={connect}
address={address}
slippage={slippage}
recipientAddress={recipientAddress}
preparedQuoteToken={preparedQuoteToken}
handleSettingsOpen={() => setSettingsOpen(true)}
formatDecimals={formatDecimals}
/>

View File

@ -1,6 +1,5 @@
import { Box, Tab, Tabs, Typography, Container, useMediaQuery } from "@mui/material";
import { useEffect, useState } from "react";
import { useParams, useLocation } from "react-router-dom";
import ReactGA from "react-ga4";
import Paper from "../../components/Paper/Paper";
@ -20,9 +19,6 @@ import { useFtsoPrice } from "../../hooks/prices";
import { useTokenSymbol } from "../../hooks/tokens";
const Bonds = ({ chainId, address, connect }) => {
const location = useLocation();
const { network } = useParams();
const [isZoomed] = useState(false);
const [secondsTo, setSecondsTo] = useState(0);
@ -30,10 +26,10 @@ const Bonds = ({ chainId, address, connect }) => {
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location]);
ReactGA.send({ hitType: "pageview", page: "/bonds" });
}, []);
const { liveBonds } = useLiveBonds(chainId, network);
const { liveBonds } = useLiveBonds(chainId);
const totalReserves = useTotalReserves(chainId);
const ftsoPrice = useFtsoPrice(chainId);
@ -49,7 +45,7 @@ const Bonds = ({ chainId, address, connect }) => {
return (
<Box>
<PageTitle name={"Protocol Bonds"} subtitle={`(1, 1) Buy ${ftsoSymbol} at a treasury-backed discount`} />
<PageTitle name={"Protocol Bonding"} subtitle={`Buy ${ftsoSymbol} from the protocol at a discount`} />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",

View File

@ -1,6 +1,5 @@
import { useState, useEffect } from "react";
import { Box, Typography, Checkbox, FormControlLabel } from "@mui/material";
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
import { useState } from "react";
import { Box, Typography } from "@mui/material";
import { styled, useTheme } from "@mui/material/styles";
import SettingsIcon from '@mui/icons-material/Settings';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
@ -11,14 +10,13 @@ import Metric from "../../../components/Metric/Metric";
import Modal from "../../../components/Modal/Modal";
import TokenStack from "../../../components/TokenStack/TokenStack";
import DataRow from "../../../components/DataRow/DataRow";
import { PrimaryButton, SecondaryButton } from "../../../components/Button";
import { PrimaryButton } from "../../../components/Button";
import BondDiscount from "./BondDiscount";
import BondVesting from "./BondVesting";
import BondSlippage from "./BondSlippage";
import { purchaseBond } from "../../../hooks/bonds";
import { useWarmupLength } from "../../../hooks/staking";
const StyledBox = styled(Box, {
shouldForwardProp: prop => prop !== "template",
@ -39,64 +37,11 @@ const BondConfirmModal = ({
sender,
handleSettingsOpen,
isOpen,
isNative,
bondQuoteTokenName,
bondQuoteTokenIcons,
bondQuoteTokenChainNames,
handleConfirmClose
}) => {
const theme = useTheme();
const { warmupLength, warmupExists: needsWarmup } = useWarmupLength(chainId);
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
const [isPending, setIsPending] = useState(false);
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup]);
const AcknowledgeWarmupCheckbox = () => {
if (!needsWarmup) return <></>;
return (
<Box sx={{ marginBottom: "1rem" }}>
<FormControlLabel
control={
<Checkbox
data-testid="acknowledge-bond-warm-up"
checked={acknowledgedWarmup}
onChange={event => setAcknowledgedWarmup(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={
<Typography variant="body2">
{`I understand the ${bondQuoteTokenName} Im bonding will only be available to claim ${warmupLength.toString()} epochs after my transaction is confirmed, and the warm-up extends with each bond purchase`}
</Typography>
}
/>
</Box>
);
};
const NeedsWarmupDetails = () => {
if (!needsWarmup) return <></>;
return (
<>
<AcknowledgeWarmupCheckbox />
<SecondaryButton
fullWidth
href="https://ghostchain.io/ghostdao_litepaper"
>
Why is there a warm-up?
</SecondaryButton>
</>
);
};
const handleConfirmCloseMaster = () => {
setAcknowledgedWarmup(false);
handleConfirmClose()
}
const onSubmit = async () => {
setIsPending(true);
@ -108,19 +53,18 @@ const BondConfirmModal = ({
const maxPrice = bond.price.inBaseToken._value * bigIntSlippage / one;
const referral = import.meta.env.VITE_APP_REFERRAL_ADDRESS;
await purchaseBond({
await purchaseBond(
chainId,
bondId: bond.id,
amount: spendAmountValue._value.toBigInt(),
bond.id,
spendAmountValue._value.toBigInt(),
maxPrice,
user: recipientAddress,
recipientAddress,
sender,
referral,
isNative
});
referral
);
setIsPending(false);
handleConfirmCloseMaster();
handleConfirmClose();
}
return (
@ -128,86 +72,43 @@ const BondConfirmModal = ({
maxWidth="476px"
minHeight="200px"
open={isOpen}
headerContent={<Typography variant="h4">{bondQuoteTokenName} Bond</Typography>}
onClose={!isPending && handleConfirmCloseMaster}
topRight={
<GhostStyledIcon
viewBox="0 0 23 23"
component={SettingsIcon}
style={{ cursor: "pointer" }}
onClick={handleSettingsOpen}
/>
headerContent={
<Box display="flex" flexDirection="row">
<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "27px" }} />
<Typography variant="h4" sx={{ marginLeft: "6px" }}>
{bond.quoteToken.name}
</Typography>
</Box>
}
onClose={!isPending && handleConfirmClose}
topLeft={<GhostStyledIcon viewBox="0 0 23 23" component={SettingsIcon} style={{ cursor: "pointer" }} onClick={handleSettingsOpen} />}
>
<>
<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Box display="flex" flexDirection="column">
<Metric
label="To Bond"
label="Assets to Bond"
metric={spendAmount}
/>
<Box
sx={{ background: "" }}
height="120px"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
>
<TokenStack
parentSx={{
height: "80px",
"& svg:nth-of-type(2)": {
transform: "translate(50%, -30%)",
zIndex: 100,
}
}}
sx={{ transform: "scale(1.65)", }}
tokens={bond.quoteToken.icons}
chainTokenNames={bond.quoteToken.chainNames}
/>
<Typography>{bondQuoteTokenName}</Typography>
<Box display="flex" flexDirection="row" justifyContent="center">
<Typography>{bond.quoteToken.name}</Typography>
</Box>
</Box>
<GhostStyledIcon sx={{ transform: "rotate(-90deg)" }} component={ArrowDropDownIcon} />
<Box display="flex" flexDirection="column">
<Metric label="To Receive" metric={receiveAmount} />
<Box
sx={{ background: "" }}
height="120px"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
gap="0"
>
<TokenStack
parentSx={{
height: "80px",
"& svg:nth-of-type(2)": {
transform: "translate(50%, -30%)",
zIndex: 100,
}
}}
sx={{ transform: "scale(1.65)", }}
tokens={bond.baseToken.icons}
chainTokenNames={bond.baseToken.chainNames}
/>
<Metric label="Assets to Receive" metric={receiveAmount} />
<Box display="flex" flexDirection="row" justifyContent="center">
<Typography>{bond.baseToken.name}</Typography>
</Box>
</Box>
</Box>
<Box mt="10px" mb="18px" borderTop={`1px solid ${theme.colors.gray[500]}`}></Box>
<Box mt="21px" mb="21px" borderTop={`1px solid ${theme.colors.gray[500]}`}></Box>
<DataRow title="ROI" balance={<BondDiscount discount={bond.discount} textOnly />} />
<DataRow title="Bond Slippage" balance={<BondSlippage slippage={slippage} textOnly />} />
<DataRow title="Vesting Term" balance={<BondVesting vesting={bond.vesting} />} />
{!acknowledgedWarmup && <Box>
<Box mt="21px" mb="21px" borderTop={`1px solid ${theme.colors.gray[500]}`}></Box>
<NeedsWarmupDetails />
</Box>}
{acknowledgedWarmup && <PrimaryButton fullWidth onClick={onSubmit} disabled={isPending} loading={isPending}>
<PrimaryButton fullWidth onClick={onSubmit} disabled={isPending} loading={isPending}>
{isPending ? "Bonding..." : "Confirm Bond Purchase"}
</PrimaryButton>}
</PrimaryButton>
</>
</Modal>
);

View File

@ -1,6 +1,6 @@
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
import { Box, Checkbox, FormControlLabel, useMediaQuery } from "@mui/material";
import { useState, useMemo, useCallback } from "react";
import { Box, Checkbox, FormControlLabel } from "@mui/material";
import { useState, useMemo } from "react";
import { useLocation } from "react-router-dom";
import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses";
@ -30,15 +30,12 @@ const BondInputArea = ({
formatDecimals,
handleSettingsOpen,
address,
preparedQuoteToken,
connect
}) => {
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const { pathname } = useLocation();
const { currentIndex } = useCurrentIndex(chainId);
const { balance, refetch } = useBalance(chainId, preparedQuoteToken.address, address);
const { balance, refetch } = useBalance(chainId, bond.quoteToken.quoteTokenAddress, address);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
@ -66,7 +63,7 @@ const BondInputArea = ({
refetch();
}
const setMax = useCallback(() => {
const setMax = () => {
if (!balance) return;
if (bond.capacity.inQuoteToken.lt(bond.maxPayout.inQuoteToken)) {
@ -82,17 +79,12 @@ const BondInputArea = ({
? bond.maxPayout.inQuoteToken.toString() // Payout is the smallest
: balance.toString(),
);
}, [bond, balance]);
};
const baseTokenString = useMemo(() => (bond.maxPayout.inBaseToken.lt(bond.capacity.inBaseToken)
const baseTokenString = (bond.maxPayout.inBaseToken.lt(bond.capacity.inBaseToken)
? bond.maxPayout.inBaseToken
: bond.capacity.inBaseToken
), [bond]);
const incorrectInputAmount = useMemo(() => {
if (!balance) return false;
return balance.lt(parsedAmount) || baseTokenString.lt(amountInBaseToken);
}, [amountInBaseToken, baseTokenString, balance, parsedAmount]);
);
return (
<Box minHeight="calc(100vh - 210px)" display="flex" flexDirection="column" justifyContent="center">
@ -100,39 +92,27 @@ const BondInputArea = ({
<Box display="flex" flexDirection="column" width="100%" maxWidth="476px">
<Box mb="21px">
<SwapCollection
iconNotNeeded
UpperSwapCard={
<SwapCard
maxWidth="476px"
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
inputWidth="280px"
id="from"
token={
<TokenStack
chainTokenNames={bond.quoteToken.chainNames}
tokens={bond.quoteToken.icons}
sx={{ fontSize: "25px", height: "30px" }}
/>
}
tokenName={preparedQuoteToken.name}
info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)}
token={<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "21px" }} />}
tokenName={bond.quoteToken.name}
info={formatCurrency(balance, formatDecimals, bond.quoteToken.name)}
endString="Max"
endStringOnClick={setMax}
value={amount}
onChange={event => setAmount(event.currentTarget.value)}
inputProps={{ "data-testid": "fromInput" }}
/>}
/>
}
LowerSwapCard={
<SwapCard
maxWidth="476px"
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
inputWidth="280px"
id="to"
token={
<TokenStack
chainTokenNames={bond.baseToken.chainNames}
tokens={bond.baseToken.icons}
sx={{ fontSize: "25px", height: "30px" }}
/>
}
token={<TokenStack tokens={bond.baseToken.icons} sx={{ fontSize: "21px" }} />}
tokenName={bond.baseToken.name}
value={amountInBaseToken.toString({ decimals: 9 })}
inputProps={{ "data-testid": "toInput" }}
@ -152,7 +132,6 @@ const BondInputArea = ({
connect={connect}
width="100%"
height="60px"
isNative={preparedQuoteToken.address === undefined}
isVertical
message={
<>
@ -176,7 +155,7 @@ const BondInputArea = ({
)}
<PrimaryButton
fullWidth
disabled={incorrectInputAmount || bond.isSoldOut || (showDisclaimer && !checked)}
disabled={bond.isSoldOut || (showDisclaimer && !checked)}
onClick={() => setConfirmOpen(true)}
>
Bond
@ -198,12 +177,12 @@ const BondInputArea = ({
)}
</span>
}
tooltip={`Total amount of ${ftsoSymbol} you will receive from this bond purchase`}
tooltip={`The total amount of payout asset you will receive from this bond purchase`}
/>
<DataRow
title="Max You Can Buy"
tooltip={`Maximum ${ftsoSymbol} that can be purchased in a single transaction. Prevents whales from buying entire bond supply at once.`}
tooltip={`The maximum quantity of payout token offered via bonds at this moment in time`}
balance={
<span>
{bond.baseToken.tokenAddress.toUpperCase() === bond.quoteToken.quoteTokenAddress.toUpperCase()
@ -216,14 +195,14 @@ const BondInputArea = ({
<DataRow
title="Discount"
balance={<BondDiscount discount={bond.discount} textOnly />}
tooltip={`The discount (or premium) between ${ftsoSymbol} market price and bond price. Higher discount = better deal for bond buyers.`}
/>
tooltip={`The bond discount is the percentage difference between ${ftsoSymbol} market value and the bond's price`}
/>
<DataRow
title={`Vesting Term`}
balance={<BondVesting vesting={bond.vesting} />}
tooltip={"Time until bond fully vests and becomes claimable. Vesting period aligns bond buyer with the protocol by encouraging longer-term holding."}
/>
tooltip={"The duration of the Bond whereby the bond can be claimed in its entirety"}
/>
{recipientAddress !== address && (
<DataRow title={`Recipient`} balance={shorten(recipientAddress)} />
@ -240,10 +219,6 @@ const BondInputArea = ({
spendAmountValue={parsedAmount}
spendAmount={formatNumber(parsedAmount, formatDecimals)}
receiveAmount={formatNumber(amountInBaseToken, formatDecimals)}
bondQuoteTokenName={preparedQuoteToken.name}
bondQuoteTokenIcons={preparedQuoteToken.icons}
bondQuoteTokenChainNames={preparedQuoteToken.chainNames}
isNative={preparedQuoteToken.address === undefined}
handleSettingsOpen={handleSettingsOpen}
isOpen={confirmOpen}
sender={address}

View File

@ -11,7 +11,7 @@ import {
Typography,
useMediaQuery
} from "@mui/material";
import { NavLink, useParams } from "react-router-dom";
import { NavLink } from "react-router-dom";
import ArrowUp from "../../../assets/icons/arrow-up.svg?react";
import BondDiscount from "./BondDiscount";
@ -26,7 +26,6 @@ import { TertiaryButton } from "../../../components/Button";
export const BondList = ({ bonds, secondsTo, chainId }) => {
const isSmallScreen = useScreenSize("md");
const { network } = useParams();
if (bonds.length === 0) {
return (
@ -44,7 +43,7 @@ export const BondList = ({ bonds, secondsTo, chainId }) => {
</Box>
{sortBondsByDiscount(bonds).map(bond => (
<BondCard key={bond.id} secondsTo={secondsTo} bond={bond} chainName={network} />
<BondCard key={bond.id} secondsTo={secondsTo} bond={bond} />
))}
</>
);
@ -54,7 +53,7 @@ export const BondList = ({ bonds, secondsTo, chainId }) => {
<>
<BondTable>
{sortBondsByDiscount(bonds).map(bond => (
<BondRow key={bond.id} bond={bond} secondsTo={secondsTo} chainName={network} />
<BondRow key={bond.id} bond={bond} secondsTo={secondsTo} />
))}
</BondTable>
@ -65,14 +64,14 @@ export const BondList = ({ bonds, secondsTo, chainId }) => {
);
};
const BondCard = ({ bond, secondsTo, chainName }) => {
const BondCard = ({ bond, secondsTo }) => {
const quoteTokenName = bond.quoteToken.name;
const baseTokenName = bond.baseToken.name;
return (
<Box key={bond.id} id={bond.id + `--bond`} mt="32px">
<Box display="flex" alignItems="center">
<TokenStack chainTokenNames={bond.quoteToken.chainNames} tokens={bond.quoteToken.icons} />
<TokenStack tokens={bond.quoteToken.icons} />
<Box display="flex" flexDirection="column" ml="8px">
<Typography>{bond.quoteToken.name}</Typography>
@ -131,11 +130,10 @@ const BondCard = ({ bond, secondsTo, chainName }) => {
<Box mt="16px">
<Link
component={NavLink}
to={`/${chainName}/bonds/${bond.id}`}
sx={{ pointerEvents: bond.isSoldOut ? 'none' : 'auto' }}
to={`/bonds/${bond.id}`}
>
<TertiaryButton fullWidth disabled={bond.isSoldOut}>
{bond.isSoldOut ? "Sold Out" : `Bond`}
<TertiaryButton fullWidth>
Bond
</TertiaryButton>
</Link>
</Box>
@ -175,7 +173,7 @@ const payoutTokenCapacity = (bond) => {
return `${formatNumber(payoutTokenCapacity, 4)} ${bond.baseToken.name}`;
};
const BondRow = ({ bond, secondsTo, chainName }) => {
const BondRow = ({ bond, secondsTo }) => {
const quoteTokenName = bond.quoteToken.name;
const baseTokenName = bond.baseToken.name;
@ -223,8 +221,7 @@ const BondRow = ({ bond, secondsTo, chainName }) => {
<TableCell style={{ padding: "8px 0" }}>
<Link
component={NavLink}
to={`/${chainName}/bonds/${bond.id}`}
sx={{ pointerEvents: bond.isSoldOut ? 'none' : 'auto' }}
to={`/bonds/${bond.id}`}
>
<TertiaryButton fullWidth disabled={bond.isSoldOut}>
{bond.isSoldOut ? "Sold Out" : `Bond`}
@ -237,7 +234,7 @@ const BondRow = ({ bond, secondsTo, chainName }) => {
const TokenIcons = ({ token, chainId, explorer }) => (
<Box display="flex" alignItems="center">
<TokenStack chainTokenNames={token.chainNames} tokens={token.icons} />
<TokenStack tokens={token.icons} />
<Box display="flex" flexDirection="column" ml="16px">
<Typography style={{ fontSize: "12px", fontWeight: 600, lineHeight: "18px" }}>{token.name}</Typography>

View File

@ -13,27 +13,24 @@ import { PrimaryButton, TertiaryButton } from "../../../components/Button";
import { useScreenSize } from "../../../hooks/useScreenSize";
import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses";
import { isNetworkLegacy } from "../../../constants";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { formatCurrency } from "../../../helpers";
import { useCurrentIndex, useEpoch, useWarmupLength, useWarmupInfo } from "../../../hooks/staking";
import { useNotes, redeem, forceRedeem } from "../../../hooks/bonds";
import { useNotes, redeem } from "../../../hooks/bonds";
import { useTokenSymbol } from "../../../hooks/tokens";
import { useGhstPrice } from "../../../hooks/prices";
import { useBreakoutModal } from "../../../hooks/breakoutModal";
export const ClaimBonds = ({ chainId, address, secondsTo }) => {
const isSmallScreen = useScreenSize("md");
const [isPending, setIsPending] = useState(false);
const [pendingIndexes, setPendingIndexes] = useState([]);
const [isWarmup, setIsWapmup] = useState(false);
const [isPreClaimConfirmed, setPreClaimConfirmed] = useState(false);
const [isPayoutGhst, _] = useState(true);
const [isPayoutGhst, setIsPayoutGhst] = useState(localStorage.getItem("bond-isGhstPayout")
? localStorage.getItem("bond-isGhstPayout") === "true"
: true
);
const ghstPrice = useGhstPrice(chainId);
const { breakoutFromBonding } = useBreakoutModal();
const { epoch } = useEpoch(chainId);
const { warmupExists } = useWarmupLength(chainId);
const { warmupInfo } = useWarmupInfo(chainId, BOND_DEPOSITORY_ADDRESSES[chainId]);
@ -42,6 +39,7 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
const { notes, refetch: notesRefetch } = useNotes(chainId, address);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
if (!notes || notes.length === 0) return null;
@ -54,38 +52,26 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
18
);
const setIsPayoutGhstInner = (value) => {
setIsPayoutGhst(value);
localStorage.setItem("bond-isGhstPayout", value);
}
const onSubmit = async (indexes) => {
setIsPending(true);
setPendingIndexes(indexes);
const defaultFunction = async () => {
await redeem({ chainId, user: address, isGhst: isPayoutGhst, indexes });
await notesRefetch();
};
if (isNetworkLegacy(chainId)) {
const isFundsInWarmup = warmupInfo.deposit._value > 0n;
if (warmupExists && isFundsInWarmup && !isPreClaimConfirmed) {
setIsWapmup(true);
} else {
await defaultFunction();
}
const isFundsInWarmup = warmupInfo.deposit._value > 0n;
if (warmupExists && isFundsInWarmup && !isPreClaimConfirmed) {
setIsWapmup(true);
} else {
const warmupLeft = warmupInfo.expiry - epoch.number;
const amountRaw = notes
.filter(note => indexes.includes(note.id))
.reduce((sum, note) => sum + note.payout._value, 0n);
const amount = new DecimalBigNumber(amountRaw, 18);
const toExecute = async (receiver) => {
const txHash = await forceRedeem({ chainId, user: address, receiver, indexes });
return txHash;
}
breakoutFromBonding({ defaultFunction, toExecute, amount, warmupLeft })
setIsPending(true);
await redeem(
chainId,
address,
isPayoutGhst,
indexes
);
await notesRefetch();
setIsPending(false);
}
setPendingIndexes([]);
setIsPending(false);
}
return (
@ -103,11 +89,29 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
My Bonds
Your Bonds
</Typography>
</Box>
}
>
<Box display="flex" flexDirection="column" alignItems="center">
<Typography variant="h4" align="center" color="textSecondary">
Payout Options
</Typography>
<Tabs
centered
textColor="primary"
indicatorColor="primary"
value={isPayoutGhst ? 1 : 0}
aria-label="Payout token tabs"
onChange={(_, view) => setIsPayoutGhstInner(view === 1)}
TabIndicatorProps={{ style: { display: "none" } }}
>
<Tab aria-label="payout-stnk-button" label={stnkSymbol} style={{ fontSize: "1rem" }} />
<Tab aria-label="payout-ghst-button" label={ghstSymbol} style={{ fontSize: "1rem" }} />
</Tabs>
</Box>
<Box display="flex" justifyContent="center">
<Box display="flex" flexDirection="column" alignItems="center" mt="24px" width={isSmallScreen ? "100%" : "50%"}>
<Typography variant="h5" align="center" color="textSecondary" style={{ fontSize: "1.2rem" }}>
@ -120,139 +124,128 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
? formatCurrency(totalClaimableBalance, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(totalClaimableBalance), 5, stnkSymbol)
}
</Typography>
<Typography variant="subtitle1" align="center">
{formatCurrency(totalClaimableBalance * ghstPrice, 2)}
</Typography>
</Typography>
</Box>
<PrimaryButton
disabled={isPending || totalClaimableBalance._value === 0n}
fullWidth
className=""
onClick={() => onSubmit(notes.filter((note) => secondsTo > note.matured).map(note => note.id))}
>
Claim All
</PrimaryButton>
</Box>
<PrimaryButton
disabled={isPending || totalClaimableBalance._value === 0n}
fullWidth
className=""
onClick={() => onSubmit(notes.filter((note) => secondsTo > note.matured).map(note => note.id))}
>
Claim All
</PrimaryButton>
</Box>
</Box>
<Box mt="48px">
{isSmallScreen ? (
<>
{notes.map((note, index) => (
<Box key={index} mt="32px">
<Box display="flex" alignItems="center">
<TokenStack chainTokenNames={note.quoteToken.chainNames} tokens={note.quoteToken.icons} />
<Box ml="8px">
<Typography>{note.quoteToken.name}</Typography>
<Box mt="48px">
{isSmallScreen ? (
<>
{notes.map((note, index) => (
<Box key={index} mt="32px">
<Box display="flex" alignItems="center">
<TokenStack tokens={note.quoteToken.icons} />
<Box ml="8px">
<Typography>{note.quoteToken.name}</Typography>
</Box>
</Box>
</Box>
<Box display="flex" justifyContent="space-between" mt="16px">
<Typography>Duration</Typography>
<Typography>
<BondVesting vesting={note.vesting} />
</Typography>
</Box>
<Box display="flex" justifyContent="space-between" mt="16px">
<Typography>Duration</Typography>
<Typography>
<BondVesting vesting={note.vesting} />
</Typography>
</Box>
<Box display="flex" justifyContent="space-between" mt="8px">
<Typography>Remaining</Typography>
<Typography>
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
</Typography>
</Box>
<Box display="flex" justifyContent="space-between" mt="8px">
<Typography>Remaining</Typography>
<Typography>
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
</Typography>
</Box>
<Box display="flex" justifyContent="space-between" mt="8px">
<Typography>Payout</Typography>
<Box display="flex" flexDirection="column" alignItems="flex-end">
<Box display="flex" justifyContent="space-between" mt="8px">
<Typography>Payout</Typography>
<Typography>
{isPayoutGhst
? formatCurrency(note.payout, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(note.payout), 5, stnkSymbol)
}
</Typography>
<Typography>{formatCurrency(note.payout * ghstPrice, 2)}</Typography>
</Box>
<Box mt="16px">
<TertiaryButton
fullWidth
disabled={isPending || secondsTo < note.matured}
onClick={() => onSubmit([note.id])}
>
Claim
</TertiaryButton>
</Box>
</Box>
<Box mt="16px">
<TertiaryButton
fullWidth
disabled={isPending || secondsTo < note.matured}
loading={isPending && pendingIndexes.includes(note.id)}
onClick={() => onSubmit([note.id])}
>
{isPending && pendingIndexes.includes(note.id) ? "Claiming" : "Claim"}
</TertiaryButton>
</Box>
</Box>
))}
</>
) : (
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell style={{ width: "180px", padding: "8px 0" }}>Bond</TableCell>
<TableCell style={{ padding: "8px 0" }}>Duration</TableCell>
<TableCell style={{ padding: "8px 0" }}>Remaining</TableCell>
<TableCell style={{ padding: "8px 0" }}>Payout</TableCell>
</TableRow>
</TableHead>
<TableBody>
{notes.map((note, index) => (
<TableRow key={index}>
<TableCell style={{ padding: "8px 0" }}>
<Box display="flex" alignItems="center">
<TokenStack chainTokenNames={note.quoteToken.chainNames} tokens={note.quoteToken.icons} />
<Box display="flex" flexDirection="column" ml="16px">
<Typography>{note.quoteToken.name}</Typography>
))}
</>
) : (
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell style={{ width: "180px", padding: "8px 0" }}>Bond</TableCell>
<TableCell style={{ padding: "8px 0" }}>Duration</TableCell>
<TableCell style={{ padding: "8px 0" }}>Remaining</TableCell>
<TableCell style={{ padding: "8px 0" }}>Payout</TableCell>
</TableRow>
</TableHead>
<TableBody>
{notes.map((note, index) => (
<TableRow key={index}>
<TableCell style={{ padding: "8px 0" }}>
<Box display="flex" alignItems="center">
<TokenStack tokens={note.quoteToken.icons} />
<Box display="flex" flexDirection="column" ml="16px">
<Typography>{note.quoteToken.name}</Typography>
</Box>
</Box>
</Box>
</TableCell>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Typography>
<BondVesting vesting={note.vesting} />
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Typography>
<BondVesting vesting={note.vesting} />
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Typography>
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Typography>
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Box display="flex" flexDirection="column" alignItems="flex-start">
<TableCell style={{ padding: "8px 0" }}>
<Typography>
{isPayoutGhst
? formatCurrency(note.payout, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(note.payout), 5, stnkSymbol)
}
</Typography>
<Typography variant="body2">{formatCurrency(note.payout * ghstPrice, 2)}</Typography>
</Box>
</TableCell>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<TertiaryButton
fullWidth
disabled={isPending || secondsTo < note.matured}
loading={isPending && pendingIndexes.includes(note.id)}
onClick={() => onSubmit([note.id])}
>
{isPending && pendingIndexes.includes(note.id) ? "Claiming" : "Claim"}
</TertiaryButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
<TableCell style={{ padding: "8px 0" }}>
<TertiaryButton
fullWidth
disabled={isPending || secondsTo < note.matured}
onClick={() => onSubmit([note.id])}
>
Claim
</TertiaryButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
</Paper>
</>
);

View File

@ -44,7 +44,7 @@ const WarmupConfirmModal = ({
minHeight="150px"
open={isOpen}
headerText={
warmupLength <= 0
warmupLength < 0
? "Bond Notification"
: "Bond in Warm-up"
}
@ -52,24 +52,24 @@ const WarmupConfirmModal = ({
>
<Box gap="20px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
<Box display="flex" flexDirection="column">
{warmupLength <= 0
{warmupLength < 0
? <FormControlLabel
control={
<Checkbox
data-testid="acknowledge-warm-up"
data-testid="acknowledge-warmup"
checked={isChecked}
onChange={event => setIsChecked(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={`I acknowledge that I am releasing warm-up funds for the bonding contract on behalf of the collective.`}
label={`I acknowledge that I am releasing warmup funds for the bonding contract on behalf of the collective.`}
/>
: `Bonding address is in a warm-up period and cannot be claimed now. It'll be available for claim in ${warmupLength} epochs.`
}
</Box>
{warmupLength <= 0 && <PrimaryButton fullWidth disabled={!isChecked} onClick={onSubmit}>
{warmupLength < 0 && <PrimaryButton fullWidth disabled={!isChecked} onClick={onSubmit}>
Confirm
</PrimaryButton>}
</Box>

View File

@ -1,508 +0,0 @@
import { useMemo, useState, useCallback, useEffect } from "react";
import { Box, Typography, Link, Checkbox, FormControlLabel, useTheme } from "@mui/material";
import { useNavigate } from 'react-router-dom';
import { getBlockNumber } from "@wagmi/core";
import { useConfig } from "wagmi";
import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
import { toHex } from "@polkadot-api/utils";
import CheckIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
import Metric from "../../components/Metric/Metric";
import Modal from "../../components/Modal/Modal";
import SwapCard from "../../components/Swap/SwapCard";
import Token from "../../components/Token/Token";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { PrimaryButton, SecondaryButton } from "../../components/Button";
import { GATEKEEPER_ADDRESSES, EMPTY_ADDRESS } from "../../constants/addresses";
import { GHOST_CONNECT } from "../../constants/ecosystem";
import { useLocalStorage } from "../../hooks/localstorage";
import { useBreakoutModal } from "../../hooks/breakoutModal";
import { useTokenSymbol } from "../../hooks/tokens";
import { useEpoch, useGatekeeperApy, useGatekeeperAddress } from "../../hooks/staking";
import { useEvmNetwork, useCurrentIndex, useUnstableProvider } from "../../hooks/ghost";
import { formatNumber, shorten } from "../../helpers";
import { prettifySecondsInDays } from "../../helpers/timeUtil";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
const BreakoutModal = ({ chainId, address }) => {
const [step, setStep] = useState(0);
const [receiver, setReceiver] = useState("");
const [convertedReceiver, setConvertedReceiver] = useState(undefined);
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const evmNetwork = useEvmNetwork({ evmChainId: chainId });
const {
isOpened,
closeModal: closeModalInner,
isStakingOpened,
isClaimBondOpened,
warmupPeriod,
setActiveTxIndex,
defaultFunction,
executableFunction,
estimatedAmount
} = useBreakoutModal();
const incomingFee = useMemo(() => {
return new DecimalBigNumber(
evmNetwork ? evmNetwork.incoming_fee : 100000000,
7
);
}, [evmNetwork]);
const closeModal = () => {
setActiveTxIndex(-1);
closeModalPure();
}
const closeModalPure = () => {
setStep(0);
setReceiver("");
closeModalInner();
}
const header = useMemo(() => {
if (isStakingOpened && warmupPeriod <= 0) return "Stake Warmed-up"
if (isClaimBondOpened && warmupPeriod <= 0) return "Bond Warmed-up"
if (isStakingOpened && warmupPeriod > 0) return "Stake in Warm-up"
if (isClaimBondOpened && warmupPeriod > 0) return "Bond in Warm-up"
}, [isStakingOpened, isClaimBondOpened, warmupPeriod]);
const bridgeNumbers = useMemo(() => {
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
const number = 1 + connectedNetworks * 3;
return `(${number}, ${number})`;
}, [chainId]);
useEffect(() => {
try {
const [publicKey, prefix] = ss58Decode(receiver);
if (prefix !== 1995 && prefix !== 1996) {
throw new Error("bad prefix");
}
setConvertedReceiver(toHex(publicKey));
} catch {
setConvertedReceiver(undefined);
}
}, [receiver]);
return (
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">{step === 0 ? header : step === 1 ? `Start ${bridgeNumbers} ${"Stake\u00B2"}` : "Bridge Confirmation"}</Typography>
</Box>
}
open={isOpened}
onClose={closeModal}
maxWidth="380px"
minHeight="200px"
>
<Box height="420px" display="flex" flexDirection="column" justifyContent="space-between">
{step === 0
? <WelcomeView
isStakingOpened={isStakingOpened}
chainId={chainId}
warmupPeriod={warmupPeriod}
ghstSymbol={ghstSymbol}
ftsoSymbol={ftsoSymbol}
bridgeNumbers={bridgeNumbers}
defaultFunction={defaultFunction}
goNext={() => setStep(1)}
closeModal={closeModal}
/>
: step === 1
? <BridgeView
receiver={receiver}
setReceiver={setReceiver}
chainId={chainId}
bridgeNumbers={bridgeNumbers}
ghstSymbol={ghstSymbol}
estimatedAmount={estimatedAmount}
incomingFee={incomingFee}
goNext={() => setStep(2)}
convertedReceiver={convertedReceiver}
setConvertedReceiver={setConvertedReceiver}
/>
: <ConfirmStep
chainId={chainId}
address={address}
receiver={receiver}
executableFunction={executableFunction}
isStakingOpened={isStakingOpened}
bridgeNumbers={bridgeNumbers}
incomingFee={incomingFee}
estimatedAmount={estimatedAmount}
ghstSymbol={ghstSymbol}
setActiveTxIndex={setActiveTxIndex}
closeModal={closeModalPure}
evmNetwork={evmNetwork}
convertedReceiver={convertedReceiver}
/>
}
</Box>
</Modal>
)
}
const BridgeView = ({
chainId,
receiver,
setReceiver,
convertedReceiver,
setConvertedReceiver,
bridgeNumbers,
ghstSymbol,
estimatedAmount,
goNext,
incomingFee
}) => {
const theme = useTheme();
const [inputFocused, setInputFocused] = useState(false);
const config = useConfig();
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
const chainExplorerUrl = useMemo(() => {
const client = config?.getClient();
return client?.chain?.blockExplorers?.default?.url;
}, [config]);
return (
<>
<Typography>Bridge to start earning {bridgeNumbers} {"Stake\u00B2"} on your {ghstSymbol} balance:</Typography>
<Box display="flex" justifyContent="center">
<Typography variant="h5">{formatNumber(estimatedAmount, 5)} {ghstSymbol}</Typography>
</Box>
<Typography>
Generate a unique address for per tx with <Link underline="hover" href={GHOST_CONNECT} color={theme.colors.primary[300]}>GHOST Connect</Link> for privacy.
</Typography>
<SwapCard
id={`bridge-token-receiver`}
inputWidth={"100%"}
onBlur={() => setInputFocused(true)}
onFocus={() => setInputFocused(false)}
onChange={event => setReceiver(event.currentTarget.value)}
placeholder={convertedReceiver ? shorten(receiver, 15, -10) : "GHOST address (sf prefixed)"}
value={convertedReceiver ? "" : receiver}
inputProps={{ "data-testid": "fromInput" }}
endString={convertedReceiver
? <GhostStyledIcon color="success" viewBox="0 0 25 25" component={CheckIcon} />
: inputFocused && receiver !== ""
? <GhostStyledIcon color="error" viewBox="0 0 25 25" component={CancelIcon} />
: undefined
}
type="text"
maxWidth="100%"
/>
<Box display="flex" justifyContent="center" flexDirection="column" alignItems="center">
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between">
<Typography variant="body2">Gatekeeper</Typography>
<Link
fontSize="12px"
lineHeight="15px"
target="_blank"
rel="noopener noreferrer"
href={`${chainExplorerUrl}/token/${gatekeeperAddress}`}
>
<Typography variant="body2">
{shorten(gatekeeperAddress, 10, -8)}
</Typography>
</Link>
</Box>
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between">
<Typography variant="body2">Bridge Fee</Typography>
<Typography variant="body2">{formatNumber(incomingFee, 4)}%</Typography>
</Box>
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between">
<Typography variant="body2">Est. Time</Typography>
<Typography variant="body2">20 mins</Typography>
</Box>
</Box>
<PrimaryButton
onClick={goNext}
disabled={convertedReceiver === undefined}
fullWidth
>
Proceed
</PrimaryButton>
</>
)
}
const WelcomeView = ({
bridgeNumbers,
goNext,
isStakingOpened,
chainId,
warmupPeriod,
ghstSymbol,
ftsoSymbol,
defaultFunction,
closeModal
}) => {
const [refreshNeeded, setRefreshNeeded] = useState(false);
const [isPending, setIsPending] = useState(false);
const { epoch } = useEpoch(chainId);
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
const { gatekeepedApy, apyInner } = useGatekeeperApy(chainId);
const { isExtensionMissing } = useUnstableProvider();
const refreshPage = () => window.location.reload();
const getConnect = () => {
setRefreshNeeded(true)
window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer');
}
const callDefaultFunction = useCallback(async () => {
setIsPending(true);
await defaultFunction()();
setIsPending(false);
closeModal();
}, [defaultFunction]);
return (
<>
<Typography>{warmupPeriod <= 0
? `You have succesfully warmed-up your ${isStakingOpened ? " " : "bonded "}${ftsoSymbol} ${isStakingOpened ? "(3, 3)" : "(1, 1)"} Staked at:`
: `${isStakingOpened ? "Stake" : "Bond"} is in warm-up${isStakingOpened ? "" : ", which extends with each purchase"}. Your ${ftsoSymbol} (3, 3) Staked at:`
}</Typography>
<Box display="flex" justifyContent="center">
<Typography variant="h5">{formatNumber(apyInner, 2)}% APY</Typography>
</Box>
<SecondaryButton
onClick={() => callDefaultFunction()}
disabled={isPending || warmupPeriod > 0}
loading={isPending}
fullWidth
>
{warmupPeriod > 0
? `Warm-up ends in ${prettifySecondsInDays(epoch.length * warmupPeriod)}`
: `${isPending ? "Claiming..." : "Claim"} ${isStakingOpened ? "(3, 3) Stake" : "(1, 1) Bond"}`
}
</SecondaryButton>
<Box display="flex" justifyContent="center" flexDirection="column" alignItems="center">
<hr style={{ width: "100%" }} />
<Typography variant="h5">OR</Typography>
<hr style={{ width: "100%" }} />
</Box>
<Box display="flex" flexDirection="column" justifyContent="space-between" gap="10px">
<Typography fontWeight="bold">Skip the Warm-up Now!</Typography>
<Typography>{`Bridge your ${ghstSymbol} to GHOST Chain and start ${bridgeNumbers} ${"Stake\u00B2"} at:`}</Typography>
</Box>
<Box display="flex" justifyContent="center" flexDirection="column" alignItems="center">
<Typography variant="h5">{formatNumber(gatekeepedApy, 2)}% APY</Typography>
</Box>
<PrimaryButton
disabled={isPending || gatekeeperAddress === EMPTY_ADDRESS}
onClick={isExtensionMissing
? (refreshNeeded ? refreshPage : getConnect)
: goNext
}
fullWidth
>
{isExtensionMissing
? (refreshNeeded ? "Refresh Page" : "Get GHOST Connect")
: `Start ${bridgeNumbers} ${"Stake\u00B2"}`}
</PrimaryButton>
</>
)
}
const ConfirmStep = ({
chainId,
address,
receiver,
convertedReceiver,
executableFunction,
ghstSymbol,
bridgeNumbers,
estimatedAmount,
bridgingRisk,
incomingFee,
setActiveTxIndex,
closeModal,
evmNetwork
}) => {
const config = useConfig();
const navigate = useNavigate();
const currentSession = useCurrentIndex();
const { getStorageValue, setStorageValue } = useLocalStorage();
const [blockNumber, setBlockNumber] = useState(0n);
const [isPending, setIsPending] = useState(false);
const [acknowledgeBridgingRisk, setAcknowledgeBridgingRisk] = useState(false);
const [acknowledgeWalletCustody, setAcknowledgeWalletCustody] = useState(false);
getBlockNumber(config).then(block => setBlockNumber(block));
const nativeSymbol = useMemo(() => config?.getClient()?.chain?.nativeCurrency?.symbol, [config]);
const networkName= useMemo(() => config?.getClient()?.chain?.name.toLowerCase(), [config]);
const receivedEstimation = useMemo(() => {
const decimals = incomingFee._decimals + 2;
const afterFee = new DecimalBigNumber(
BigInt(Math.pow(10, decimals) - incomingFee._value),
decimals
);
return estimatedAmount.mul(afterFee);
}, [incomingFee, estimatedAmount]);
const execute = useCallback(async () => {
setIsPending(true);
try {
const txHash = await executableFunction()(convertedReceiver);
if (txHash) {
const expectedSessionIndex = (currentSession ?? 0) + (evmNetwork
? Number((evmNetwork.avg_block_speed * evmNetwork.finality_delay) / (1000n * 14400n))
: 0);
const transaction = {
receiverAddress: receiver,
amount: estimatedAmount._value.toString(),
sessionIndex: expectedSessionIndex,
transactionHash: txHash,
blockNumber: Number(blockNumber),
chainId: chainId,
bridgeStability: 69, // TODO: avoid stability
timestamp: Date.now()
}
const storedTransactions = getStorageValue(chainId, address, "bridge-txs", []);
const newStoredTransactions = [transaction, ...storedTransactions];
setStorageValue(chainId, address, "bridge-txs", newStoredTransactions);
setActiveTxIndex(0);
navigate(`${networkName}/bridge`);
}
} finally {
setIsPending(false);
closeModal();
}
}, [
executableFunction,
convertedReceiver,
receiver,
networkName,
chainId,
address,
blockNumber,
evmNetwork,
currentSession,
]);
return (
<>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box display="flex" flexDirection="column" justifyContent="space-between" alignItems="center" gap="10px">
<Metric label="To Bridge" metric={formatNumber(estimatedAmount, 5)} />
<Box width="100%" display="flex" flexDirection="column" justifyContent="center" alignItems="center">
<Token chainTokenName={nativeSymbol} name={"GHST"} sx={{ fontSize: "55px" }} />
<Typography>{ghstSymbol}</Typography>
</Box>
</Box>
<GhostStyledIcon sx={{ transform: "rotate(-90deg)" }} component={ArrowDropDownIcon} />
<Box display="flex" flexDirection="column" justifyContent="space-between" alignItems="center" gap="10px">
<Metric label="To Receive" metric={formatNumber(receivedEstimation, 5)} />
<Box width="100%" display="flex" flexDirection="column" justifyContent="center" alignItems="center">
<Token isNative name={"GHST"} sx={{ fontSize: "55px" }} />
<Typography>{ghstSymbol}</Typography>
</Box>
</Box>
</Box>
<Typography>{`You are bridging to GHOST Chain now to claim ${bridgeNumbers} ${"Stake\u00B2"} rewards.`}</Typography>
<hr style={{ width: "100%" }} />
<Box display="flex" flexDirection="column" justifyContent="space-between" alignItems="left">
<FormControlLabel
control={
<Checkbox
data-testid="acknowledge-breakout-warm-up"
checked={acknowledgeBridgingRisk}
onChange={event => setAcknowledgeBridgingRisk(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={
<Typography variant="body2">
{`I acknowledge decentralized bridging risk.`}&nbsp;
<Link
sx={{
margin: "0px",
font: "inherit",
letterSpacing: "inherit",
textDecoration: "underline",
textUnderlineOffset: "0.23rem",
cursor: "pointer",
textDecorationThickness: "1px",
"&:hover": {
textDecoration: "underline",
}
}}
target="_blank"
rel="noopener noreferrer"
href="https://ghostchain.io/bridge-disclaimer"
>
Learn more.
</Link>
</Typography>
}
sx={{ '& .MuiFormControlLabel-label': { textAlign: "justify" } }}
/>
<FormControlLabel
control={
<Checkbox
data-testid="acknowledge-breakout-warm-up"
checked={acknowledgeWalletCustody}
onChange={event => setAcknowledgeWalletCustody(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={
<Typography variant="body2">
I confirm that recipient address is a self-custodial wallet, not an exchange, third party service, or smart-contract.
</Typography>
}
sx={{ '& .MuiFormControlLabel-label': { textAlign: "justify" } }}
/>
</Box>
<PrimaryButton
onClick={() => execute()}
loading={isPending}
disabled={isPending || !acknowledgeWalletCustody || !acknowledgeBridgingRisk}
fullWidth
>
{isPending ? "Confirming..." : "Confirm"}
</PrimaryButton>
</>
)
}
export default BreakoutModal;

View File

@ -1,5 +1,4 @@
import { useEffect, useState, useMemo, useCallback } from "react";
import { useLocation } from "react-router-dom";
import ReactGA from "react-ga4";
import {
@ -11,10 +10,9 @@ import {
useMediaQuery,
} from "@mui/material";
import { decodeAddress } from "@polkadot/util-crypto";
import { fromHex } from "@polkadot-api/utils";
import { getBlockNumber } from "@wagmi/core";
import { useTransaction } from "wagmi";
import { keccak256, decodeFunctionData } from "viem";
import { keccak256 } from "viem";
import { u32, u64, u128 } from "scale-ts";
import PendingActionsIcon from '@mui/icons-material/PendingActions';
@ -24,7 +22,6 @@ import PageTitle from "../../components/PageTitle/PageTitle";
import Paper from "../../components/Paper/Paper";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { abi as StakingAbi } from "../../abi/GhostStaking.json";
import { networkAvgBlockSpeed } from "../../constants";
import { timeConverter } from "../../helpers";
@ -45,20 +42,16 @@ import {
useCurrentSlot,
useGenesisSlot,
useErasTotalStake,
useLatestBlockNumber,
useEraIndex,
} from "../../hooks/ghost";
import { useLocalStorage } from "../../hooks/localstorage";
import { useBreakoutModal } from "../../hooks/breakoutModal";
import { ValidatorTable } from "./ValidatorTable";
import { BridgeModal, BridgeConfirmModal } from "./BridgeModal";
import { BridgeHeader } from "./BridgeHeader";
import { BridgeCardAction, BridgeCardHistory } from "./BridgeCard";
const Bridge = ({ chainId, address, config, connect }) => {
const location = useLocation();
const STORAGE_PREFIX = "storedTransactions"
const Bridge = ({ chainId, address, config, connect }) => {
const isBigScreen = useMediaQuery("(max-width: 980px)")
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isSemiSmallScreen = useMediaQuery("(max-width: 540px)");
@ -66,20 +59,19 @@ const Bridge = ({ chainId, address, config, connect }) => {
const [bridgeModalOpen, setBridgeModalOpen] = useState(false);
const [isConfirmed, setIsConfirmed] = useState(false);
const [activeTxIndex, setActiveTxIndex] = useState(-1);
const [blockNumber, setBlockNumber] = useState(0n);
const [bridgeAction, setBridgeAction] = useState(true);
const [currentTime, setCurrentTime] = useState(Date.now());
const { getStorageValue, setStorageValue } = useLocalStorage();
const { activeTxIndex, setActiveTxIndex } = useBreakoutModal();
useEffect(() => {
const interval = setInterval(() => setCurrentTime(Date.now()), 1000);
return () => clearInterval(interval);
}, []);
const [storedTransactions, setStoredTransactions] = useState(() =>
getStorageValue(chainId, address, "bridge-txs", [])
const initialStoredTransactions = localStorage.getItem(STORAGE_PREFIX);
const [storedTransactions, setStoredTransactions] = useState(
initialStoredTransactions ? JSON.parse(initialStoredTransactions) : []
);
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
@ -105,33 +97,29 @@ const Bridge = ({ chainId, address, config, connect }) => {
});
const hashedArguments = useMemo(() => {
if (!watchTransaction) return undefined;
if (!watchTransaction) return undefined
const networkIdEncoded = u64.enc(BigInt(chainId));
const amountEncoded = u128.enc(BigInt(watchTransaction.amount));
const addressEncoded = decodeAddress(watchTransaction.receiverAddress, false, 1996);
const transactionHashEncoded = fromHex(watchTransaction.transactionHash);
const blockNumber = u64.enc(watchTransactionInfo?.blockNumber ?? 0n);
const clapArgsStr = new Uint8Array([
...addressEncoded,
...amountEncoded,
...blockNumber,
...transactionHashEncoded,
...networkIdEncoded
]);
return keccak256(clapArgsStr)
}, [watchTransaction, watchTransactionInfo])
const latestBlockNumber = useLatestBlockNumber();
const eraIndex = useEraIndex();
const currentSlot = useCurrentSlot();
const genesisSlot = useGenesisSlot();
const currentSession = useCurrentIndex();
const applauseThreshold = useApplauseThreshold();
const evmNetwork = useEvmNetwork({ evmChainId: chainId });
const totalStakedAmount = useErasTotalStake({
epochIndex: eraIndex?.index ?? 0,
eraIndex: Math.floor((watchTransaction?.sessionIndex ?? currentSession) / 6)
});
const authorities = useAuthorities({
currentSession: watchTransaction?.sessionIndex ?? currentSession
@ -161,8 +149,8 @@ const Bridge = ({ chainId, address, config, connect }) => {
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location]);
ReactGA.send({ hitType: "pageview", page: "/bridge" });
}, []);
const chainExplorerUrl = useMemo(() => {
const client = config?.getClient();
@ -187,8 +175,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
return sum + countOnesInBigInt(bigIntValue);
}, 0);
const storedBlockNumber = watchTransactionInfo ? Number(watchTransactionInfo.blockNumber) : 0;
const finalization = Math.max(0, (finalityDelay + storedBlockNumber) - Number(blockNumber));
const finalization = Math.max(0, (finalityDelay + watchTransaction.blockNumber) - Number(blockNumber));
const applaused = transactionApplaused?.finalized ?? false;
const clappedAmount = transactionApplaused?.clapped_amount ?? 0n;
const clappedPercentage = clappedAmount * 100n / (totalStakedAmount ?? 1n);
@ -204,10 +191,9 @@ const Bridge = ({ chainId, address, config, connect }) => {
clapsPercentage,
}
}, [
watchTransaction,
watchTransactionInfo,
transactionApplaused,
finalityDelay,
watchTransaction,
blockNumber,
totalStakedAmount,
authorities
@ -219,20 +205,16 @@ const Bridge = ({ chainId, address, config, connect }) => {
const latestCommits = useMemo(() => {
return validators?.map((validator, index) => {
const lastUpdatedNumber = Number(blockCommitments?.at(index)?.last_updated ?? 0);
const timestampDelta = (latestBlockNumber - lastUpdatedNumber) * 6000; // ideal 6 seconds for block
const lastUpdatedTimestamp = Math.floor(currentTime - timestampDelta);
return {
validator: validator,
lastActive: timestampDelta,
lastUpdated: lastUpdatedTimestamp,
lastActive: currentTime - Number(blockCommitments?.at(index)?.last_updated ?? 0),
lastUpdated: blockCommitments?.at(index)?.last_updated,
lastStoredBlock: blockCommitments?.at(index)?.last_stored_block,
storedBlockTime: (blockCommitments?.at(index)?.last_stored_block ?? 0n) * networkAvgBlockSpeed(chainId),
disabled: disabledValidators?.includes(index),
}
})
}, [blockCommitments, disabledValidators, validators, latestBlockNumber, chainId]);
}, [blockCommitments, disabledValidators, validators, chainId]);
const latestUpdate = useMemo(() => {
const validCommits = latestCommits?.filter(commit =>
@ -288,7 +270,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
const removeStoredRecord = useCallback(() => {
const newStoredTransactions = storedTransactions.filter((_, index) => index !== activeTxIndex)
setStoredTransactions(newStoredTransactions);
setStorageValue(chainId, address, "bridge-txs", newStoredTransactions);
localStorage.setItem(storagePrefix, JSON.stringify(newStoredTransactions));
setActiveTxIndex(-1);
}, [storedTransactions, activeTxIndex, setStoredTransactions, setActiveTxIndex]);
@ -314,13 +296,13 @@ const Bridge = ({ chainId, address, config, connect }) => {
const newStoredTransactions = [transaction, ...storedTransactions];
setStoredTransactions(newStoredTransactions);
setStorageValue(chainId, address, "bridge-txs", newStoredTransactions);
setActiveTxIndex(0);
localStorage.setItem(STORAGE_PREFIX, JSON.stringify(newStoredTransactions));
setActiveTxIndex(0)
}
return (
<>
<PageTitle name="GHOST Bridge" subtitle={`(10, 10) Bridge ${ghstSymbol} into (10, 10) Stake²`} />
<PageTitle name="GHOST Bridge" subtitle={`Bridge $${ghstSymbol} via the only pure Web3 protocol`} />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",

View File

@ -28,8 +28,7 @@ import SwapCollection from "../../components/Swap/SwapCollection";
import { ghost } from "../../hooks/staking";
import { useBalance } from "../../hooks/tokens";
import CheckIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import CheckIcon from '@mui/icons-material/Check';
import HourglassBottomIcon from '@mui/icons-material/HourglassBottom';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
@ -59,7 +58,6 @@ export const BridgeCardAction = ({
storeTransactionHash,
}) => {
const [isPending, setIsPending] = useState(false);
const [inputFocused, setInputFocused] = useState(false);
const [receiver, setReceiver] = useState("");
const [convertedReceiver, setConvertedReceiver] = useState(undefined);
const [amount, setAmount] = useState("");
@ -168,17 +166,13 @@ export const BridgeCardAction = ({
UpperSwapCard={<SwapCard
id={`bridge-token-receiver`}
inputWidth={"100%"}
value={convertedReceiver ? "" : receiver}
onBlur={() => setInputFocused(true)}
onFocus={() => setInputFocused(false)}
onChange={event => setReceiver(event.currentTarget.value)}
value={convertedReceiver ? sliceString(receiver, 15, -10) : receiver}
onChange={event => setReceiver(convertedReceiver ? "" : event.currentTarget.value)}
inputProps={{ "data-testid": "fromInput" }}
placeholder={convertedReceiver ? sliceString(receiver, 15, -10) : "GHOST address (sf prefixed)"}
placeholder="GHOST address (sf prefixed)"
endString={convertedReceiver
? <GhostStyledIcon color="success" viewBox="0 0 25 25" component={CheckCircleIcon} />
: inputFocused && receiver !== ""
? <GhostStyledIcon color="error" viewBox="0 0 25 25" component={CancelIcon} />
: undefined
: undefined
}
type="text"
maxWidth="100%"
@ -252,7 +246,7 @@ export const BridgeCardAction = ({
loading={isPending}
onClick={() => ghostOrConnect()}
>
{address === "" ? "Connect" : isPending ? "Bridging..." : "Bridge" }
{address === "" ? "Connect" : "Bridge" }
</PrimaryButton>
</Box>
)
@ -313,7 +307,7 @@ export const BridgeCardHistory = ({
<Box display="flex" flexDirection="column" justifyContent="center">
<Typography variant="caption">
{formatCurrency(
new DecimalBigNumber(BigInt(obj.amount ?? "0"), 18).toString(),
new DecimalBigNumber(BigInt(obj.amount), 18).toString(),
isSemiSmallScreen ? 3 : 8,
ghstSymbol
)}

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect } from "react";
import { Box, Typography, Link, FormControlLabel, Checkbox, useTheme } from "@mui/material";
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
@ -19,12 +19,10 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
import Modal from "../../components/Modal/Modal";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { PrimaryButton, TertiaryButton, SecondaryButton } from "../../components/Button";
import { PrimaryButton, TertiaryButton } from "../../components/Button";
import { formatCurrency } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { GATEKEEPER_ADDRESSES } from "../../constants/addresses";
import { GHOST_CONNECT } from "../../constants/ecosystem";
export const BridgeModal = ({
providerDetail,
@ -52,12 +50,6 @@ export const BridgeModal = ({
});
};
const bridgeNumbers = () => {
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
const number = 1 + connectedNetworks * 3;
return `(${number}, ${number})`;
};
return (
<Modal
data-testid="transaction-details-modal"
@ -97,36 +89,15 @@ export const BridgeModal = ({
minHeight={"100px"}
>
<Box display="flex" gap="1.5rem" flexDirection="column" marginTop=".8rem">
{!providerDetail && <Box
width="90%"
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center"
backgroundColor="#1f4771"
sx={{
position: 'absolute',
top: '130px',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 1,
}}
>
<SecondaryButton
{!providerDetail && <Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<TertiaryButton
fullWidth
sx={{ marginTop: "0 !important", marginBottom: "0 !important" }}
onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}
onClick={() => window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}
>
Get GHOST Connect
</SecondaryButton>
</TertiaryButton>
</Box>}
<Box
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center"
sx={{ filter: providerDetail ? '' : 'blur(5px)' }}
>
{providerDetail && <Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
{currentRecord?.finalization > 0 && (
<>
<Box
@ -314,7 +285,7 @@ export const BridgeModal = ({
</Box>
</>
)}
</Box>
</Box>}
<Box display="flex" flexDirection="column" gap="5px" padding="0.6rem 0">
<Box display="flex" flexDirection="row" justifyContent="space-between">
@ -369,7 +340,7 @@ export const BridgeModal = ({
<Typography variant="body2">Bridged Amount:</Typography>
<Typography variant="body2">{formatCurrency(
new DecimalBigNumber(
BigInt(currentRecord && currentRecord.amount ? currentRecord.amount : "0"),
BigInt(currentRecord ? currentRecord.amount : "0"),
18
).toString(), 9, ghstSymbol)
}</Typography>
@ -386,21 +357,13 @@ export const BridgeModal = ({
</Box>
<Box display="flex" flexDirection="column" gap="5px">
{currentRecord && currentRecord.finalization < 1 && currentRecord.applaused && <PrimaryButton
fullWidth
loading={false}
onClick={() => removeStoredRecord()}
>
{`Get ${bridgeNumbers()} Stake\u00B2`}
</PrimaryButton>}
<TertiaryButton
<PrimaryButton
fullWidth
loading={false}
onClick={() => removeStoredRecord()}
>
Erase Record
</TertiaryButton>
</PrimaryButton>
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
This will permanently remove the bridge transaction record from the session storage, but it will not cancel the bridge transaction.
@ -436,12 +399,6 @@ export const BridgeConfirmModal = ({
>
<Box gap="20px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
<Box width="100%" display="flex" flexDirection="column" alignItems="start">
<Box>
<Typography variant="subtitle1">
You are bridging to GHOST Chain. We will guide you towards (10, 10) Stake<sup>2</sup> rewards right after that.
</Typography>
</Box>
<hr style={{ margin: "10px 0", width: "100%" }} />
<FormControlLabel
control={
<Checkbox
@ -477,7 +434,7 @@ export const BridgeConfirmModal = ({
</span>
}
/>
<hr style={{ margin: "10px 0", width: "100%" }} />
<hr style={{ margin: "10px 0", width: "100%" }} />
<FormControlLabel
control={
<Checkbox

View File

@ -33,23 +33,11 @@ const RouteHop = ({ theme, token, chainTokenName, arrowNeeded }) => {
sx={{ backgroundColor: theme.colors.gray[600] }}
borderRadius="6px"
paddingX="9px"
paddingY="2px"
paddingY="6.5px"
alignItems="center"
>
<Token
chainTokenName={chainTokenName}
name={token}
parentSx={{
"& svg:nth-of-type(2)": {
transform: "scale(0.52)",
}
}}
sx={{
transform: "scale(0.65)",
transformOrigin: "center"
}}
/>
<Typography fontSize="15px" lineHeight="24px" marginLeft="5px">
<Token chainTokenName={chainTokenName} name={token} sx={{ fontSize: "21px" }} />
<Typography fontSize="15px" lineHeight="24px" marginLeft="9px">
{token}
</Typography>
</Box>

View File

@ -25,8 +25,6 @@ import GhostStyledIcon from "../../components/Icon/GhostIcon";
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
import { PrimaryButton } from "../../components/Button";
import { GHOST_CONNECT } from "../../constants/ecosystem";
export const ValidatorTable = ({
currentTime,
currentBlock,
@ -110,7 +108,7 @@ export const ValidatorTable = ({
<Typography sx={{ textAlign: "center" }} variant="h6">GHOST Connect is not detected on your browser!</Typography>
<Typography sx={{ textAlign: "center" }} variant="body2">Download GHOST Connect browser extension for real-time visibility into validator status and related transaction risks.</Typography>
<Typography sx={{ textAlign: "center" }} variant="body2"><b>Important:</b> The GHOST Connect is optional, but be aware that your bridge transaction will succeed or fail irreversibly based on the condition of the validators.</Typography>
<PrimaryButton onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}>
<PrimaryButton onClick={() => window.open('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}>
Get GHOST Connect
</PrimaryButton>
</Box>

View File

@ -10,10 +10,9 @@ import {
useTheme,
} from "@mui/material";
import SettingsIcon from '@mui/icons-material/Settings';
import { useEffect, useMemo, useState, useCallback } from "react";
import { useEffect, useState } from "react";
import { useParams, useLocation, useSearchParams } from "react-router-dom";
import { Helmet } from "react-helmet";
import { isAddress } from "viem";
import ReactGA from "react-ga4";
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
@ -29,24 +28,18 @@ import {
UNISWAP_V2_FACTORY,
RESERVE_ADDRESSES,
FTSO_ADDRESSES,
EMPTY_ADDRESS,
WETH_ADDRESSES,
} from "../../constants/addresses";
import { AVAILABLE_DEXES } from "../../constants/dexes";
import { useLocalStorage } from "../../hooks/localstorage";
import { useTokenSymbol } from "../../hooks/tokens";
import { getTokenAddress } from "../../hooks/helpers";
import PoolContainer from "./PoolContainer";
import SwapContainer from "./SwapContainer";
import TokenModal from "./TokenModal";
const Dex = ({ chainId, address, connect, config }) => {
const Dex = ({ chainId, address, connect }) => {
const location = useLocation();
const pathname = useParams();
const theme = useTheme();
const { getStorageValue, setStorageValue } = useLocalStorage();
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
@ -59,51 +52,15 @@ const Dex = ({ chainId, address, connect, config }) => {
const [topTokenListOpen, setTopTokenListOpen] = useState(false);
const [bottomTokenListOpen, setBottomTokenListOpen] = useState(false);
const [secondsToWait, setSecondsToWait] = useState(() => getStorageValue(chainId, address, "dex-deadline", "60"));
const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "dex-slippage", "5"));
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "dex-decimals", "5"));
const [actualDestinationAddress, setActualDestinationAddress] = useState(() => getStorageValue(chainId, address, "dex-destination", address));
const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress);
const [secondsToWait, setSecondsToWait] = useState(localStorage.getItem("dex-deadline") || "60");
const [slippage, setSlippage] = useState(localStorage.getItem("dex-slippage") || "5");
const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("dex-decimals") || "5");
const [tokenAddressTop, setTokenAddressTop] = useState(EMPTY_ADDRESS);
const [tokenAddressTop, setTokenAddressTop] = useState(RESERVE_ADDRESSES[chainId]);
const [tokenAddressBottom, setTokenAddressBottom] = useState(FTSO_ADDRESSES[chainId]);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: tokenNameTopInner } = useTokenSymbol(chainId, tokenAddressTop);
const { symbol: tokenNameBottomInner } = useTokenSymbol(chainId, tokenAddressBottom);
const chainSymbol = useMemo(() => {
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
if (chainSymbol) return chainSymbol;
return "WTF";
}, [config, chainId])
const tokenNameTop = useMemo(() => {
if (chainSymbol && tokenAddressTop === EMPTY_ADDRESS) {
return chainSymbol;
}
return tokenNameTopInner;
}, [tokenAddressTop, tokenNameTopInner, chainSymbol]);
const tokenNameBottom = useMemo(() => {
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
if (chainSymbol && tokenAddressBottom === EMPTY_ADDRESS) {
return chainSymbol;
}
return tokenNameBottomInner;
}, [tokenAddressBottom, tokenNameBottomInner, config]);
const isWrapping = useMemo(() => {
const isNative = tokenAddressTop === EMPTY_ADDRESS;
const isWrappedNative = tokenAddressBottom === WETH_ADDRESSES[chainId];
return isNative && isWrappedNative;
}, [chainId, tokenAddressTop, tokenAddressBottom]);
const isUnwrapping = useMemo(() => {
const isWrappedNative = tokenAddressTop === WETH_ADDRESSES[chainId];
const isNative = tokenAddressBottom === EMPTY_ADDRESS;
return isNative && isWrappedNative;
}, [chainId, tokenAddressTop, tokenAddressBottom]);
const { symbol: tokenNameTop } = useTokenSymbol(chainId, tokenAddressTop);
const { symbol: tokenNameBottom } = useTokenSymbol(chainId, tokenAddressBottom);
useEffect(() => {
if (currentQueryParameters.has("pool")) {
@ -118,8 +75,8 @@ const Dex = ({ chainId, address, connect, config }) => {
setTokenAddressTop(currentQueryParameters.get("from"));
newQueryParameters.set("from", currentQueryParameters.get("from"));
} else {
setTokenAddressTop(EMPTY_ADDRESS);
newQueryParameters.set("from", EMPTY_ADDRESS);
setTokenAddressTop(RESERVE_ADDRESSES[chainId]);
newQueryParameters.set("from", RESERVE_ADDRESSES[chainId]);
}
if (currentQueryParameters.has("to")) {
@ -135,7 +92,7 @@ const Dex = ({ chainId, address, connect, config }) => {
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname + location.search });
}, [location]);
}, [location])
const dexAddresses = {
router: UNISWAP_V2_ROUTER[chainId],
@ -156,7 +113,7 @@ const Dex = ({ chainId, address, connect, config }) => {
}
const changeSwapTab = (swap) => {
if (swap || (isWrapping || isUnwrapping)) newQueryParameters.delete("pool");
if (swap) newQueryParameters.delete("pool");
else newQueryParameters.set("pool", true);
newQueryParameters.set("from", currentQueryParameters.get("from"));
newQueryParameters.set("to", currentQueryParameters.get("to"));
@ -185,45 +142,24 @@ const Dex = ({ chainId, address, connect, config }) => {
setSearchParams(newQueryParameters);
}
const isDexFork = useMemo(() => {
const dexInfo = AVAILABLE_DEXES[chainId]?.find(elem =>
elem.name.toLowerCase() == pathname.name.toLowerCase()
)
return dexInfo ? dexInfo.isFork : false;
}, [chainId, pathname])
const setSlippageInner = useCallback((value) => {
const setSlippageInner = (value) => {
const maybeValue = parseFloat(value);
if (!maybeValue || parseFloat(value) <= 100) {
setSlippage(value);
setStorageValue(chainId, address, "dex-slippage", value);
localStorage.setItem("dex-slippage", value);
}
}, [chainId, address]);
}
const setSecondsToWaitInner = useCallback((value) => {
const setSecondsToWaitInner = (value) => {
localStorage.setItem("dex-deadline", value);
setSecondsToWait(value);
setStorageValue(chainId, address, "dex-deadline", value);
}, [chainId, address]);
}
const setFormatDecimalsInner = useCallback((value) => {
const setFormatDecimalsInner = (value) => {
if (Number(value) <= 17) {
localStorage.setItem("dex-decimals", value);
setFormatDecimals(value);
setStorageValue(chainId, address, "dex-decimals", value);
}
}, [chainId, address]);
const setDestinationAddressInner = useCallback((value) => {
const cleanedValue = value.trim();
setDestinationAddress(value);
if (isAddress(cleanedValue)) {
setActualDestinationAddress(value);
setStorageValue(chainId, address, "dex-destination", value);
}
}, [chainId, address]);
const handleCloseSetting = () => {
setDestinationAddress(undefined);
handleSettingsOpen(false);
}
return (
@ -249,8 +185,8 @@ const Dex = ({ chainId, address, connect, config }) => {
</Helmet>
<PageTitle
name="DEX Swap"
subtitle={`(3, 3) Swap ${chainSymbol} for ${ftsoSymbol} and ${ftsoSymbol}-${chainSymbol} LP powered by ${pathname.name.charAt(0).toUpperCase() + pathname.name.slice(1).toLowerCase()} V2${isDexFork ? " Fork" : ""}`}
name={`${pathname.name.charAt(0).toUpperCase() + pathname.name.slice(1).toLowerCase()} V2 Classic`}
subtitle="Swap via Uniswap V2 Fork"
/>
<Container
style={{
@ -263,11 +199,11 @@ const Dex = ({ chainId, address, connect, config }) => {
}}
>
<Modal
maxWidth="450px"
maxWidth="376px"
minHeight="200px"
open={settingsOpen}
headerText={"Settings"}
onClose={() => handleCloseSetting()}
onClose={() => handleSettingsOpen(false)}
>
<Box>
<InputLabel htmlFor="slippage">Slippage</InputLabel>
@ -331,35 +267,9 @@ const Dex = ({ chainId, address, connect, config }) => {
</Typography>
</Box>
</Box>
<Box mt="32px">
<InputLabel htmlFor="recipient">
{`${actualDestinationAddress ? "Custom" : "Default"} destination address`}
</InputLabel>
<Box mt="8px">
<FormControl variant="outlined" color="primary" fullWidth>
<OutlinedInput
inputProps={{ "data-testid": "decimals-to-wait" }}
type="text"
id="destination-to-wait"
value={destinationAddress
? destinationAddress
: actualDestinationAddress ?? address
}
onChange={event => setDestinationAddressInner(event.currentTarget.value)}
/>
</FormControl>
</Box>
<Box mt="8px">
<Typography variant="body2" color="textSecondary">
Recipient address of swapped assets and liquidity tokens
</Typography>
</Box>
</Box>
</Modal>
<TokenModal
chainSymbol={chainSymbol}
account={address}
chainId={chainId}
listOpen={topTokenListOpen}
@ -367,7 +277,6 @@ const Dex = ({ chainId, address, connect, config }) => {
setTokenAddress={setInnerTokenAddressTop}
/>
<TokenModal
chainSymbol={chainSymbol}
account={address}
chainId={chainId}
listOpen={bottomTokenListOpen}
@ -405,7 +314,6 @@ const Dex = ({ chainId, address, connect, config }) => {
<Box height="350px">
{isSwap ?
<SwapContainer
chainSymbol={chainSymbol}
tokenNameTop={tokenNameTop}
tokenNameBottom={tokenNameBottom}
onCardsSwap={onCardsSwap}
@ -414,18 +322,14 @@ const Dex = ({ chainId, address, connect, config }) => {
dexAddresses={dexAddresses}
connect={connect}
slippage={slippage}
destination={actualDestinationAddress ? actualDestinationAddress : address}
secondsToWait={secondsToWait}
setTopTokenListOpen={setTopTokenListOpen}
setBottomTokenListOpen={setBottomTokenListOpen}
setIsSwap={setIsSwap}
formatDecimals={formatDecimals}
isWrapping={isWrapping}
isUnwrapping={isUnwrapping}
/>
:
<PoolContainer
chainSymbol={chainSymbol}
tokenNameTop={tokenNameTop}
tokenNameBottom={tokenNameBottom}
onCardsSwap={onCardsSwap}
@ -434,7 +338,6 @@ const Dex = ({ chainId, address, connect, config }) => {
dexAddresses={dexAddresses}
connect={connect}
slippage={slippage}
destination={actualDestinationAddress ? actualDestinationAddress : address}
secondsToWait={secondsToWait}
setTopTokenListOpen={setTopTokenListOpen}
setBottomTokenListOpen={setBottomTokenListOpen}

View File

@ -9,18 +9,12 @@ import { TokenAllowanceGuard } from "../../components/TokenAllowanceGuard/TokenA
import { SecondaryButton } from "../../components/Button";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { formatNumber, formatCurrency, bigIntSqrt } from "../../helpers";
import { formatNumber, formatCurrency } from "../../helpers";
import { useBalance, useTotalSupply } from "../../hooks/tokens";
import {
useUniswapV2Pair,
useUniswapV2PairReserves,
addLiquidity,
addLiquidityETH,
} from "../../hooks/uniswapv2";
import { useUniswapV2Pair, useUniswapV2PairReserves, addLiquidity } from "../../hooks/uniswapv2";
const PoolContainer = ({
chainSymbol,
tokenNameTop,
tokenNameBottom,
onCardsSwap,
@ -29,11 +23,10 @@ const PoolContainer = ({
dexAddresses,
connect,
slippage,
destination,
secondsToWait,
setTopTokenListOpen,
setBottomTokenListOpen,
formatDecimals,
formatDecimals
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 456px)");
@ -46,13 +39,11 @@ const PoolContainer = ({
balance: balanceTop,
refetch: balanceRefetchTop,
contractAddress: addressTop,
isNative: topIsNative,
} = useBalance(chainId, tokenNameTop, address);
const {
balance: balanceBottom,
refetch: balanceRefetchBottom,
contractAddress: addressBottom,
isNative: bottomIsNative,
} = useBalance(chainId, tokenNameBottom, address);
const {
@ -84,6 +75,34 @@ const PoolContainer = ({
const setMaxTop = () => setAmountTop(balanceTop.toString());
const setMaxBottom = () => setAmountBottom(balanceBottom.toString());
const bigIntSqrt = (n) => {
if (n < 0n) {
throw new Error("Cannot compute the square root of a negative number.");
}
if (n < 2n) {
return n; // The square root of 0 or 1 is the number itself
}
let low = 0n;
let high = n;
let mid;
while (low <= high) {
mid = (low + high) / 2n;
const midSquared = mid * mid;
if (midSquared === n) {
return mid; // Found the exact square root
} else if (midSquared < n) {
low = mid + 1n; // Move to the right half
} else {
high = mid - 1n; // Move to the left half
}
}
return high; // The integer part of the square root
}
const estimatedAmountOut = useMemo(() => {
const pairReserves0 = addressTop.toUpperCase() === tokenAddresses.token0.toUpperCase()
? pairReserves.reserve0
@ -132,40 +151,14 @@ const PoolContainer = ({
}
}
}, [addressBottom, balanceTop, balanceBottom, amountTop, amountBottom, tokenAddresses, pairReserves]);
const poolShares = useMemo(() => {
const hundred = new DecimalBigNumber(1n, 2);
if (lpTotalSupply?._value == 0n) {
return { currentShares: lpTotalSupply, nextShares: lpTotalSupply };
}
const currentShares = lpBalance.div(lpTotalSupply).div(hundred);
const nextShares = (lpBalance.add(estimatedAmountOut)).div(lpTotalSupply.add(estimatedAmountOut)).div(hundred);
return { currentShares, nextShares };
}, [lpBalance, lpTotalSupply, estimatedAmountOut]);
const poolPrices = useMemo(() => {
const amountIn = addressTop.toUpperCase() === tokenAddresses.token0.toUpperCase()
? pairReserves.reserve0
: pairReserves.reserve1;
const amountOut = addressBottom.toUpperCase() === tokenAddresses.token1.toUpperCase()
? pairReserves.reserve1
: pairReserves.reserve0;
let priceIn = "0";
let priceOut = "0";
if (amountIn?._value > 0n) priceIn = (amountOut.div(amountIn)).toString();
if (amountOut?._value > 0n) priceOut = (amountIn.div(amountOut)).toString();
return { priceIn , priceOut }
}, [addressTop, addressBottom, balanceTop, tokenAddresses, pairReserves]);
}, [addressBottom, balanceTop, balanceBottom, amountTop, amountBottom, tokenAddresses, pairReserves])
const addLiquidityInner = async () => {
setIsPending(true);
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
const destination = address;
const shares = 100000;
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
@ -178,7 +171,7 @@ const PoolContainer = ({
const amountAMin = amountADesired * bigIntSlippage / one;
const amountBMin = amountBDesired * bigIntSlippage / one;
const params = {
await addLiquidity(
chainId,
tokenNameTop,
tokenNameBottom,
@ -186,16 +179,9 @@ const PoolContainer = ({
amountBDesired,
amountAMin,
amountBMin,
address,
destination,
deadline,
}
if (topIsNative || bottomIsNative) {
await addLiquidityETH(params)
} else {
await addLiquidity(params);
}
);
await balanceRefetchTop();
await balanceRefetchBottom();
@ -220,8 +206,7 @@ const PoolContainer = ({
UpperSwapCard={
<SwapCard
id="from"
token={tokenNameTop}
chainTokenName={tokenNameTop !== chainSymbol && chainSymbol}
token={<TokenStack tokens={[tokenNameTop]} sx={{ fontSize: "21px" }} />}
tokenName={tokenNameTop}
info={(!isSmallScreen ? "Balance: " : "") + formatCurrency(balanceTop, formatDecimals, tokenNameTop)}
endString="Max"
@ -235,8 +220,7 @@ const PoolContainer = ({
LowerSwapCard={
<SwapCard
id="to"
token={tokenNameBottom}
chainTokenName={tokenNameBottom !== chainSymbol && chainSymbol}
token={<TokenStack tokens={[tokenNameBottom]} sx={{ fontSize: "21px" }} />}
tokenName={tokenNameBottom}
value={amountBottom}
onChange={event => emptyPool ? setAmountBottom(event.currentTarget.value) : {}}
@ -249,7 +233,7 @@ const PoolContainer = ({
}
arrowOnClick={onSwap}
/>
<Box
{!isSmallScreen && <Box
m="10px 0"
display="flex"
flexDirection="column"
@ -260,29 +244,24 @@ const PoolContainer = ({
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameTop}`}</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(poolPrices.priceIn, formatDecimals, tokenNameBottom)}</Typography>
<Typography fontSize="12px" lineHeight="15px">Current Balance:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatNumber(lpBalance, formatDecimals)} LP</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameBottom}`}</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(poolPrices.priceOut, formatDecimals, tokenNameTop)}</Typography>
<Typography fontSize="12px" lineHeight="15px">Total Supply:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatNumber(lpTotalSupply, formatDecimals)} LP</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">Current Pool Share:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatNumber(poolShares.currentShares, formatDecimals)}%</Typography>
<Typography fontSize="12px" lineHeight="15px">Extra Balance:</Typography>
<Typography fontSize="12px" lineHeight="15px">~{formatNumber(estimatedAmountOut, formatDecimals)} LP</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">Next Pool Share:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatNumber(poolShares.nextShares, formatDecimals)}%</Typography>
</Box>
</Box>
</Box>}
<TokenAllowanceGuard
spendAmount={new DecimalBigNumber(amountTop, balanceTop._decimals)}
tokenName={tokenNameTop}
owner={address}
spender={dexAddresses.router}
decimals={balanceTop._decimals}
isNative={topIsNative}
approvalText={"Approve " + tokenNameTop}
approvalPendingText={"Approving..."}
connect={connect}
@ -295,7 +274,6 @@ const PoolContainer = ({
owner={address}
spender={dexAddresses.router}
decimals={balanceBottom._decimals}
isNative={bottomIsNative}
approvalText={"Approve " + tokenNameBottom}
approvalPendingText={"Approving..."}
connect={connect}
@ -320,9 +298,9 @@ const PoolContainer = ({
"Connect"
:
pairAddress === "0x0000000000000000000000000000000000000000" ?
isPending ? "Creating Pool..." : "Create Pool"
"Create Pool"
:
isPending ? "Adding Liquidity..." : "Add Liquidity"
"Add Liquidity"
}
</SecondaryButton>
</TokenAllowanceGuard>

View File

@ -1,4 +1,4 @@
import { useState, useMemo, useEffect } from "react";
import { useState, useEffect } from "react";
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
import toast from "react-hot-toast";
@ -13,19 +13,10 @@ import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { prettifySecondsInDays } from "../../helpers/timeUtil";
import { getTokenAddress } from "../../hooks/helpers";
import { useBalance, depositNative, withdrawWeth } from "../../hooks/tokens";
import {
useUniswapV2Pair,
useUniswapV2PairReserves,
swapExactTokensForTokens,
swapExactETHForTokens,
swapExactTokensForETH,
} from "../../hooks/uniswapv2";
import { EMPTY_ADDRESS } from "../../constants/addresses";
import { useBalance } from "../../hooks/tokens";
import { useUniswapV2Pair, useUniswapV2PairReserves, swapExactTokensForTokens } from "../../hooks/uniswapv2";
const SwapContainer = ({
chainSymbol,
tokenNameTop,
tokenNameBottom,
address,
@ -36,12 +27,9 @@ const SwapContainer = ({
setTopTokenListOpen,
setBottomTokenListOpen,
slippage,
destination,
secondsToWait,
setIsSwap,
formatDecimals,
isWrapping,
isUnwrapping,
formatDecimals
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 456px)");
@ -57,14 +45,12 @@ const SwapContainer = ({
balance: balanceTop,
refetch: balanceRefetchTop,
contractAddress: addressTop,
isNative: topIsNative,
} = useBalance(chainId, tokenNameTop, address);
const {
balance: balanceBottom,
refetch: balanceRefetchBottom,
contractAddress: addressBottom,
isNative: bottomIsNative,
} = useBalance(chainId, tokenNameBottom, address);
const {
@ -88,80 +74,42 @@ const SwapContainer = ({
const setMax = () => setAmountTop(balanceTop.toString());
useEffect(() => {
if (isWrapping || isUnwrapping) {
setAmountBottom(amountTop.toString());
setNextPrice("1");
setCurrentPrice("1");
return;
}
const zero = new DecimalBigNumber(0n, 0);
const raw = new DecimalBigNumber(amountTop, balanceTop._decimals);
const amountInRaw = new DecimalBigNumber(raw._value.toBigInt(), balanceTop._decimals);
const amountInWithFee = amountInRaw.mul(new DecimalBigNumber(997n, 3));
const topAddress = getTokenAddress(chainId, tokenNameTop);
const bottomAddress = getTokenAddress(chainId, tokenNameBottom);
const amountIn = addressTop.toUpperCase() === tokenAddresses.token0.toUpperCase() ? pairReserves.reserve0 : pairReserves.reserve1;
const amountOut = addressBottom.toUpperCase() === tokenAddresses.token1.toUpperCase() ? pairReserves.reserve1 : pairReserves.reserve0;
if (amountOut.eq(zero)) {
if (amountIn.eq(zero)) {
setCurrentPrice("");
} else {
setCurrentPrice(amountIn.div(amountOut).toString());
}
if (amountOut.eq(zero) || amountInWithFee.eq(zero)) {
if (amountIn.eq(zero) || amountInWithFee.eq(zero)) {
setAmountBottom("");
setNextPrice("");
} else {
const nominator = amountOut.mul(amountInWithFee);
const nominator = amountOut.mul(amountIn);
const denominator = amountIn.add(amountInWithFee);
const newAmountOut = nominator.div(denominator);
const newReserveIn = amountIn.add(amountInWithFee);
const newReserveOut = amountOut.sub(newAmountOut);
const nextPrice = newReserveIn.div(newReserveOut);
setAmountBottom(newAmountOut.toString());
setNextPrice(nextPrice.toString());
setAmountBottom(amountOut.sub(newAmountOut).toString());
setNextPrice(denominator.div(newAmountOut).toString())
}
}, [pairReserves, addressBottom, amountTop, addressTop, isWrapping, isUnwrapping]);
const minReceived = useMemo(() => {
const decimals = 7;
const shares = Math.pow(10, decimals);
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
const bigIntSlippage = one - BigInt(Math.round(floatSlippage * shares));
const slippageDecimalBigNumber = new DecimalBigNumber(bigIntSlippage, 2);
const bigIntAmount = BigInt(Math.round(amountBottom * shares));
const amountDecimalBigNumber = new DecimalBigNumber(bigIntAmount, decimals);
const tmpResult = amountDecimalBigNumber.mul(slippageDecimalBigNumber);
const result = new DecimalBigNumber(tmpResult?._value, tmpResult?._decimals + decimals);
return result?.toString();
}, [amountBottom, amountBottom, slippage, balanceBottom]);
const buttonText = useMemo(() => {
let text = "Swap";
if (isWrapping) text = "Wrap";
else if (isUnwrapping) text = "Unwrap";
else if (pairAddress === EMPTY_ADDRESS) text = "Create Pool";
if (isPending) text = `${text}ping...`
if (
!(isWrapping || isUnwrapping) &&
(pairAddress === EMPTY_ADDRESS && isPending)
) text = "Creating Pool..."
return text;
}, [isPending, isWrapping, isUnwrapping, pairAddress]);
}, [amountTop, addressTop]);
const swapTokens = async () => {
setIsPending(true);
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
const destination = address;
const shares = 100000;
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
@ -173,30 +121,14 @@ const SwapContainer = ({
const amountBDesired = BigInt(Math.round(parseFloat(amountBottom) * Math.pow(10, balanceBottom._decimals)));
const amountBMin = amountBDesired * bigIntSlippage / one;
if (isWrapping) {
await depositNative(chainId, address, amountADesired);
} else if (isUnwrapping) {
await withdrawWeth(chainId, address, amountADesired);
} else {
const params = {
chainId,
amountADesired,
amountBMin,
tokenNameTop,
tokenNameBottom,
destination,
address,
deadline
};
if (topIsNative) {
await swapExactETHForTokens(params)
} else if (bottomIsNative) {
await swapExactTokensForETH(params)
} else {
await swapExactTokensForTokens(params);
}
}
await swapExactTokensForTokens(
chainId,
amountADesired,
amountBMin,
[tokenNameTop, tokenNameBottom],
destination,
deadline
);
await balanceRefetchTop();
await balanceRefetchBottom();
@ -213,8 +145,7 @@ const SwapContainer = ({
<SwapCard
maxWidth="356px"
id="from"
token={tokenNameTop}
chainTokenName={tokenNameTop !== chainSymbol && chainSymbol}
token={<TokenStack tokens={[tokenNameTop]} sx={{ fontSize: "21px" }} />}
tokenName={tokenNameTop}
info={
(!isSmallScreen ? "Balance: " : "") +
@ -232,8 +163,8 @@ const SwapContainer = ({
<SwapCard
maxWidth="356px"
id="to"
token={tokenNameBottom}
chainTokenName={tokenNameBottom !== chainSymbol && chainSymbol}
token={<TokenStack tokens={[tokenNameBottom]} sx={{ fontSize: "21px" }} />}
tokenName={tokenNameBottom}
value={amountBottom}
inputProps={{ "data-testid": "toInput" }}
tokenOnClick={() => setBottomTokenListOpen(true)}
@ -245,7 +176,7 @@ const SwapContainer = ({
}
arrowOnClick={onSwap}
/>
<Box
{!isSmallScreen && <Box
m="10px 0"
display="flex"
flexDirection="column"
@ -256,29 +187,24 @@ const SwapContainer = ({
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameBottom} (Current)`}</Typography>
<Typography fontSize="12px" lineHeight="15px">Current price:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(currentPrice, formatDecimals, tokenNameTop)}</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameBottom} (Next)`}</Typography>
<Typography fontSize="12px" lineHeight="15px">Next price:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(nextPrice === "" ? currentPrice : nextPrice, formatDecimals, tokenNameTop)}</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">Min. Receive:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(minReceived, formatDecimals, tokenNameBottom)}</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">Tx. Deadline:</Typography>
<Typography fontSize="12px" lineHeight="15px">Transaction deadline:</Typography>
<Typography fontSize="12px" lineHeight="15px">~{prettifySecondsInDays(secondsToWait)}</Typography>
</Box>
</Box>
</Box>}
<TokenAllowanceGuard
spendAmount={new DecimalBigNumber(amountTop, balanceTop._decimals)}
tokenName={tokenNameTop}
owner={address}
spender={dexAddresses.router}
decimals={balanceTop._decimals}
isNative={topIsNative}
approvalText={"Approve " + tokenNameTop}
approvalPendingText={"Approving..."}
connect={connect}
@ -298,10 +224,17 @@ const SwapContainer = ({
onClick={() => address === "" ?
connect()
:
(!isWrapping && !isUnwrapping) && pairAddress === EMPTY_ADDRESS ? setIsSwap(false) : swapTokens()
pairAddress === "0x0000000000000000000000000000000000000000" ? setIsSwap(false) : swapTokens()
}
>
{address === "" ? "Connect" : buttonText }
{address === "" ?
"Connect"
:
pairAddress === "0x0000000000000000000000000000000000000000" ?
"Create Pool"
:
"Swap"
}
</SecondaryButton>
</TokenAllowanceGuard>
</Box>

View File

@ -15,19 +15,15 @@ import { isAddress, getAddress } from "@ethersproject/address";
import Modal from "../../components/Modal/Modal";
import SwapCard from "../../components/Swap/SwapCard";
import Token from "../../components/Token/Token";
import TokenStack from "../../components/TokenStack/TokenStack";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { formatNumber } from "../../helpers/";
import { useBalance, useTokenSymbol } from "../../hooks/tokens";
import {
RESERVE_ADDRESSES,
FTSO_ADDRESSES,
GHST_ADDRESSES,
EMPTY_ADDRESS
} from "../../constants/addresses";
import { isNetworkLegacy } from "../../constants";
import { RESERVE_ADDRESSES, FTSO_ADDRESSES, STNK_ADDRESSES, GHST_ADDRESSES } from "../../constants/addresses";
const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setTokenAddress }) => {
const TokenModal = ({ chainId, account, listOpen, setListOpen, setTokenAddress }) => {
const isSmallScreen = useMediaQuery("(max-width: 599px)");
const isVerySmallScreen = useMediaQuery("(max-width: 425px)");
@ -43,15 +39,19 @@ const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setT
const { symbol: searchSymbol } = useTokenSymbol(chainId, address);
const { balance: searchBalance } = useBalance(chainId, address, account);
const { balance: nativeBalance } = useBalance(chainId, chainSymbol, account);
const { balance: reserveBalance } = useBalance(chainId, "RESERVE", account);
const { balance: ftsoBalance } = useBalance(chainId, "FTSO", account);
const { balance: stnkBalance } = useBalance(chainId, "STNK", account);
const { balance: ghstBalance } = useBalance(chainId, "GHST", account);
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const config = useConfig();
const nativeSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
const searchToken = useMemo(() => {
return [{
name: searchSymbol,
@ -63,30 +63,32 @@ const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setT
const knownTokens = useMemo(() => {
return [
{
name: chainSymbol,
balance: nativeBalance,
address: EMPTY_ADDRESS,
},
{
name: reserveSymbol,
icons: isNetworkLegacy(chainId) ? ["GDAI"] : [nativeSymbol],
balance: reserveBalance,
address: RESERVE_ADDRESSES[chainId]
},
{
name: ftsoSymbol,
icons: ["FTSO"],
balance: ftsoBalance,
chainTokenName: chainSymbol,
address: FTSO_ADDRESSES[chainId]
},
{
name: stnkSymbol,
icons: ["STNK"],
balance: stnkBalance,
address: STNK_ADDRESSES[chainId]
},
{
name: ghstSymbol,
icons: ["GHST"],
balance: ghstBalance,
chainTokenName: chainSymbol,
address: GHST_ADDRESSES[chainId]
}
]
}, [reserveSymbol, ftsoSymbol, ghstSymbol, reserveBalance, ftsoBalance, ghstBalance]);
}, [reserveSymbol, ftsoSymbol, stnkSymbol, ghstSymbol, reserveBalance, ftsoBalance, stnkBalance, ghstBalance]);
useEffect(() => {
if (isAddress(userInput)) {
@ -103,7 +105,7 @@ const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setT
maxWidth="476px"
minHeight="200px"
open={listOpen}
headerText={"Select a Token"}
headerText={"Select a token"}
onClose={() => setListOpen(false)}
>
<SwapCard
@ -130,18 +132,10 @@ const TokenModal = ({ chainId, account, chainSymbol, listOpen, setListOpen, setT
return (
<TableRow onClick={() => setUsedAddress(token)} hover key={index} id={index + `--token`} data-testid={index + `--token`}>
<TableCell style={{ padding: "8px 0" }}>
<Token
chainTokenName={token.chainTokenName}
name={token.name}
parentSx={{
"& svg:nth-of-type(2)": {
transform: "translate(-5%, 10%) scale(0.8)",
}
}}
/>
<TokenStack style={{ width: "32px", height: "32px" }} tokens={token.icons} />
</TableCell>
<TableCell style={{ padding: "8px", height: "32px" }}>
<TableCell style={{ padding: "8px 0", height: "32px" }}>
<Typography>{token.name}</Typography>
</TableCell>

View File

@ -2,7 +2,6 @@ import { useState, useEffect, useMemo } from "react";
import { Box, Container, Typography, useMediaQuery } from "@mui/material";
import { useConfig, useBalance } from "wagmi";
import { Helmet } from "react-helmet";
import { useLocation } from "react-router-dom";
import ReactGA from "react-ga4";
import PageTitle from "../../components/PageTitle/PageTitle";
@ -27,8 +26,6 @@ import {
} from "../../hooks/tokens";
const Faucet = ({ chainId, address, config, connect }) => {
const location = useLocation();
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
@ -57,8 +54,8 @@ const Faucet = ({ chainId, address, config, connect }) => {
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location])
ReactGA.send({ hitType: "pageview", page: "/faucet" });
}, [])
useEffect(() => {
const value = nativeBalance ? nativeBalance.value : 0n;

View File

@ -1,6 +1,6 @@
import { useEffect } from "react";
import ReactGA from "react-ga4";
import { useNavigate, useParams, useLocation } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Box, Container, Grid, Divider, Typography, useMediaQuery } from "@mui/material";
@ -15,23 +15,24 @@ import { ProposalsCount, MinQuorumPercentage, ProposalThreshold } from "./compon
import { useTokenSymbol } from "../../hooks/tokens";
const Governance = ({ connect, config, address, chainId }) => {
const location = useLocation();
const navigate = useNavigate();
const { network } = useParams();
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const navigate = useNavigate();
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const handleModal = () => {
const navigate = useNavigate();
}
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location]);
ReactGA.send({ hitType: "pageview", page: "/governance" });
}, []);
return (
<Box>
<PageTitle name="Protocol Governance" subtitle={`Vote with ${ghstSymbol} on DAO proposals`} />
<PageTitle name="Protocol Governance" subtitle={`Cast $${ghstSymbol} to facilitate DAO decisions`} />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
@ -69,7 +70,7 @@ const Governance = ({ connect, config, address, chainId }) => {
<Box mt="45px" display="flex" flexDirection="column" alignItems="center" justifyContent="center">
<PrimaryButton
fullWidth
onClick={() => navigate(`/${network}/governance/create`)}
onClick={() => navigate(`/governance/create`)}
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
>
Create Proposal

View File

@ -1,5 +1,4 @@
import { useState, useMemo, useCallback, useEffect } from "react";
import { useLocation } from 'react-router-dom';
import ReactGA from "react-ga4";
import {
@ -29,7 +28,6 @@ import PageTitle from "../../components/PageTitle/PageTitle";
import { PrimaryButton, TertiaryButton } from "../../components/Button";
import { TokenAllowanceGuard } from "../../components/TokenAllowanceGuard/TokenAllowanceGuard";
import { useLocalStorage } from "../../hooks/localstorage";
import { useTokenSymbol, useBalance } from "../../hooks/tokens";
import { useProposalThreshold, useProposalHash, propose } from "../../hooks/governance";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
@ -39,17 +37,16 @@ import { parseFunctionCalldata } from "./components/functions/index";
import { MY_PROPOSALS_PREFIX } from "./helpers";
const NewProposal = ({ config, address, connect, chainId }) => {
const location = useLocation();
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const theme = useTheme();
const { getStorageValue, setStorageValue } = useLocalStorage();
const myStoredProposals = getStorageValue(chainId, address, MY_PROPOSALS_PREFIX, []);
const [myProposals, setMyProposalsInner] = useState(() => myStoredProposals.map(id => BigInt(id)));
const myStoredProposals = localStorage.getItem(MY_PROPOSALS_PREFIX);
const [myProposals, setMyProposals] = useState(
myStoredProposals ? JSON.parse(myStoredProposals).map(id => BigInt(id)) : []
);
const [isPending, setIsPending] = useState(false);
const [isModalOpened, setIsModalOpened] = useState(false);
@ -64,14 +61,14 @@ const NewProposal = ({ config, address, connect, chainId }) => {
proposalDescription
} = useProposalHash(chainId, proposalFunctions);
const setMyProposals = useCallback((proposals) => {
setMyProposalsInner(proposals);
setStorageValue(chainId, address, MY_PROPOSALS_PREFIX, proposals.map(id => id.toString()))
}, [chainId, address]);
useEffect(() => {
const toStore = JSON.stringify(myProposals.map(id => id.toString()));
localStorage.setItem(MY_PROPOSALS_PREFIX, toStore);
}, [myProposals]);
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location]);
ReactGA.send({ hitType: "pageview", page: "/governance/create" });
}, []);
const addCalldata = (calldata) => setProposalFunctions(prev => [...prev, calldata]);
const removeCalldata = (index) => setProposalFunctions(prev => prev.filter((_, i) => i !== index));
@ -107,7 +104,7 @@ const NewProposal = ({ config, address, connect, chainId }) => {
closeModal={() => setIsModalOpened(false)}
/>
<Box>
<PageTitle name="Create Proposal" subtitle="Submit proposals to strengthen the DAO" />
<PageTitle name="Create Proposal" subtitle="Submit your proposal to strengthen the DAO" />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
@ -166,14 +163,6 @@ const NewProposal = ({ config, address, connect, chainId }) => {
isVertical
>
<Box display="flex" flexDirection="column" alignItems="center">
<TertiaryButton
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
fullWidth
disabled={isPending}
onClick={() => setIsModalOpened(true)}
>
Add New Function
</TertiaryButton>
<PrimaryButton
disabled={
proposalFunctions.length === 0 ||
@ -181,11 +170,18 @@ const NewProposal = ({ config, address, connect, chainId }) => {
isPending
}
fullWidth
loading={isPending}
onClick={() => submitProposal()}
>
{isPending ? "Submitting..." : "Submit Proposal"}
</PrimaryButton>
<TertiaryButton
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
fullWidth
disabled={isPending}
onClick={() => setIsModalOpened(true)}
>
Add New
</TertiaryButton>
</Box>
</TokenAllowanceGuard>
</Box>
@ -204,41 +200,21 @@ const NewProposal = ({ config, address, connect, chainId }) => {
</Box>
}
>
{isSmallScreen
? <Box display="flex" flexDirection="column">
{proposalFunctions?.map((metadata, index) => {
return parseFunctionCalldata({
metadata,
index,
chainId,
removeCalldata,
nativeCoin: nativeCurrency,
isTable: false,
});
})}
</Box>
: <TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell align="center" style={{ padding: "8px 0" }}>Function</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Target</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Calldata</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>{proposalFunctions?.map((metadata, index) => {
return parseFunctionCalldata({
metadata,
index,
chainId,
removeCalldata,
nativeCoin: nativeCurrency,
});
})}</TableBody>
</Table>
</TableContainer>
}
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell align="center" style={{ padding: "8px 0" }}>Function</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Target</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Calldata</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>{proposalFunctions.map((metadata, index) => {
return parseFunctionCalldata(metadata, index, chainId, nativeCurrency, removeCalldata);
})}</TableBody>
</Table>
</TableContainer>
</Paper>}
</Box>
</Container>

View File

@ -1,8 +1,7 @@
import { useEffect, useState, useMemo, useCallback } from "react";
import { useEffect, useState, useMemo } from "react";
import ReactGA from "react-ga4";
import { useParams, useLocation } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useBlock, useBlockNumber } from "wagmi";
import { useParams } from 'react-router-dom';
import { useBlock, useBlockNumber } from 'wagmi'
import {
Box,
@ -33,16 +32,13 @@ import { formatNumber, formatCurrency, shorten } from "../../helpers";
import { prettifySecondsInDays } from "../../helpers/timeUtil";
import { DecimalBigNumber, } from "../../helpers/DecimalBigNumber";
import { convertStatusToColor, convertStatusToLabel } from "./helpers";
import { convertStatusToTemplate, convertStatusToLabel } from "./helpers";
import { parseFunctionCalldata } from "./components/functions";
import { networkAvgBlockSpeed } from "../../constants";
import { useLocalStorage } from "../../hooks/localstorage";
import { useTokenSymbol, usePastTotalSupply, usePastVotes, useBalance } from "../../hooks/tokens";
import {
getVoteValue,
getVoteTarget,
useProposalState,
useProposalProposer,
useProposalLocked,
@ -77,13 +73,10 @@ import RepeatIcon from '@mui/icons-material/Repeat';
const HUNDRED = new DecimalBigNumber(100n, 0);
const ProposalDetails = ({ chainId, address, connect, config }) => {
const location = useLocation();
const { id } = useParams();
const proposalId = BigInt(id);
const [isPendingVote, setIsPendingVote] = useState(-1);
const [isPendingExecute, setIsPendingExecute] = useState(false);
const [isPendingRelease, setIsPendingRelease] = useState(false);
const [isPending, setIsPending] = useState(false);
const [selectedDiscussionUrl, setSelectedDiscussionUrl] = useState(undefined);
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
@ -91,8 +84,6 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const theme = useTheme();
const queryClient = useQueryClient();
const { getStorageValue, setStorageValue } = useLocalStorage();
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { balance } = useBalance(chainId, "GHST", address);
@ -111,7 +102,7 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
const { pastVotes } = usePastVotes(chainId, "GHST", proposalSnapshot, address);
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: `${chainId}/governance/${id}` });
ReactGA.send({ hitType: "pageview", page: `/governance/${id}` });
}, []);
const quorumPercentage = useMemo(() => {
@ -121,24 +112,48 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
const votePercentage = useMemo(() => {
if (totalSupply._value === 0n) return 0;
return totalVotes * HUNDRED / totalSupply;
const value = (totalVotes?._value ?? 0n) * 100n / (totalSupply?._value ?? 1n);
return new DecimalBigNumber(value, 0);
}, [totalVotes, totalSupply]);
const forPercentage = useMemo(() => {
if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2);
return forVotes * HUNDRED / totalSupply;
const value = (forVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n);
return new DecimalBigNumber(value, 2);
}, [forVotes, totalSupply]);
const againstPercentage= useMemo(() => {
if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2);
return againstVotes * HUNDRED / totalSupply;
const value = (againstVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n);
return new DecimalBigNumber(value, 2);
}, [againstVotes, totalSupply]);
const voteWeightPercentage = useMemo(() => {
if (totalSupply._value === 0n) return new DecimalBigNumber(0n, 2);
return pastVotes * HUNDRED / totalSupply;
const value = (pastVotes?._value ?? 0n) * 10000n / (totalSupply?._value ?? 1n);
return new DecimalBigNumber(value, 2);
}, [pastVotes, totalSupply]);
const voteValue = useMemo(() => {
if (totalVotes?._value == 0n) {
return 0;
}
return Number(forVotes._value * 100n / totalVotes._value);
}, [forVotes, totalVotes]);
const voteTarget = useMemo(() => {
const first = (5n * againstVotes._value + forVotes._value);
const second = BigInt(Math.floor(Math.sqrt(Number(totalVotes._value))));
const bias = 3n * first + second;
const denominator = totalVotes._value + bias;
if (denominator === 0n) {
return 80;
}
return Number(totalVotes?._value * 100n / denominator);
}, [againstVotes, forVotes, totalVotes]);
const nativeCurrency = useMemo(() => {
const client = config?.getClient();
return client?.chain?.nativeCurrency?.symbol;
@ -153,45 +168,38 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
return url;
}, [proposalProposer, config]);
const isPending = useMemo(() => {
return isPendingExecute || isPendingRelease || isPendingVote > -1;
}, [isPendingExecute, isPendingRelease, isPendingVote]);
const handleVote = useCallback(async (support) => {
setIsPendingVote(support);
const handleVote = async (against) => {
setIsPending(true);
const support = against ? 0 : 1;
const result = await castVote(chainId, address, proposalId, support);
if (result) {
const storedVotedProposals = getStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, []);
const proposals = JSON.parse(storedVotedProposals || "[]").map(id => BigInt(id));
proposals.push(proposalId);
setStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, proposals.map(id => id.toString()));
await queryClient.invalidateQueries();
const toStore = JSON.stringify(proposalId.toString());
localStorage.setItem(VOTED_PROPOSALS_PREFIX, toStore);
}
setIsPendingVote(-1);
}, [chainId, address, proposalId]);
const handleExecute = async () => {
setIsPendingExecute(true);
await executeProposal(chainId, address, proposalId);
await queryClient.invalidateQueries();
setIsPendingExecute(false);
setIsPending(true);
}
const handleRelease = async () => {
setIsPendingRelease(true);
const handleExecute = async () => {
setIsPending(true);
await executeProposal(chainId, address, proposalId);
setIsPending(true);
}
const handleRelease = async (proposalId) => {
setIsPending(true);
await releaseLocked(chainId, address, proposalId);
await queryClient.invalidateQueries();
setIsPendingRelease(false);
setIsPending(true);
}
return (
<Box>
<PageTitle
name={`GDP-${id.slice(-5)}`}
name={`GBP-${id.slice(-5)}`}
subtitle={
<Typography component="span">
{`Cast ${ghstSymbol} to shape this proposal's outcome`}
Proposal details, need more in-depth description
</Typography>
}
/>
@ -206,24 +214,19 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
}}
>
<Box sx={{ mt: "15px" }}>
<Box
gap="20px"
display="flex"
justifyContent={"space-between"}
flexDirection={isSmallScreen ? "column" : "row"}
>
<Box display="flex" justifyContent="space-between" gap="20px">
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="10px">
{!isSmallScreen &&<Typography variant="h6">
<Typography variant="h6">
Progress
</Typography>}
</Typography>
<Chip
sx={{ marginTop: "4px", width: "88px" }}
label={convertStatusToLabel(proposalState)}
background={convertStatusToColor(proposalState)}
template={convertStatusToTemplate(proposalState)}
/>
</Box>
}
@ -237,18 +240,18 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
<Box display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between">
<Typography sx={{ textShadow: "0 0 black", fontWeight: 600 }} variant="body1" color={theme.colors.feedback.success}>
For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forPercentage?.toString(), 2)}%)
For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forPercentage?.toString(), 1)}%)
</Typography>
<Typography sx={{ textShadow: "0 0 black", fontWeight: 600 }} variant="body1" color={theme.colors.feedback.error}>
Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstPercentage?.toString(), 2)}%)
Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstPercentage?.toString(), 1)}%)
</Typography>
</Box>
<LinearProgressBar
barColor={theme.colors.feedback.success}
barBackground={theme.colors.feedback.error}
variant="determinate"
value={getVoteValue(forVotes?._value ?? 0n, totalVotes?._value ?? 0n)}
target={getVoteTarget(totalVotes?._value ?? 0n, totalSupply?._value ?? 0n)}
value={voteValue}
target={voteTarget}
/>
</Box>
@ -261,7 +264,7 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
</Box>
<Box display="flex" justifyContent="space-between">
<Typography>Locked</Typography>
<Typography>{formatCurrency(proposalLocked, 5, ghstSymbol)}</Typography>
<Typography>{formatCurrency(proposalLocked, 4, ghstSymbol)}</Typography>
</Box>
<hr width="100%" />
@ -271,7 +274,7 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
<Typography>Quorum</Typography>
<InfoTooltip message={`Minimum $${ghstSymbol} turnout required for the proposal to become valid, as percentage of the total $${ghstSymbol} supply at the time when proposal was created`} />
</Box>
<Typography>{formatNumber(proposalQuorum.toString(), 5)} ({formatNumber(quorumPercentage, 2)}%)</Typography>
<Typography>{formatNumber(proposalQuorum.toString(), 4)} ({formatNumber(quorumPercentage, 1)}%)</Typography>
</Box>
<Box display="flex" justifyContent="space-between">
@ -279,7 +282,7 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
<Typography>Total</Typography>
<InfoTooltip message={`Total votes for the proposal, as percentage of the total $${ghstSymbol} supply at the time when proposal was created`}/>
</Box>
<Typography>{formatNumber(totalSupply.toString(), 5)} ({formatNumber(votePercentage, 2)}%)</Typography>
<Typography>{formatNumber(totalSupply.toString(), 4)} ({formatNumber(votePercentage, 1)}%)</Typography>
</Box>
<Box display="flex" justifyContent="space-between">
@ -287,7 +290,7 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
<Typography>Votes</Typography>
<InfoTooltip message={`Your voting power, as percentage of total $${ghstSymbol} at the time when proposal was created`} />
</Box>
<Typography>{formatNumber(pastVotes.toString(), 5)} ({formatNumber(voteWeightPercentage, 2)}%)</Typography>
<Typography>{formatNumber(pastVotes.toString(), 4)} ({formatNumber(voteWeightPercentage, 1)}%)</Typography>
</Box>
</Box>
@ -299,18 +302,16 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
<SecondaryButton
fullWidth
disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending}
loading={isPendingVote === 1}
onClick={() => handleVote(1)}
>
{isPendingVote === 1 ? "Voting For..." : "For"}
For
</SecondaryButton>
<SecondaryButton
fullWidth
disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending}
loading={isPendingVote === 0}
onClick={() => handleVote(0)}
>
{isPendingVote === 0 ? "Voting Against..." : "Against"}
Against
</SecondaryButton>
</>
: <SecondaryButton
@ -345,9 +346,6 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
isProposer={proposalProposer === address}
chainId={chainId}
proposalId={id}
isPending={isPending}
isPendingExecute={isPendingExecute}
isPendingRelease={isPendingRelease}
/>
</Paper>
</Box>
@ -364,59 +362,28 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
</Box>
}
>
{isSmallScreen
? <Box display="flex" flexDirection="column">
{proposalDetails?.map((metadata, index) => {
return parseFunctionCalldata({
metadata,
index,
chainId,
nativeCoin: nativeCurrency,
isTable: false,
});
})}
</Box>
: <TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell align="center" style={{ padding: "8px 0" }}>Function</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Target</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Calldata</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>{proposalDetails?.map((metadata, index) => {
return parseFunctionCalldata({
metadata,
index,
chainId,
nativeCoin: nativeCurrency,
});
})}</TableBody>
</Table>
</TableContainer>
}
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell align="center" style={{ padding: "8px 0" }}>Function</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Target</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Calldata</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>{proposalDetails?.map((metadata, index) => {
return parseFunctionCalldata(metadata, index, chainId, nativeCurrency);
})}</TableBody>
</Table>
</TableContainer>
</Paper>
</Container>
</Box>
)
}
const VotingTimeline = ({
connect,
handleExecute,
handleRelease,
proposalLocked,
proposalId,
chainId,
state,
address,
isProposer,
isPending,
isPendingExecute,
isPendingRelease,
}) => {
const VotingTimeline = ({ connect, handleExecute, handleRelease, proposalLocked, proposalId, chainId, state, address, isProposer }) => {
const { delay: propsalVotingDelay } = useProposalVotingDelay(chainId, proposalId);
const { snapshot: proposalSnapshot } = useProposalSnapshot(chainId, proposalId);
const { deadline: proposalDeadline } = useProposalDeadline(chainId, proposalId);
@ -436,26 +403,19 @@ const VotingTimeline = ({
<VotingTimelineItem chainId={chainId} occured={proposalDeadline} message="Voting ends" />
</Timeline>
<Box width="100%" display="flex" gap="10px">
{(isProposer && (proposalLocked?._value ?? 0n) > 0n) && <SecondaryButton
{isProposer && <SecondaryButton
fullWidth
loading={isPendingRelease}
disabled={isPending || state < 2}
disabled={(proposalLocked?._value ?? 0n) === 0n || state < 2}
onClick={() => address === "" ? connect() : handleRelease()}
>
{address === "" ? "Connect" : isPendingRelease ? "Releasing..." : "Release"}
{address === "" ? "Connect" : "Release"}
</SecondaryButton>}
<SecondaryButton
fullWidth
loading={isPending}
disabled={isPending || state !== 4}
disabled={state !== 4}
onClick={() => address === "" ? connect() : handleExecute()}
>
{address === ""
? "Connect"
: state !== 4
? convertStatusToLabel(state)
: isPendingExecute ? "Executing..." : "Execute"
}
{address === "" ? "Connect" : "Execute"}
</SecondaryButton>
</Box>
</Box>

View File

@ -12,7 +12,7 @@ const GovernanceInfoText = () => {
ghostDAOs adaptive governance system algorithmically sets minimum collateral based on activity.
&nbsp;<Link
color={theme.colors.primary[300]}
href="http://forum.ghostchain.io/"
href="https://docs.dao.ghostchain.io/"
target="_blank"
rel="noopener noreferrer"
>Learn more here.</Link>

View File

@ -103,11 +103,7 @@ const InitialStep = ({ selectedOption, handleChange, handleCalldata, handleProce
return allPossibleFunctions.find(opt => opt.value === selected)?.label || selected;
}}
/>
<Typography align="center" variant="body2">
<b>{selectedOption}</b>
{" "}
{functionDescription}
</Typography>
<Typography align="center" variant="body2">{functionDescription}</Typography>
{ready
? <TertiaryButton disabled={!selectedOption} onClick={() => handleProceed()} fullWidth>Proceed</TertiaryButton>
: <PrimaryButton disabled={!selectedOption} onClick={() => handleCalldata()} fullWidth>Create Function</PrimaryButton>

View File

@ -1,5 +1,5 @@
import { useState, useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import {
Box,
@ -23,7 +23,6 @@ import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
import { networkAvgBlockSpeed } from "../../../constants";
import { prettifySecondsInDays, prettifySeconds } from "../../../helpers/timeUtil";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import Chip from "../../../components/Chip/Chip";
import Modal from "../../../components/Modal/Modal";
@ -33,7 +32,7 @@ import { PrimaryButton, TertiaryButton } from "../../../components/Button";
import ProposalInfoText from "./ProposalInfoText";
import {
convertStatusToColor,
convertStatusToTemplate,
convertStatusToLabel,
MY_PROPOSALS_PREFIX,
VOTED_PROPOSALS_PREFIX
@ -41,8 +40,9 @@ import {
import { useScreenSize } from "../../../hooks/useScreenSize";
import { useProposals } from "../../../hooks/governance";
import { useLocalStorage } from "../../../hooks/localstorage";
import {
useProposals,
} from "../../../hooks/governance";
const MAX_PROPOSALS_TO_SHOW = 10;
@ -51,16 +51,17 @@ const ProposalsList = ({ chainId, address, config }) => {
const navigate = useNavigate();
const theme = useTheme();
const { network } = useParams();
const { getStorageValue, setStorageValue } = useLocalStorage();
const [proposalsFilter, setProposalFilter] = useState("active");
const myStoredProposals = getStorageValue(chainId, address, MY_PROPOSALS_PREFIX, []);
const [myProposals, setMyProposals] = useState(() => myStoredProposals.map(id => BigInt(id)));
const myStoredProposals = localStorage.getItem(MY_PROPOSALS_PREFIX);
const [myProposals, setMyProposals] = useState(
myStoredProposals ? JSON.parse(myStoredProposals).map(id => BigInt(id)) : []
);
const storedVotedProposals = getStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, []);
const [votedProposals, setVotedProposals] = useState(() => storedVotedProposals.map(id => BigInt(id)));
const storedVotedProposals = localStorage.getItem(VOTED_PROPOSALS_PREFIX);
const [votedProposals, setVotedProposals] = useState(
storedVotedProposals ? JSON.parse(storedVotedProposals).map(id => BigInt(id)) : []
);
const searchedIndexes = useMemo(() => {
switch (proposalsFilter) {
@ -106,7 +107,7 @@ const ProposalsList = ({ chainId, address, config }) => {
proposal={proposal}
blockNumber={blockNumber}
chainId={chainId}
openProposal={() => navigate(`${network}/governance/${proposal.hashes.full}`)}
openProposal={() => navigate(`/governance/${proposal.hashes.full}`)}
/>
))}
</Box>
@ -153,7 +154,7 @@ const ProposalsList = ({ chainId, address, config }) => {
proposal={proposal}
blockNumber={blockNumber}
chainId={chainId}
openProposal={() => navigate(`/${network}/governance/${proposal.hashes.full}`)}
openProposal={() => navigate(`/governance/${proposal.hashes.full}`)}
/>
))}
</ProposalTable>
@ -174,7 +175,7 @@ const ProposalTable = ({ children }) => (
<TableCell align="center" style={{ width: "130px", padding: "8px 0" }}>Status</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Vote Ends</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Voting Stats</TableCell>
<TableCell align="center" style={{ width: "180px", padding: "8px 0" }}></TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}></TableCell>
</TableRow>
</TableHead>
@ -185,6 +186,34 @@ const ProposalTable = ({ children }) => (
const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => {
const theme = useTheme();
const voteValue = useMemo(() => {
const againstVotes = proposal?.votes?.at(0)?._value ?? 0n;
const forVotes = proposal?.votes?.at(1)?._value ?? 0n;
const totalVotes = againstVotes + forVotes;
if (totalVotes == 0) {
return 0;
}
return Number(forVotes * 100n / totalVotes);
}, [proposal]);
const voteTarget = useMemo(() => {
const againstVotes = proposal?.votes?.at(0)?._value ?? 0n;
const forVotes = proposal?.votes?.at(1)?._value ?? 0n;
const totalVotes = againstVotes + forVotes;
const first = (5n * againstVotes + forVotes);
const second = BigInt(Math.floor(Math.sqrt(Number(totalVotes))));
const bias = 3n * first + second;
const denominator = totalVotes + bias;
if (denominator === 0n) {
return 80;
}
return Number(totalVotes * 100n / denominator);
}, [proposal]);
return (
<TableRow id={proposal.hashes.short + `--proposal`} data-testid={proposal.hashes.short + `--proposal`}>
<TableCell align="center" style={{ padding: "8px 0" }}>
@ -195,7 +224,7 @@ const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => {
<Chip
sx={{ width: "100px" }}
label={convertStatusToLabel(proposal.state)}
background={convertStatusToColor(proposal.state)}
template={convertStatusToTemplate(proposal.state)}
/>
</TableCell>
@ -215,8 +244,8 @@ const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => {
barColor={theme.colors.feedback.success}
barBackground={theme.colors.feedback.error}
variant="determinate"
value={proposal?.voteValue ?? 0}
target={proposal?.voteTarget ?? 50}
value={voteValue}
target={voteTarget}
/>
</Box>
</TableCell>
@ -244,6 +273,7 @@ const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => {
const ProposalCard = ({ proposal, blockNumber, openProposal, chainId }) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery('(max-width: 450px)');
return (
<Box id={proposal.hashes.short + `--proposal`} data-testid={proposal.hashes.short + `--proposal`}>
<Box display="flex" flexDirection={isSmallScreen ? "column" : "row"} justifyContent="space-between">
@ -253,7 +283,7 @@ const ProposalCard = ({ proposal, blockNumber, openProposal, chainId }) => {
<Chip
sx={{ width: "88px" }}
label={convertStatusToLabel(proposal.state)}
background={convertStatusToColor(proposal.state)}
template={convertStatusToTemplate(proposal.state)}
/>
</Box>
<Typography>
@ -271,8 +301,8 @@ const ProposalCard = ({ proposal, blockNumber, openProposal, chainId }) => {
barColor={theme.colors.feedback.success}
barBackground={theme.colors.feedback.error}
variant="determinate"
value={proposal?.voteValue ?? 0}
target={proposal?.voteTarget ?? 50}
value={69}
target={Math.floor(Math.random() * 101)}
/>
</Box>
<Box marginBottom="20px">
@ -312,11 +342,10 @@ const ProposalFilterTrigger = ({ trigger, setTrigger }) => {
}
const convertDeadline = (deadline, blockNumber, chainId) => {
const alreadyHappened = blockNumber > deadline;
const diff = alreadyHappened ? blockNumber - deadline : deadline - blockNumber;
const diff = blockNumber > deadline ? blockNumber - deadline : deadline - blockNumber;
const voteSeconds = Number(diff * networkAvgBlockSpeed(chainId));
const result = prettifySeconds(voteSeconds, "min");
const result = prettifySeconds(voteSeconds, "mins");
if (result === "now") {
return new Date(Date.now()).toLocaleDateString('en-US', {
year: 'numeric',
@ -325,10 +354,7 @@ const convertDeadline = (deadline, blockNumber, chainId) => {
});
}
const prefix = alreadyHappened ? "" : "in ";
const postfix = alreadyHappened ? " ago" : "";
return `${prefix}${result}${postfix}`;
return `in ${result}`;
}
export default ProposalsList;

View File

@ -20,12 +20,20 @@ export const prepareAuditReservesCalldata = (chainId) => {
return { label, target, calldata, value };
}
export const prepareAuditReservesDescription = "function audits and updates the protocol's total reserve value. It sums the value of all approved reserve and liquidity tokens, then stores and logs the new total.";
export const prepareAuditReservesDescription = "Audit Reserves function audits and updates the protocol's total reserve value. It sums the value of all approved reserve and liquidity tokens, then stores and logs the new total.";
export const AuditReservesSteps = () => {
return null;
}
export const AuditReservesParsed = (props) => {
return (
<>
{props.isTable && <AuditReservesParsedCell {...props} />}
</>
)
}
const AuditReservesParsedCell = (props) => {
return <ParsedCell {...props} />
}

View File

@ -29,7 +29,7 @@ export const prepareCreateBondCalldata = (chainId, markets, terms, quoteToken, i
return { label, target, calldata, value };
}
export const prepareCreateBondDescription = "function creates a new bond market by processing pricing, capacity, and term inputs. It initializes and stores the new bond market's complete configuration in the protocol.";
export const prepareCreateBondDescription = "Create Bond function creates a new bond market by processing pricing, capacity, and term inputs. It initializes and stores the new bond market's complete configuration in the protocol.";
export const CreateBondParsed = (props) => {
const [isOpened, setIsOpened] = useState(false);
@ -51,11 +51,15 @@ export const CreateBondParsed = (props) => {
<CreateBondSteps args={props.args} ftsoSymbol={ftsoSymbol} nativeCurrency={props.nativeCoin} {...props} />
</Box>
</Modal>
<ParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props} />
{props.isTable && <CreateBondParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props}/>}
</>
)
}
const CreateBondParsedCell = (props) => {
return <ParsedCell {...props} />
}
export const CreateBondSteps = ({ nativeCurrency, ftsoSymbol, chainId, toInitialStep, addCalldata, args }) => {
const createMode = args === undefined;
@ -114,7 +118,7 @@ export const CreateBondSteps = ({ nativeCurrency, ftsoSymbol, chainId, toInitial
}, [step, tokenAddress, capacity, initialPrice, debtBuffer, depositInterval, tuneInterval]);
const possibleTokens = [
{ value: FTSO_DAI_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_DAI_LP_ADDRESSES[chainId])}` },
{ value: FTSO_DAI_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${nativeCurrency} LP`, label: `${ftsoSymbol}-${nativeCurrency} LP: ${shorten(FTSO_DAI_LP_ADDRESSES[chainId])}` },
{ value: RESERVE_ADDRESSES[chainId], symbol: reserveSymbol, label: `${reserveSymbol}: ${shorten(RESERVE_ADDRESSES[chainId])}` },
];
@ -125,7 +129,7 @@ export const CreateBondSteps = ({ nativeCurrency, ftsoSymbol, chainId, toInitial
createMode={createMode}
possibleTokens={possibleTokens}
setTokenAddress={createMode ? setTokenAddress : empty}
nativeCurrency={reserveSymbol}
nativeCurrency={nativeCurrency}
ftsoSymbol={ftsoSymbol}
capacityInQuote={capacityInQuote}
setCapacityInQuote={createMode ? setCapacityInQuote : empty}
@ -222,7 +226,7 @@ const IntervalsArguments = ({
/>
<ArgumentInput
endString="seconds"
label="_intervals[1]"
label="_intervals[0]"
value={tuneInterval ?? ""}
setValue={setTuneInterval}
tooltip="Time in seconds between bond price tuning events that evaluate actual vs. expected bond sales, adjusting how aggressively price decays in the next interval."
@ -292,7 +296,7 @@ const TokenAndBooleansArguments = ({
rightText={"False"}
setLeftValue={() => setCapacityInQuote(true)}
setRightValue={() => setCapacityInQuote(false)}
tooltip={`Determines how the bond market capacity is measured. True = measured in _quoteToken, False = measured in ${ftsoSymbol}.`}
tooltip={`Determines how the bond market capacity is measured. True = measured in ${nativeCurrency}, False = measured in ${ftsoSymbol}.`}
/>
<BooleanTrigger
value={fixedTerm}
@ -311,7 +315,6 @@ const TokenAndBooleansArguments = ({
value={selectedOption ?? ""}
onChange={handleChange}
options={possibleTokens}
maxWidth="calc(100vw - 70px)"
inputWidth="100%"
renderValue={(selected) => {
if (!selected || selected.length === 0) {

View File

@ -1,85 +0,0 @@
import { useState } from "react";
import { encodeFunctionData } from 'viem';
import { Box, Typography, TableRow, TableCell, useTheme } from "@mui/material";
import Modal from "../../../../components/Modal/Modal";
import { PrimaryButton, TertiaryButton } from "../../../../components/Button";
import { GHOST_GOVERNANCE_ADDRESSES } from "../../../../constants/addresses";
import { abi as GovernorAbi } from "../../../../abi/GhostGovernor.json";
import { ArgumentInput, ParsedCell } from "./index";
export const prepareSetLateQuorumExtensionCalldata = (chainId, lateQuorumVoteExtension) => {
const value = 0n;
const label = "setLateQuorumVoteExtension";
const target = GHOST_GOVERNANCE_ADDRESSES[chainId];
const calldata = encodeFunctionData({
abi: GovernorAbi,
functionName: 'setLateQuorumVoteExtension',
args: [lateQuorumVoteExtension]
});
return { label, target, calldata, value };
}
export const prepareSetLateQuorumExtensionDescription = "updates the current value of the vote extension parameter: the number of blocks that are required to pass from the time a proposal reaches quorum until its voting period ends.";
export const SetLateQuorumExtensionParsed = (props) => {
const [isOpened, setIsOpened] = useState(false);
return (
<>
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">View setLateQuorumVoteExtension</Typography>
</Box>
}
open={isOpened}
onClose={() => setIsOpened(false)}
maxWidth="460px"
minHeight="80px"
>
<Box minHeight="100px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
<SetLateQuorumExtensionSteps {...props} />
</Box>
</Modal>
<ParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props} />
</>
)
}
export const SetLateQuorumExtensionSteps = ({ chainId, toInitialStep, addCalldata, args }) => {
const createMode = args === undefined;
const [lateQuorumVoteExtension, setlateQuorumVoteExtension] = useState(args?.at(0));
const handleProceed = () => {
addCalldata(prepareSetLateQuorumExtensionCalldata(chainId, lateQuorumVoteExtension));
}
return (
<Box height="100%" width="100%" display="flex" flexDirection="column" justifyContent={"space-between"}>
<ArgumentInput
disabled={!createMode}
endString="blocks"
label="lateQuorumVoteExtension"
value={lateQuorumVoteExtension ?? ""}
setValue={createMode ? setlateQuorumVoteExtension : () => {}}
tooltip="Vote extension defines extension in blocks if a vote causes quorum to be reached, the proposal's voting period may be extended so that it does not end before at least a specified time has passed"
/>
{createMode && <Box width="100%" sx={{ marginTop: "20px" }} display="flex" flexDirection="column" gap="5px">
<Box display="flex" gap="10px">
<TertiaryButton onClick={toInitialStep} fullWidth>Back</TertiaryButton>
<TertiaryButton disabled fullWidth>Next</TertiaryButton>
</Box>
<PrimaryButton
disabled={!lateQuorumVoteExtension}
onClick={() => handleProceed()}
fullWidth
>
Create Function
</PrimaryButton>
</Box>}
</Box>
);
}

View File

@ -1,85 +0,0 @@
import { useState } from "react";
import { encodeFunctionData } from 'viem';
import { Box, Typography, TableRow, TableCell, useTheme } from "@mui/material";
import Modal from "../../../../components/Modal/Modal";
import { PrimaryButton, TertiaryButton } from "../../../../components/Button";
import { GHOST_GOVERNANCE_ADDRESSES } from "../../../../constants/addresses";
import { abi as GovernorAbi } from "../../../../abi/GhostGovernor.json";
import { ArgumentInput, ParsedCell } from "./index";
export const prepareSetProposalThresholdCalldata = (chainId, newThreshold) => {
const value = 0n;
const label = "setProposalThreshold";
const target = GHOST_GOVERNANCE_ADDRESSES[chainId];
const calldata = encodeFunctionData({
abi: GovernorAbi,
functionName: 'setProposalThreshold',
args: [newThreshold]
});
return { label, target, calldata, value };
}
export const prepareSetProposalThresholdDescription = "updates the minimum amount required to create a proposal. This threshold applies only when there are no Active or Pending proposals; otherwise, a higher threshold is enforced.";
export const SetProposalThresholdParsed = (props) => {
const [isOpened, setIsOpened] = useState(false);
return (
<>
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">View setProposalThreshold</Typography>
</Box>
}
open={isOpened}
onClose={() => setIsOpened(false)}
maxWidth="460px"
minHeight="80px"
>
<Box minHeight="100px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
<SetProposalThresholdSteps {...props} />
</Box>
</Modal>
<ParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props} />
</>
)
}
export const SetProposalThresholdSteps = ({ chainId, toInitialStep, addCalldata, args }) => {
const createMode = args === undefined;
const [newThreshold, setNewThreshold] = useState(args?.at(0));
const handleProceed = () => {
addCalldata(prepareSetProposalThresholdCalldata(chainId, newThreshold));
}
return (
<Box height="100%" width="100%" display="flex" flexDirection="column" justifyContent={"space-between"}>
<ArgumentInput
disabled={!createMode}
endString="!CSPR units"
label="newProposalThreshold"
value={newThreshold ?? ""}
setValue={createMode ? setNewThreshold : () => {}}
tooltip="The new proposal threshold defines the minimum amount each proposer must lock for the duration of the proposal. This locked amount serves as the required deposit to create a proposal."
/>
{createMode && <Box width="100%" sx={{ marginTop: "20px" }} display="flex" flexDirection="column" gap="5px">
<Box display="flex" gap="10px">
<TertiaryButton onClick={toInitialStep} fullWidth>Back</TertiaryButton>
<TertiaryButton disabled fullWidth>Next</TertiaryButton>
</Box>
<PrimaryButton
disabled={!newThreshold}
onClick={() => handleProceed()}
fullWidth
>
Create Function
</PrimaryButton>
</Box>}
</Box>
);
}

View File

@ -24,7 +24,7 @@ export const prepareSetAdjustmentCalldata = (chainId, rateChange, targetRate, in
return { label, target, calldata, value };
}
export const prepareSetAdjustmentDescription = "function schedules a gradual change to the staking APY. It increases or decreases the staking APY by a specified rate per epoch until a target rate reached.";
export const prepareSetAdjustmentDescription = "Set Adjustment function schedules a gradual change to the staking APY. It increases or decreases the staking APY by a specified rate per epoch until a target rate reached.";
export const SetAdjustmentParsed = (props) => {
const [isOpened, setIsOpened] = useState(false);
@ -45,17 +45,21 @@ export const SetAdjustmentParsed = (props) => {
<SetAdjustmentSteps {...props} />
</Box>
</Modal>
<ParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props} />
{props.isTable && <SetAdjustmentParsedCell isOpened={isOpened} setIsOpened={setIsOpened} {...props} />}
</>
)
}
const SetAdjustmentParsedCell = (props) => {
return <ParsedCell {...props} />
}
export const SetAdjustmentSteps = ({ chainId, toInitialStep, addCalldata, args }) => {
const createMode = args === undefined;
const [rate, setRate] = useState(args?.at(0));
const [target, setTarget] = useState(args?.at(1));
const [increase, setIncrease] = useState(args === undefined ? true : args.at(2));
const [increase, setIncrease] = useState(args?.at(1) ?? true);
const handleProceed = () => {
addCalldata(prepareSetAdjustmentCalldata(chainId, rate, target, increase));

View File

@ -10,7 +10,6 @@ import InfoTooltip from "../../../../components/Tooltip/InfoTooltip";
import { abi as TreasuryAbi } from "../../../../abi/GhostTreasury.json";
import { abi as DistributorAbi } from "../../../../abi/GhostDistributor.json";
import { abi as DepositoryAbi } from "../../../../abi/GhostBondDepository.json";
import { abi as GovernorAbi } from "../../../../abi/GhostGovernor.json";
import { config } from "../../../../config";
import { shorten, formatCurrency } from "../../../../helpers";
@ -18,20 +17,16 @@ import { shorten, formatCurrency } from "../../../../helpers";
import { prepareAuditReservesDescription, prepareAuditReservesCalldata, AuditReservesSteps, AuditReservesParsed } from "./AuditReserves";
import { prepareSetAdjustmentDescription, prepareSetAdjustmentCalldata, SetAdjustmentSteps, SetAdjustmentParsed } from "./SetAdjustment";
import { prepareCreateBondDescription, prepareCreateBondCalldata, CreateBondSteps, CreateBondParsed } from "./CreateBond";
import { prepareSetProposalThresholdDescription, prepareSetProposalThresholdCalldata, SetProposalThresholdSteps, SetProposalThresholdParsed } from "./ProposalThreshold";
import { prepareSetLateQuorumExtensionDescription, prepareSetLateQuorumExtensionCalldata, SetLateQuorumExtensionSteps, SetLateQuorumExtensionParsed } from "./LateQuorumExtension";
const DEFAULT_DESCRIPTION = "Please select the function to include in your proposal. Multi-functional proposals are allowed, but each included function should be clearly specified."
export const allPossibleFunctions = [
{ value: "auditReserves", label: "auditReserves" },
{ value: "setAdjustment", label: "setAdjustment" },
{ value: "create", label: "create" },
{ value: "setProposalThreshold", label: "setProposalThreshold" },
{ value: "setLateQuorumVoteExtension", label: "setLateQuorumVoteExtension" },
{ value: "auditReserves", label: "auditReserves" },
{ value: "setAdjustment", label: "setAdjustment" },
{ value: "create", label: "create" },
];
const allAbis = [TreasuryAbi, DistributorAbi, DepositoryAbi, GovernorAbi];
const allAbis = [TreasuryAbi, DistributorAbi, DepositoryAbi];
const identifyAction = (calldata) => {
let decoded = { functionName: "Unknown", args: [] };
@ -49,58 +44,78 @@ const identifyAction = (calldata) => {
return decoded;
}
export const parseFunctionCalldata = ({
metadata,
index,
chainId,
nativeCoin,
removeCalldata,
isTable = true,
}) => {
export const parseFunctionCalldata = (metadata, index, chainId, nativeCoin, removeCalldata) => {
const { label, calldata, target, value } = metadata;
const { functionName, args } = identifyAction(calldata);
const labelOrName = label ?? (functionName ?? "Unknown");
const remove = removeCalldata && (() => removeCalldata(index));
const props = {
isTable,
calldata,
label: labelOrName,
chainId: chainId,
remove: removeCalldata,
nativeCoin,
value,
target,
args,
id: index,
};
switch (functionName) {
case allPossibleFunctions[0].value:
return <AuditReservesParsed key={index} {...props} />;
case allPossibleFunctions[1].value:
return <SetAdjustmentParsed key={index} {...props} />;
case allPossibleFunctions[2].value:
return <CreateBondParsed key={index} {...props} />;
case allPossibleFunctions[3].value:
return <SetProposalThresholdParsed key={index} {...props} />;
case allPossibleFunctions[4].value:
return <SetLateQuorumExtensionParsed key={index} {...props} />;
case "auditReserves":
return <AuditReservesParsed
isTable
key={index}
calldata={calldata}
label={labelOrName}
chainId={chainId}
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
case "setAdjustment":
return <SetAdjustmentParsed
isTable
args={args}
key={index}
calldata={calldata}
label={labelOrName}
chainId={chainId}
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
case "create":
return <CreateBondParsed
isTable
args={args}
key={index}
calldata={calldata}
label={labelOrName}
chainId={chainId}
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
default:
return <ParsedCell key={index} {...props} />;
return <ParsedCell
isTable
key={index}
calldata={calldata}
label="Unknown"
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
}
}
export const getFunctionArguments = (functionName) => {
switch (functionName) {
case allPossibleFunctions[1].value:
case "auditReserves":
return null;
case "setAdjustment":
return SetAdjustmentSteps;
case allPossibleFunctions[2].value:
case "create":
return CreateBondSteps;
case allPossibleFunctions[3].value:
return SetProposalThresholdSteps;
case allPossibleFunctions[4].value:
return SetLateQuorumExtensionSteps;
default:
return null;
}
@ -108,16 +123,12 @@ export const getFunctionArguments = (functionName) => {
export const getFunctionCalldata = (functionName, chainId) => {
switch (functionName) {
case allPossibleFunctions[0].value:
case "auditReserves":
return prepareAuditReservesCalldata(chainId);
case allPossibleFunctions[1].value:
case "setAdjustment":
return prepareSetAdjustmentCalldata(chainId);
case allPossibleFunctions[2].value:
case "create":
return prepareCreateBondCalldata(chainId);
case allPossibleFunctions[3].value:
return prepareSetProposalThresholdCalldata(chainId);
case allPossibleFunctions[4].value:
return prepareSetLateQuorumExtensionCalldata(chainId);
default:
return null;
}
@ -125,16 +136,12 @@ export const getFunctionCalldata = (functionName, chainId) => {
export const getFunctionDescription = (functionName) => {
switch (functionName) {
case allPossibleFunctions[0].value:
case "auditReserves":
return prepareAuditReservesDescription;
case allPossibleFunctions[1].value:
case "setAdjustment":
return prepareSetAdjustmentDescription;
case allPossibleFunctions[2].value:
case "create":
return prepareCreateBondDescription;
case allPossibleFunctions[3].value:
return prepareSetProposalThresholdDescription;
case allPossibleFunctions[4].value:
return prepareSetLateQuorumExtensionDescription;
default:
return DEFAULT_DESCRIPTION;
}
@ -268,86 +275,34 @@ export const ParsedCell = (props) => {
}
};
if (props.isTable) {
return (
<TableRow id={props.id + `--proposalFunction`} data-testid={props.id + `--proposalFunction`}>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.label}</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Link href={etherscanLink} target="_blank" rel="noopener noreferrer">
<Typography>{shorten(props.target)}</Typography>
</Link>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Link onClick={handleCalldataCopy} target="_blank" rel="noopener noreferrer">
<Typography>{shorten(props.calldata)}</Typography>
</Link>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{formatCurrency(props.value, 5, props.nativeCoin)}</Typography>
</TableCell>
<TableCell>
<div style={{ display: 'flex', gap: '8px' }}>
{props.args && <TertiaryButton fullWidth onClick={() => props.setIsOpened(!props.isOpened)}>View</TertiaryButton>}
{props.remove && <TertiaryButton fullWidth onClick={() => props.remove()}>Delete</TertiaryButton>}
</div>
</TableCell>
</TableRow>
)
}
return (
<Box
id={props.id + `--proposalFunction`}
data-testid={props.id + `--proposalFunction`}
display="flex"
flexDirection="column"
>
<Box display="flex" justifyContent="space-between">
<Typography>{`Function ${props.id + 1}`}</Typography>
<TableRow id={props.id + `--proposalFunction`} data-testid={props.id + `--proposalFunction`}>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.label}</Typography>
</Box>
</TableCell>
<Box display="flex" justifyContent="space-between">
<Typography>Target</Typography>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Link href={etherscanLink} target="_blank" rel="noopener noreferrer">
<Typography>{shorten(props.target)}</Typography>
</Link>
</Box>
</TableCell>
<Box display="flex" justifyContent="space-between">
<Typography>Calldata</Typography>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Link onClick={handleCalldataCopy} target="_blank" rel="noopener noreferrer">
<Typography>{shorten(props.calldata)}</Typography>
</Link>
</Box>
</TableCell>
<Box display="flex" justifyContent="space-between">
<Typography>Value</Typography>
<Link onClick={handleCalldataCopy} target="_blank" rel="noopener noreferrer">
<Typography>{formatCurrency(props.value, 5, props.nativeCoin)}</Typography>
</Link>
</Box>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{formatCurrency(props.value, 4, props.nativeCoin)}</Typography>
</TableCell>
{(props.args || props.remove) && <Box
display="flex"
flexDirection="row"
justifyContent="space-between"
gap="10px"
marginTop="10px"
>
{props.args && <TertiaryButton fullWidth onClick={() => props.setIsOpened(!props.isOpened)}>View</TertiaryButton>}
{props.remove && <TertiaryButton fullWidth onClick={() => props.remove()}>Delete</TertiaryButton>}
</Box>}
<hr width="100%" style={{ margin: "20px 0" }} />
</Box>
<TableCell>
<div style={{ display: 'flex', gap: '8px' }}>
{props.args && <TertiaryButton fullWidth onClick={() => props.setIsOpened(!props.isOpened)}>View</TertiaryButton>}
{props.remove && <TertiaryButton fullWidth onClick={() => props.remove()}>Delete</TertiaryButton>}
</div>
</TableCell>
</TableRow>
)
}

View File

@ -1,24 +1,18 @@
export const MY_PROPOSALS_PREFIX = "MY_PROPOSALS";
export const VOTED_PROPOSALS_PREFIX = "VOTED_PROPOSALS";
export const convertStatusToColor = (status) => {
export const convertStatusToTemplate = (status) => {
switch (status) {
case 1:
return "#f2e370";
case 2:
return "#f06f73";
case 3:
return "#f06f73";
case 4:
return "#60c45b";
case 5:
return "#60c45b";
case 6:
return "#f06f73";
case 7:
return "#f2e370";
return 'darkGray';
case 2:
return 'warning';
case 4:
return 'success';
case 3:
return 'error';
default:
return "#a2b7ce";
return 'darkGray';
}
}

View File

@ -31,6 +31,7 @@ export default function NotFound({
});
setWrongNetworkToastId(toastId);
setNetworkId(newChainId);
switchChain({ chainId: newChainId });
}

View File

@ -14,11 +14,12 @@ import Paper from "../../components/Paper/Paper";
import TokenStack from "../../components/TokenStack/TokenStack";
import { useTokenSymbol } from "../../hooks/tokens";
const Stake = ({ chainId, chainSymbol, address, isOpened, closeModal, connect }) => {
const Stake = ({ chainId, address, isOpened, closeModal, connect }) => {
const [action, setAction] = useState("STAKE");
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const [upperToken, setUpperToken] = useState(ftsoSymbol);
@ -30,26 +31,38 @@ const Stake = ({ chainId, chainSymbol, address, isOpened, closeModal, connect })
useEffect(() => {
switch (true) {
case (upperToken === ftsoSymbol && bottomToken === stnkSymbol):
setAction("STAKE")
break;
case (upperToken === ftsoSymbol && bottomToken === ghstSymbol):
setAction("Stake")
setAction("STAKE")
break;
case (upperToken === stnkSymbol && bottomToken === ftsoSymbol):
setAction("UNSTAKE")
break;
case (upperToken === ghstSymbol && bottomToken === ftsoSymbol):
setAction("Unstake")
setAction("UNSTAKE")
break;
case (upperToken === stnkSymbol && bottomToken === ghstSymbol):
setAction("WRAP")
break;
case (upperToken === ghstSymbol && bottomToken === stnkSymbol):
setAction("UNWRAP")
break;
default:
setAction("Stake")
setAction("STAKE")
}
}, [ftsoSymbol, ghstSymbol, upperToken, bottomToken])
}, [upperToken, bottomToken])
return (
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">{action}</Typography>
<TokenStack tokens={[upperToken, bottomToken, chainSymbol]} />
<TokenStack tokens={[upperToken, bottomToken]} />
</Box>
}
topRight={
topLeft={
<GhostStyledIcon
viewBox="0 0 23 23"
component={SettingsIcon}
@ -64,7 +77,6 @@ const Stake = ({ chainId, chainSymbol, address, isOpened, closeModal, connect })
>
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column">
<StakeArea
chainSymbol={chainSymbol}
settingsModalOpen={settingsModalOpen}
closeSettingModal={() => setSettingsModalOpenInner(false)}
upperToken={upperToken}

View File

@ -1,6 +1,5 @@
import { Dispatch, SetStateAction, useState, useEffect, useMemo } from "react";
import { Dispatch, SetStateAction, useState, useEffect } from "react";
import { Box, Container, Grid, Divider, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useLocation } from "react-router-dom";
import ReactGA from "react-ga4";
import Paper from "../../components/Paper/Paper";
@ -19,8 +18,7 @@ import { Apy, CurrentIndex, TotalDeposit } from "./components/Metric";
import { useEpoch } from "../../hooks/staking";
import { useTokenSymbol } from "../../hooks/tokens";
export const StakeContainer = ({ chainId, address, config, connect }) => {
const location = useLocation();
export const StakeContainer = ({ chainId, address, connect }) => {
const [isModalOpened, handleModal] = useState(false);
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
@ -32,20 +30,13 @@ export const StakeContainer = ({ chainId, address, config, connect }) => {
const { epoch, refetch: refetchEpoch } = useEpoch(chainId);
const chainSymbol = useMemo(() => {
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
if (chainSymbol) return chainSymbol;
return "WTF";
}, [config, chainId])
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location])
ReactGA.send({ hitType: "pageview", page: "/stake" });
}, [])
if (isModalOpened) {
return (
<Stake
chainSymbol={chainSymbol}
chainId={chainId}
address={address}
isOpened={isModalOpened}
@ -57,7 +48,7 @@ export const StakeContainer = ({ chainId, address, config, connect }) => {
return (
<Box>
<PageTitle name="Protocol Staking" subtitle={`(3, 3) Stake ${ftsoSymbol} to earn rebase yields backed by protocol revenue`} />
<PageTitle name="Protocol Staking" subtitle={`Stake your ${ftsoSymbol} to earn rebase yields`} />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
@ -95,7 +86,7 @@ export const StakeContainer = ({ chainId, address, config, connect }) => {
</Grid>
<Divider sx={{ marginTop: "30px" }} />
<ClaimsArea chainId={chainId} address={address} chainSymbol={chainSymbol} epoch={epoch} />
<ClaimsArea chainId={chainId} address={address} epoch={epoch} />
<Divider />
<Box mt="15px" display="flex" flexDirection="column" alignItems="center" justifyContent="center">
@ -111,7 +102,7 @@ export const StakeContainer = ({ chainId, address, config, connect }) => {
</Box>
</Box>
</Paper>
<FarmPools chainId={chainId} chainSymbol={chainSymbol} />
<FarmPools chainId={chainId} />
</Box>
</Container>
</Box>

View File

@ -30,6 +30,8 @@ const ClaimConfirmationModal = (props) => {
props.ghstSymbol
);
break;
default:
console.log("Unknown action")
}
setIsPending(false);
@ -42,7 +44,7 @@ const ClaimConfirmationModal = (props) => {
maxWidth="356px"
topLeft={<></>}
headerContent={
<Box display="flex" flexDirection="column">
<Box display="flex" flexDirection="row">
<Typography variant="h5">{`Confirm ${capitalize(props.action)}`}</Typography>
</Box>
}

View File

@ -9,7 +9,7 @@ import {
Skeleton,
} from "@mui/material";
import { styled } from "@mui/material/styles";
import { useState, useMemo, useCallback } from "react";
import { useState, useMemo } from "react";
import useMediaQuery from "@mui/material/useMediaQuery";
@ -21,16 +21,14 @@ import InfoTooltip from "../../../components/Tooltip/InfoTooltip";
import Paper from "../../../components/Paper/Paper";
import Token from "../../../components/Token/Token";
import { PrimaryButton, SecondaryButton } from "../../../components/Button";
import { Tab, Tabs } from "../../../components/Tabs/Tabs";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { prettifySecondsInDays } from "../../../helpers/timeUtil";
import { formatNumber, formatCurrency } from "../../../helpers";
import { formatNumber } from "../../../helpers";
import { STAKING_ADDRESSES } from "../../../constants/addresses";
import { useCurrentIndex, useWarmupInfo, claim, breakout } from "../../../hooks/staking";
import { useCurrentIndex, useWarmupInfo } from "../../../hooks/staking";
import { useBalanceForShares, useTokenSymbol } from "../../../hooks/tokens";
import { useGhstPrice, useStnkPrice } from "../../../hooks/prices";
import { useBreakoutModal } from "../../../hooks/breakoutModal";
import { isNetworkLegacy } from "../../../constants";
import ClaimConfirmationModal from "./ClaimConfirmationModal";
@ -50,15 +48,14 @@ const StyledTableHeader = styled(TableHead)(({ theme }) => ({
},
}));
export const ClaimsArea = ({ chainId, address, chainSymbol, epoch }) => {
export const ClaimsArea = ({ chainId, address, epoch }) => {
const isSmallScreen = useMediaQuery("(max-width: 745px)");
const { breakoutFromStaking } = useBreakoutModal();
const [confirmationModalOpen, setConfirmationModalOpenInner] = useState(false);
const [isPayoutGhst, _] = useState(true);
const ghstPrice = useGhstPrice(chainId);
const stnkPrice = useStnkPrice(chainId);
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
const [isPayoutGhst, setIsPayoutGhst] = useState(localStorage.getItem("stake-isGhstPayout")
? localStorage.getItem("stake-isGhstPayout")
: false
);
const { warmupInfo: claim, refetch: claimRefetch } = useWarmupInfo(chainId, address);
const { currentIndex, refetch: currentIndexRefetch } = useCurrentIndex(chainId);
@ -68,65 +65,29 @@ export const ClaimsArea = ({ chainId, address, chainSymbol, epoch }) => {
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const claimableBalance = useMemo(() => {
if (isNetworkLegacy(chainId)) {
return isPayoutGhst
? new DecimalBigNumber(
balanceForShares && currentIndex && currentIndex._value !== 0n
? (balanceForShares._value * (10n ** BigInt(9 + currentIndex._decimals - balanceForShares._decimals))) / currentIndex._value
: 0n,
9
)
: new DecimalBigNumber(
balanceForShares ? balanceForShares._value : 0n,
balanceForShares ? balanceForShares._decimals : 9
);
}
const toClaim = new DecimalBigNumber(claim.shares, 18);
return isPayoutGhst ? toClaim : toClaim.mul(currentIndex);
}, [chainId, claim, currentIndex, balanceForShares]);
const breakoutBalance = useMemo(() => {
if (isNetworkLegacy(chainId)) {
return undefined; // short circuit
}
return isPayoutGhst ? claim.shares : claim.shares.mul(currentIndex);
}, [chainId, claim, currentIndex]);
const setConfirmationModalOpen = useCallback(async (value) => {
if (isNetworkLegacy(chainId) || value) {
setConfirmationModalOpenInner(true);
} else {
const defaultFunction = async () => {
await claim(chainId, address, false, stnkSymbol, ghstSymbol);
await claimRefetch();
}
const warmupLeft = claim.expiry - epoch.number;
const toExecute = async (receiver) => {
const txHash = await breakout(chainId, address, receiver, breakoutBalance);
return txHash;
}
breakoutFromStaking({ defaultFunction, toExecute, amount: claimableBalance, warmupLeft })
}
}, [claim, epoch, address, chainId, ghstSymbol, breakoutBalance, claimableBalance]);
const closeConfirmationModal = () => {
setConfirmationModalOpenInner(false);
setConfirmationModalOpen(false);
claimRefetch();
currentIndexRefetch();
}
if (claim.shares === 0n) return <></>;
const warmupTooltip = `Claim earns rebases during warm-up. You can emergency withdraw, but this forfeits the rebases`;
const setIsPayoutGhstInner = (value) => {
setIsPayoutGhst(value);
localStorage.setitem("bond-isGhstPayout", value);
}
const warmupTooltip = `Your claim earns rebases during warmup. You can emergency withdraw, but this forfeits the rebases`;
return (
<>
<ClaimConfirmationModal
open={confirmationModalOpen}
onClose={() => closeConfirmationModal()}
chainid={chainId}
receiver={address}
receiveAmount={claim.expiry > epoch.number ? claim.deposit : claimableBalance}
receiveAmount={claim.expiry > epoch.number ? claim.deposit : isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
outputToken={claim.expiry > epoch.number ? ftsoSymbol : isPayoutGhst ? ghstSymbol : stnkSymbol}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
@ -144,7 +105,19 @@ export const ClaimsArea = ({ chainId, address, chainSymbol, epoch }) => {
justifyContent="space-between"
flexDirection={isSmallScreen ? "column" : "row"}
>
<Typography variant="h6">My active {isPayoutGhst ? ghstSymbol : stnkSymbol} claim</Typography>
<Typography variant="h6">Your active {isPayoutGhst ? ghstSymbol : stnkSymbol} claim</Typography>
<Tabs
centered
textColor="primary"
indicatorColor="primary"
value={isPayoutGhst ? 1 : 0}
aria-label="Claim token tabs"
onChange={(_, view) => setIsPayoutGhstInner(view === 1)}
TabIndicatorProps={{ style: { display: "none" } }}
>
<Tab aria-label="payout-stnk-button" label={stnkSymbol} style={{ fontSize: "1rem" }} />
<Tab aria-label="payout-ghst-button" label={ghstSymbol} style={{ fontSize: "1rem" }} />
</Tabs>
</Box>
}
tooltip={warmupTooltip}
@ -153,38 +126,33 @@ export const ClaimsArea = ({ chainId, address, chainSymbol, epoch }) => {
{isSmallScreen ? (
<MobileClaimInfo
setConfirmationModalOpen={setConfirmationModalOpen}
prepareBalance={claimableBalance}
prepareBalance={isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
isPayoutGhst={isPayoutGhst}
claim={claim}
epoch={epoch}
isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
/>
) : (
<Table>
<StyledTableHeader className={classes.stakePoolHeaderText}>
<TableRow>
<TableCell style={{ width: "200px", padding: "8px 0" }}>Asset</TableCell>
<TableCell style={{ width: "200px", padding: "8px 0" }}>Staked Amount</TableCell>
<TableCell style={{ width: "200px", padding: "8px 0" }}>Amount</TableCell>
<TableCell style={{ width: "150px", padding: "8px 0" }}>Claimable In</TableCell>
<TableCell></TableCell>
</TableRow>
</StyledTableHeader>
<ClaimInfo
chainSymbol={chainSymbol}
setConfirmationModalOpen={setConfirmationModalOpen}
prepareBalance={claimableBalance}
prepareBalance={isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
isPayoutGhst={isPayoutGhst}
claim={claim}
epoch={epoch}
isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
/>
</Table>
@ -194,28 +162,13 @@ export const ClaimsArea = ({ chainId, address, chainSymbol, epoch }) => {
);
};
const ClaimInfo = ({
chainSymbol,
setConfirmationModalOpen,
prepareBalance,
claim,
epoch,
isClaimable,
isPayoutGhst,
stnkSymbol,
ghstSymbol,
tokenPrice,
isLegacy,
}) => {
const ClaimInfo = ({ setConfirmationModalOpen, prepareBalance, claim, epoch, isClaimable, isPayoutGhst, stnkSymbol, ghstSymbol }) => {
return (
<TableBody>
<TableRow>
<TableCell style={{ padding: "8px 8px 8px 0" }}>
<Box display="flex" flexDirection="row" alignItems="center" style={{ whiteSpace: "nowrap" }}>
<Token
chainTokenName={chainSymbol}
name={isPayoutGhst ? ghstSymbol : stnkSymbol}
/>
<Token key={isPayoutGhst ? ghstSymbol : stnkSymbol} name={isPayoutGhst ? ghstSymbol : stnkSymbol} />
<Box marginLeft="14px" marginRight="10px">
<Typography>{isPayoutGhst ? ghstSymbol : stnkSymbol}</Typography>
</Box>
@ -223,10 +176,7 @@ const ClaimInfo = ({
</TableCell>
<TableCell style={{ padding: "8px 8px 8px 0" }}>
<Typography gutterBottom={false} style={{ lineHeight: 1.4 }}>
{formatCurrency(prepareBalance, 5, isPayoutGhst ? ghstSymbol : stnkSymbol)}
</Typography>
<Typography variant="body2" gutterBottom={false} style={{ lineHeight: 1.4 }}>
{formatCurrency(prepareBalance * tokenPrice, 2)}
{`${formatNumber(prepareBalance, 5)} ${isPayoutGhst ? ghstSymbol : stnkSymbol}`}
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 8px 8px 0" }}>
@ -236,7 +186,6 @@ const ClaimInfo = ({
<ActionButtons
setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable}
isLegacy={isLegacy}
/>
</TableCell>
</TableRow>
@ -244,18 +193,7 @@ const ClaimInfo = ({
);
};
const MobileClaimInfo = ({
setConfirmationModalOpen,
prepareBalance,
epoch,
claim,
isPayoutGhst,
isClaimable,
ghstSymbol,
stnkSymbol,
tokenPrice,
isLegacy,
}) => {
const MobileClaimInfo = ({ setConfirmationModalOpen, prepareBalance, epoch, claim, isPayoutGhst, isClaimable, ghstSymbol, stnkSymbol }) => {
return (
<Box mt="10px">
<Box display="flex" flexDirection="row" alignItems="center" style={{ whiteSpace: "nowrap" }}>
@ -266,15 +204,10 @@ const MobileClaimInfo = ({
</Box>
<DataRow
title="Staked Amount"
title="Amount"
balance={formatNumber(prepareBalance, 7)}
/>
<DataRow
title="Price Estimation"
balance={formatCurrency(prepareBalance * tokenPrice, 2)}
/>
<DataRow
title="Claimed in"
balance={prettifySecondsInDays(epoch.length * (claim.expiry - epoch.number))}
@ -284,13 +217,12 @@ const MobileClaimInfo = ({
isSmallScreen={true}
setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable}
isLegacy={isLegacy}
/>
</Box>
);
};
const ActionButtons = ({ setConfirmationModalOpen, isLegacy, isSmallScreen = false, isClaimable = false }) => {
const ActionButtons = ({ setConfirmationModalOpen, isSmallScreen = false, isClaimable = false }) => {
return (
<Box
display="flex"
@ -311,8 +243,8 @@ const ActionButtons = ({ setConfirmationModalOpen, isLegacy, isSmallScreen = fal
fullWidth={isSmallScreen}
loading={false}
sx={{ flexGrow: 1 }}
onClick={() => setConfirmationModalOpen(false)}
disabled={isLegacy && isClaimable}
onClick={() => setConfirmationModalOpen(true)}
disabled={isClaimable}
>
Claim
</PrimaryButton>

View File

@ -9,7 +9,7 @@ import {
Box,
useMediaQuery,
} from "@mui/material";
import { useNavigate, useParams, createSearchParams } from "react-router-dom";
import { useNavigate, createSearchParams } from "react-router-dom";
import Paper from "../../../components/Paper/Paper";
import { SecondaryButton } from "../../../components/Button";
@ -17,15 +17,15 @@ import TokenStack from "../../../components/TokenStack/TokenStack";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { formatCurrency } from "../../../helpers";
import { tokenNameConverter } from "../../../helpers/tokenConverter";
import { isNetworkLegacy } from "../../../constants";
import { useLpValuation } from "../../../hooks/treasury";
import { useTotalSupply, useTokenSymbol } from "../../../hooks/tokens";
import { EMPTY_ADDRESS, FTSO_ADDRESSES } from "../../../constants/addresses";
import { RESERVE_ADDRESSES, FTSO_ADDRESSES } from "../../../constants/addresses";
const FarmPools = ({ chainId, chainSymbol }) => {
const FarmPools = ({ chainId }) => {
const isSmallScreen = useMediaQuery("(max-width: 775px)");
const { network } = useParams();
const { totalSupply: reserveFtsoUniTotalSupply } = useTotalSupply(chainId, "RESERVE_FTSO");
const reserveFtsoUniValuation = useLpValuation(chainId, "RESERVE_FTSO", reserveFtsoUniTotalSupply._value);
@ -35,15 +35,14 @@ const FarmPools = ({ chainId, chainSymbol }) => {
const pools = [
{
icons: [tokenNameConverter(chainId, reserveSymbol), "FTSO"],
chainTokenNames: [undefined, chainSymbol],
name: `${reserveSymbol}-${ftsoSymbol}`,
icons: ["FTSO", isNetworkLegacy(chainId) ? "GDAI" : tokenNameConverter(chainId, reserveSymbol)],
name: `${ftsoSymbol}-${reserveSymbol}`,
dex: "Uniswap V2",
url: `/${network}/dex/uniswap`,
url: "/dex/uniswap",
tvl: reserveFtsoUniValuation,
params: createSearchParams({
pool: "true",
from: `${EMPTY_ADDRESS}`,
from: `${RESERVE_ADDRESSES[chainId]}`,
to: `${FTSO_ADDRESSES[chainId]}`,
})
},
@ -102,7 +101,7 @@ const FarmPoolCard = ({ id, pool }) => {
<Box id={id + `--bond`}>
<Box display="flex" alignItems="center" flexDirection="column" gap="20px">
<Box display="flex" alignItems="center" gap="20px">
<TokenStack chainTokenNames={pool.chainTokenNames} tokens={pool.icons} />
<TokenStack tokens={pool.icons} />
<Typography>{pool.name}</Typography>
</Box>
@ -128,7 +127,7 @@ const FarmPoolRow = ({ id, pool }) => {
<TableRow key={id} id={id + `--pool`} data-testid={id + `--pool`}>
<TableCell style={{ padding: "8px 0" }}>
<Box display="flex" alignItems="center" gap="20px">
<TokenStack chainTokenNames={pool.chainTokenNames} tokens={pool.icons} />
<TokenStack tokens={pool.icons} />
<Typography>{pool.name}</Typography>
</Box>
</TableCell>

View File

@ -52,7 +52,7 @@ export const TotalDeposit = props => {
const _props = {
...props,
label: "Total Deposit",
tooltip: `Total reserves of native coins, LP tokens, and other assets in the ghostDAO treasury backing the entire circulating supply of ${props.stnkSymbol}.`,
tooltip: `The total stablecoin reserves in the ghostDAO treasury backing the entire circulating supply of ${props.stnkSymbol}.`,
};
if (deposit) _props.metric = `${formatCurrency(deposit, 2)}`;

View File

@ -5,7 +5,6 @@ import { useAccount } from "wagmi";
import { StakeInputArea } from "./StakeInputArea";
export const StakeArea = ({
chainSymbol,
upperToken,
setUpperToken,
bottomToken,
@ -20,7 +19,6 @@ export const StakeArea = ({
return (
<Box width="100%">
<StakeInputArea
chainSymbol={chainSymbol}
upperToken={upperToken}
setUpperToken={setUpperToken}
bottomToken={bottomToken}

View File

@ -7,9 +7,8 @@ import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import GhostStyledIcon from "../../../components/Icon/GhostIcon";
import Modal from "../../../components/Modal/Modal";
import Metric from "../../../components/Metric/Metric";
import Token from "../../../components/Token/Token";
import { PrimaryButton, SecondaryButton } from "../../../components/Button";
import Metric from "../../../components/Metric/Metric";
import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard";
import { useWarmupLength, stake, unstake, wrap, unwrap } from "../../../hooks/staking";
@ -33,19 +32,15 @@ const StakeConfirmationModal = (props) => {
<FormControlLabel
control={
<Checkbox
data-testid="acknowledge-stake-warm-up"
data-testid="acknowledge-warmup"
checked={acknowledgedWarmup}
onChange={event => setAcknowledgedWarmup(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={
<Typography variant="body2">
{`I understand the ${props.ftsoSymbol} Im staking will only be available to claim ${warmupLength.toString()} epochs after my transaction is confirmed`}
</Typography>
}
/>
label={`I understand the ${props.ftsoSymbol} Im staking will only be available to claim ${warmupLength.toString()} epochs after my transaction is confirmed`}
/>
</Box>
</>
);
@ -60,7 +55,7 @@ const StakeConfirmationModal = (props) => {
fullWidth
href="https://ghostchain.io/ghostdao_litepaper"
>
Why is there a warm-up?
Why is there a warmup?
</SecondaryButton>
</>
);
@ -74,7 +69,7 @@ const StakeConfirmationModal = (props) => {
const actionAmount = new DecimalBigNumber(props.amount, props.spendDecimals);
let isRebase = false;
switch (props.action.toUpperCase()) {
switch (props.action) {
case "STAKE":
isRebase = props.bottomToken === props.stnkSymbol;
await stake(
@ -128,9 +123,12 @@ const StakeConfirmationModal = (props) => {
return (
<Modal
data-testid="stake-confirmation-modal"
maxWidth="406px"
maxWidth="476px"
topLeft={<></>}
headerContent={
<Typography variant="h4">{`Confirm ${props.action}`}</Typography>
<Box display="flex" flexDirection="row">
<Typography variant="h5">{`Confirm ${props.action}`}</Typography>
</Box>
}
open={props.open}
onClose={() => isPending ? {} : props.onClose()}
@ -138,52 +136,32 @@ const StakeConfirmationModal = (props) => {
>
<Box display="flex" flexDirection="column">
<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Box height="170px" display="flex" flexDirection="column">
<Box display="flex" flexDirection="column">
<Metric
label={`To ${props.action}`}
label={`Assets to Spend`}
metric={formatNumber(new DecimalBigNumber(props.amount, props.spendDecimals), 5)}
/>
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center">
<Token
parentSx={{
height: "80px",
"& svg:nth-of-type(2)": {
transform: "translate(50%, -30%)",
zIndex: 100,
}
}}
sx={{ transform: "scale(1.65)", }}
chainTokenName={props.chainSymbol}
name={props.upperToken}
/>
<Typography>{props.upperToken}</Typography>
<Box display="flex" flexDirection="row" justifyContent="center">
<Typography>
{props.upperToken}
</Typography>
</Box>
</Box>
<GhostStyledIcon component={ArrowRightIcon} />
<Box display="flex" flexDirection="column">
<Metric
label="To Receive"
label="Assets to Receive"
metric={formatNumber(new DecimalBigNumber(props.receiveAmount, props.receiveDecimals), 5)}
/>
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center">
<Token
parentSx={{
height: "80px",
"& svg:nth-of-type(2)": {
transform: "translate(50%, -30%)",
zIndex: 100,
}
}}
sx={{ transform: "scale(1.65)", }}
chainTokenName={props.chainSymbol}
name={props.upperToken === props.ftsoSymbol ? props.ghstSymbol : props.ftsoSymbol}
/>
<Typography>{props.bottomToken}</Typography>
<Box display="flex" flexDirection="row" justifyContent="center">
<Typography>
{props.bottomToken}
</Typography>
</Box>
</Box>
</Box>
<Box sx={{ marginTop: "1.5rem" }}>
<Box sx={{ marginTop: "2rem" }}>
{acknowledgedWarmup || props.action !== "STAKE" ?
<TokenAllowanceGuard
spendAmount={new DecimalBigNumber(props.amount, props.spendDecimals)}

View File

@ -9,7 +9,7 @@ const StakeInfoText = () => {
fontSize="0.875em"
lineHeight="15px"
>
Staking generates rebase yields governed by ghostDAO monetary policy and backed by bonds, Protocol Owned Liquidity (POL), and allocators.
Staking drives value accrual, rewarding stakers with rebase yields that fluctuate based on ghostDAO monetary policy and are backed by bond proceeds.
&nbsp;<Link
color={theme.colors.primary[300]}
href="https://ghostchain.io/ghostdao_litepaper"

View File

@ -1,7 +1,7 @@
import { Avatar, Box, Link } from "@mui/material";
import { styled } from "@mui/material/styles";
import React, { useEffect, useState, useCallback } from "react";
import React, { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
@ -23,7 +23,6 @@ import {
STAKING_ADDRESSES,
} from "../../../constants/addresses";
import { useCurrentIndex } from "../../../hooks/staking";
import { useLocalStorage } from "../../../hooks/localstorage";
import { useBalance, useTokenSymbol } from "../../../hooks/tokens";
import { formatNumber } from "../../../helpers";
@ -68,7 +67,6 @@ const StyledBox = styled(Box)(({ theme }) => ({
}));
export const StakeInputArea = ({
chainSymbol,
upperToken,
setUpperToken,
bottomToken,
@ -84,11 +82,20 @@ export const StakeInputArea = ({
const [bottomTokenModalOpen, setBottomTokenModalOpen] = useState(false);
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
const { getStorageValue, setStorageValue } = useLocalStorage();
const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("stake-decimals")
? localStorage.getItem("stake-decimals")
: "5"
);
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "stake-decimals", "5"));
const [isClaim, setIsClaim] = useState(() => getStorageValue(chainId, address, "stake-isClaim", true));
const [isTrigger, setIsTrigger] = useState(() => getStorageValue(chainId, address, "stake-isTrigger", true));
const [isClaim, setIsClaim] = useState(localStorage.getItem("stake-isClaim")
? localStorage.getItem("stake-isClaim") === 'true'
: true
);
const [isTrigger, setIsTrigger] = useState(localStorage.getItem("stake-isTrigger")
? localStorage.getItem("stake-isTrigger") === 'true'
: true
);
const [amount, setAmount] = useState("");
const [receiveAmount, setReceiveAmount] = useState("");
@ -103,22 +110,23 @@ export const StakeInputArea = ({
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const setIsClaimInner = useCallback((value) => {
const setIsClaimInner = (value) => {
setIsClaim(value);
setStorageValue(chainId, address, "stake-isClaim", value);
}, [chainId, address]);
localStorage.setItem("stake-isClaim", value);
const setIsTriggerInner = useCallback((value) => {
}
const setIsTriggerInner = (value) => {
setIsTrigger(value);
setStorageValue(chainId, address, "stake-isTrigger", value);
}, [chainId, address]);
localStorage.setItem("stake-isTrigger", value);
}
const setFormatDecimalsInner = useCallback((value) => {
const setFormatDecimalsInner = (value) => {
if (Number(value) <= 17) {
setFormatDecimals(value);
setStorageValue(chainId, address, "stake-decimals", value);
localStorage.setItem("staking-decimals", value);
}
}, [chainId, address]);
}
useEffect(() => {
const innerBalance = upperToken === ghstSymbol ?
@ -199,7 +207,7 @@ export const StakeInputArea = ({
id={`${tokenName.toLowerCase()}-input`}
token={tokenName}
tokenName={realTokenName}
chainTokenName={chainSymbol}
tokenOnClick={() => handleModal(true)}
inputProps={{ "data-testid": `${tokenName.toLowerCase()}-input`, min: "0" }}
value={tokenAmount}
onChange={event => setAmount(event.target.value)}
@ -234,6 +242,36 @@ export const StakeInputArea = ({
/>
</Box>
{upperTokenModalOpen && (
<TokenModal
open={upperTokenModalOpen}
handleSelect={data => handleTokenModalInput(data.token, data.isUpper)}
handleClose={() => setUpperTokenModalOpen(false)}
ftsoBalance={formatNumber(ftsoBalance, formatDecimals)}
stnkBalance={formatNumber(stnkBalance, formatDecimals)}
ghstBalance={formatNumber(ghstBalance, formatDecimals)}
isUpper={true}
ftsoSymbol={ftsoSymbol}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
/>
)}
{bottomTokenModalOpen && (
<TokenModal
open={bottomTokenModalOpen}
handleSelect={data => handleTokenModalInput(data.token, data.isUpper)}
handleClose={() => setBottomTokenModalOpen(false)}
ftsoBalance={formatNumber(ftsoBalance, formatDecimals)}
stnkBalance={formatNumber(stnkBalance, formatDecimals)}
ghstBalance={formatNumber(ghstBalance, formatDecimals)}
tokenToExclude={upperToken}
isUpper={false}
ftsoSymbol={ftsoSymbol}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
/>
)}
<Box>
<PrimaryButton
fullWidth
@ -241,7 +279,11 @@ export const StakeInputArea = ({
loading={false}
disabled={exceedsAmount}
>
{exceedsAmount ? "Exceeds amount" : action }
{exceedsAmount ?
"Exceeds amount"
:
action[0] + String(action.toLowerCase()).slice(1)
}
</PrimaryButton>
</Box>
</Box>
@ -258,7 +300,6 @@ export const StakeInputArea = ({
onTriggerRebaseChange={setIsTriggerInner}
/>
<StakeConfirmationModal
chainSymbol={chainSymbol}
open={confirmationModalOpen}
onClose={() => closeAndUpdate()}
chainId={chainId}

View File

@ -35,7 +35,7 @@ const StakeSettingsModal = props => {
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={<Typography variant="body2">Always try to claim during stake, works only if warm-up period is zero</Typography>}
label={<Typography variant="body2">Always try to claim during stake, works only if warmup period is zero</Typography>}
/>
</Box>
</Box>

View File

@ -1,6 +1,6 @@
import { Box, Container, Grid, useMediaQuery, useTheme } from "@mui/material";
import { memo, useEffect, useMemo, useState } from "react";
import { Outlet, Route, Routes, useSearchParams, useLocation } from "react-router-dom";
import { Outlet, Route, Routes, useSearchParams } from "react-router-dom";
import ReactGA from "react-ga4";
import {
@ -12,7 +12,6 @@ import {
CurrentIndex,
} from "./components/Metric";
import TokenInfo from "./components/TokenInfo";
import ProtocolDetails from "./components/ProtocolDetails";
import Paper from "../../components/Paper/Paper";
import PageTitle from "../../components/PageTitle/PageTitle";
@ -61,7 +60,7 @@ const MetricsDashboard = ({ chainId }) => {
</Paper>
<Paper style={{ padding: "0px" }} fullWidth={true} >
<ProtocolDetails theme={theme} isMobileScreen={isMobileScreen} chainId={chainId} />
<TokenInfo isMobileScreen={isMobileScreen} chainId={chainId} />
</Paper>
</Grid>
</Grid>
@ -74,15 +73,13 @@ const TreasuryDashboard = ({ chainId }) => {
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const location = useLocation();
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location]);
ReactGA.send({ hitType: "pageview", page: "/dashboard" });
}, []);
return (
<>
<PageTitle name="Protocol Overview" subtitle="Confirm protocol health by monitoring supply and reserves" />
<PageTitle name="Protocol Metrics" subtitle="Confirm protocol health by auditing supply and reserves" />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",

View File

@ -84,7 +84,7 @@ export const FatsoBacking = props => {
const _props = {
...props,
label: `Backing per ${props.ftsoSymbol}`,
tooltip: `The total amount of native coins, LP tokens, and other assets held by the ghostDAO treasury to support the value of each ${props.ftsoSymbol} in circulation.`
tooltip: `The total amount of stablecoins held by the ghostDAO treasury to support the value of each ${props.ftsoSymbol} in circulation.`
};
if (backing) _props.metric = formatCurrency(backing, 2);

View File

@ -1,136 +0,0 @@
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 { useLiveBonds } from "../../../hooks/bonds/index";
import { useGatekeeperApy } from "../../../hooks/staking";
import { useTokenSymbol, useBalance } 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 { liveBonds } = useLiveBonds(chainId);
const { gatekeepedApy, apyInner } = useGatekeeperApy(chainId);
const maxBondDiscountTest = useMemo(() => {
const sortedGhostBonds = liveBonds.filter((bond) => !bond.isSoldOut)
if (sortedGhostBonds?.length === 0) return "Coming Up";
const maxDiscountBond = sortedGhostBonds.reduce((prev, current) =>
(prev.discount > current.discount) ? prev : current
);
const maxDiscount = maxDiscountBond.discount.mul(new DecimalBigNumber(100, 0));
return `Up to ${formatNumber(maxDiscount, 0)}% Discount`;
}, [liveBonds]);
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={maxBondDiscountTest}
description={`Bonding strategy grows Treasury and builds Protocol Owned Liquidity (POL) by allowing users to 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, 2)}% APY`}
description={`Staking enables (3, 3) coordination by aligning long-term incentives, sustainably backed by (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(gatekeepedApy, 2)}% APY`}
description={`Staking\u00B2 strategy further deepens long-term incentives powered by sustainable cross-chain bridging revenue. ${bridgeNumbers} Stake\u00B2 ${csprSymbol} to receive organic APY with no warmup and no dilution.`}
/>
</Box>
</Grid>
)
}
export default ProtocolDetails;

View File

@ -1,23 +1,23 @@
import { Grid, Box, Typography, useTheme } from "@mui/material";
import { useAccount, useConfig, useBalance as useBalanceNative } from "wagmi";
import { useNavigate, useParams, createSearchParams } from "react-router-dom";
import { useNavigate, createSearchParams } from "react-router-dom";
import Token from "../../../components/Token/Token";
import { SecondaryButton } from "../../../components/Button";
import { formatNumber, formatCurrency } from "../../../helpers";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { isNetworkLegacy } from "../../../constants"
import { useBalance, useTokenSymbol } from "../../../hooks/tokens";
import {
useFtsoPrice,
useStnkPrice,
useGhstPrice,
useReservePrice,
useNativePrice,
} from "../../../hooks/prices";
import { tokenNameConverter } from "../../../helpers/tokenConverter";
import { EMPTY_ADDRESS, WETH_ADDRESSES } from "../../../constants/addresses";
const TokenTab = ({ isMobileScreen, theme, tokenName, tokenUrl, tokenUrlParams, balance, price, description }) => {
const navigate = useNavigate();
const actualBalance = balance ? balance : new DecimalBigNumber(0, 0);
@ -58,13 +58,10 @@ const TokenTab = ({ isMobileScreen, theme, tokenName, tokenUrl, tokenUrlParams,
<Box display="flex" justifyContent="center" width="100%">
<SecondaryButton
onClick={() => tokenUrlParams
? navigate({
pathname: tokenUrl,
search: tokenUrlParams.toString()
})
: window.open(tokenUrl, '_blank')
}
onClick={() => navigate({
pathname: tokenUrl,
search: tokenUrlParams.toString()
})}
fullWidth
>
Get {tokenName}
@ -78,7 +75,6 @@ const TokenTab = ({ isMobileScreen, theme, tokenName, tokenUrl, tokenUrlParams,
const TokenInfo = ({ chainId, isMobileScreen }) => {
const theme = useTheme();
const { network } = useParams();
const { address } = useAccount();
const config = useConfig();
@ -87,15 +83,18 @@ const TokenInfo = ({ chainId, isMobileScreen }) => {
const nativePrice = useNativePrice(chainId);
const ftsoPrice = useFtsoPrice(chainId);
const stnkPrice = useStnkPrice(chainId);
const ghstPrice = useGhstPrice(chainId);
const reservePrice = useReservePrice(chainId);
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { data: nativeBalance } = useBalanceNative({ address });
const { balance: ftsoBalance, contractAddress: ftsoAddress } = useBalance(chainId, "FTSO", address);
const { balance: stnkBalance, contractAddress: stnkAddress } = useBalance(chainId, "STNK", address);
const { balance: ghstBalance, contractAddress: ghstAddress } = useBalance(chainId, "GHST", address);
const { balance: reserveBalance, contractAddress: reserveAddress } = useBalance(chainId, "RESERVE", address);
@ -104,7 +103,7 @@ const TokenInfo = ({ chainId, isMobileScreen }) => {
<Box display="flex" flexWrap="wrap" justifyContent="space-between" mt="10px" gap="25px">
<TokenTab
isMobileScreen={isMobileScreen}
tokenUrl={`/${network}/dex/uniswap`}
tokenUrl="/dex/uniswap"
tokenUrlParams={createSearchParams({
from: `${reserveAddress}`,
to: `${ftsoAddress}`,
@ -117,7 +116,20 @@ const TokenInfo = ({ chainId, isMobileScreen }) => {
/>
<TokenTab
isMobileScreen={isMobileScreen}
tokenUrl={`/${network}/dex/uniswap`}
tokenUrl="/dex/uniswap"
tokenUrlParams={createSearchParams({
from: `${reserveAddress}`,
to: `${stnkAddress}`,
})}
theme={theme}
tokenName={stnkSymbol}
balance={stnkBalance}
price={stnkPrice}
description={`${stnkSymbol} is a receipt for staked ${ftsoSymbol}, growing with staking rewards. When unstaked, its burned for ${ftsoSymbol} at a 1:1 ratio.`}
/>
<TokenTab
isMobileScreen={isMobileScreen}
tokenUrl="/dex/uniswap"
tokenUrlParams={createSearchParams({
from: `${reserveAddress}`,
to: `${ghstAddress}`,
@ -126,31 +138,36 @@ const TokenInfo = ({ chainId, isMobileScreen }) => {
tokenName={ghstSymbol}
balance={ghstBalance}
price={ghstPrice}
description={`${ghstSymbol} is the governance token enabling pure Web3 cross-chain magic. 1 ${ghstSymbol} = 1 ${ftsoSymbol} x Current Index.`}
description={`${ghstSymbol} enables ghostDAO to have on-chain governance and to be truly cross-chain. ${ghstSymbol} Price = ${ftsoSymbol} Price x Current Index.`}
/>
<TokenTab
isMobileScreen={isMobileScreen}
tokenUrl={"https://ghostchain.io/faucet/"}
theme={theme}
tokenName={nativeSymbol}
balance={new DecimalBigNumber(nativeBalance?.value ?? 0n, 18)}
price={reservePrice}
description={`${nativeSymbol} is the native currency of the ${networkName} Network, functioning as the backing asset for the ghostDAO.`}
/>
<TokenTab
isMobileScreen={isMobileScreen}
tokenUrl={`/${network}/dex/uniswap`}
tokenUrlParams={createSearchParams({
from: `${EMPTY_ADDRESS}`,
to: `${WETH_ADDRESSES[chainId]}`,
})}
tokenUrl={isNetworkLegacy(chainId) ? "/faucet" : "/wrapper"}
tokenUrlParams=""
theme={theme}
tokenName={reserveSymbol}
balance={reserveBalance}
price={reservePrice}
description={`${ftsoSymbol} is backed by a treasury reserve of crypto assets, with ${reserveSymbol} being the primary and most liquid asset.`}
description={isNetworkLegacy(chainId)
? `${ftsoSymbol} is backed by a treasury reserve of crypto assets, with ${reserveSymbol} being the primary and most liquid asset.`
: `${reserveSymbol} (Wrapped ${nativeSymbol}) is an ERC-20 token that represents ${nativeSymbol} and is pegged 1:1 to the value of ${nativeSymbol}.`
}
/>
</Box>
{!isNetworkLegacy(chainId) && (
<Box width="100%" mt="25px">
<TokenTab
isMobileScreen={true}
tokenUrl={isNetworkLegacy(chainId) ? "/faucet" : "/wrapper"}
tokenUrlParams=""
theme={theme}
tokenName={nativeSymbol}
balance={new DecimalBigNumber(nativeBalance?.value ?? 0n, 18)}
price={reservePrice}
description={`${nativeSymbol} is the native currency of the ${networkName} Network, functioning as the backing asset for the ghostDAO.`}
/>
</Box>
)}
</Grid>
)
}

View File

@ -2,7 +2,6 @@ import { useState, useEffect, useMemo } from "react";
import { Box, Container, Typography, useMediaQuery } from "@mui/material";
import { useConfig, useBalance } from "wagmi";
import { Helmet } from "react-helmet";
import { useLocation } from "react-router-dom";
import ReactGA from "react-ga4";
import PageTitle from "../../components/PageTitle/PageTitle";
@ -31,8 +30,6 @@ const WethWrapper = ({ chainId, address, config, connect }) => {
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const location = useLocation();
const [chainName, setChainName] = useState("");
const [isMint, setIsMint] = useState(true);
const [isPending, setIsPending] = useState(false);
@ -53,8 +50,8 @@ const WethWrapper = ({ chainId, address, config, connect }) => {
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location])
ReactGA.send({ hitType: "pageview", page: "/wrapper" });
}, [])
useEffect(() => {
const value = nativeBalance ? nativeBalance.value : 0n;

View File

@ -1,6 +1,6 @@
export function shorten(str, first = 6, second = -4) {
export function shorten(str) {
if (str.length < 10) return str;
return `${str.slice(0, first)}...${str.slice(second)}`;
return `${str.slice(0, 6)}...${str.slice(str.length - 4)}`;
}
export function capitalize(str) {
@ -39,7 +39,7 @@ export const formatNumber = (number, precision = 0) => {
};
export const sortBondsByDiscount = (bonds) => {
return Array.from(bonds).sort((a, b) => (a.discount.gt(b.discount) ? -1 : 1));
return Array.from(bonds).filter((bond) => !bond.isSoldOut).sort((a, b) => (a.discount.gt(b.discount) ? -1 : 1));
};
export const timeConverter = (time, max = 7200, maxText = "long ago") => {
@ -52,16 +52,3 @@ export const timeConverter = (time, max = 7200, maxText = "long ago") => {
return `${mins}m ${secs < 10 ? '0' : ''}${secs}s`;
}
}
export const bigIntSqrt = (n) => {
if (n < 0n) return 0n;
if (n < 2n) return n;
let x = n / 2n + 1n;
let y = (x + n / x) / 2n;
while (y < x) {
x = y;
y = (x + n / x) / 2n;
}
return x;
}

View File

@ -1,11 +1,9 @@
export const tokenNameConverter = (chainId, name, address) => {
export const tokenNameConverter = (chainId, name) => {
if (name?.toUpperCase() === "WETH") {
switch (chainId) {
case 63:
name = "wmETC"
break;
default:
name = "wETH";
}
}
return name;

View File

@ -1,6 +1,9 @@
import { useReadContract, useReadContracts } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast";
import { config } from "../../config";
import {
BOND_DEPOSITORY_ADDRESSES,
DAO_TREASURY_ADDRESSES,
@ -10,26 +13,14 @@ import { abi as BondAbi } from "../../abi/GhostBondDepository.json";
import { abi as TreasuryAbi } from "../../abi/GhostTreasury.json";
import { abi as BondingCalculatorAbi } from "../../abi/GhostBondingCalculator.json";
import { useReservePrice, useFtsoPrice } from "../prices";
import { useOrinalCoefficient } from "../treasury";
import { useFtsoPrice } from "../prices";
import { useTokenSymbol, useTokenSymbols } from "../tokens";
import {
getTokenAddress,
getTokenIcons,
getChainTokenNames,
getBondNameDisplayName,
getTokenPurchaseLink,
executeOnChainTransaction
} from "../helpers";
import { getTokenAddress, getTokenIcons, getBondNameDisplayName, getTokenPurchaseLink } from "../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { shorten } from "../../helpers";
import { tokenNameConverter } from "../../helpers/tokenConverter";
export const useLiveBonds = (chainId, chainName) => {
const ftsoPrice = useFtsoPrice(chainId);
const baseTokenPerUsd = useReservePrice(chainId);
const originalCoefficient = useOrinalCoefficient(chainId);
export const useLiveBonds = (chainId) => {
const baseTokenPerUsd = useFtsoPrice(chainId);
const { data: liveIndexesRaw, refetch } = useReadContract({
abi: BondAbi,
@ -124,7 +115,7 @@ export const useLiveBonds = (chainId, chainName) => {
const marketCapacity = markets?.at(index).result?.at(0) ? markets.at(index).result.at(0) : 0n;
const quoteTokenAddress = markets?.at(index).result?.at(1) ? markets.at(index).result.at(1) : "";
const capacityInQuote = markets?.at(index).result?.at(2) ? markets.at(index).result.at(2) : 0n;
const marketMaxPayout = markets?.at(index).result?.at(4) ? markets.at(index).result.at(4) : 0n;
const marketMaxPayout = markets?.at(index).result?.at(4) ? markets.at(index).result.at(4) : 0n;
const quoteTokenDecimals = metas?.at(index).result?.at(5) ? metas.at(index).result.at(5) : 18;
const quoteTokenPerUsd = quotePrices?.at(index).result
? new DecimalBigNumber(quotePrices.at(index).result * (10n ** 9n), quoteTokenDecimals)
@ -135,11 +126,10 @@ export const useLiveBonds = (chainId, chainName) => {
const quoteTokenSymbol = quoteTokenSymbols?.at(index).result ? quoteTokenSymbols.at(index).result : "";
const quoteTokenPerBaseToken = new DecimalBigNumber(marketPrice, 9);
const priceInUsd = quoteTokenPerBaseToken.mul(baseTokenPerUsd).mul(quoteTokenPerUsd).mul(markdown).div(originalCoefficient);
const discount = ftsoPrice._value > 0n
? ftsoPrice.sub(priceInUsd).div(ftsoPrice)
: new DecimalBigNumber("0", ftsoPrice._decimals);
const priceInUsd = quoteTokenPerUsd.mul(quoteTokenPerBaseToken).mul(markdown);
const discount = baseTokenPerUsd._value > 0n
? baseTokenPerUsd.sub(priceInUsd).div(baseTokenPerUsd)
: new DecimalBigNumber("0", baseTokenPerUsd._decimals);
const capacityInBaseToken = capacityInQuote
? new DecimalBigNumber(marketPrice > 0n ? marketCapacity / marketPrice : 0n, 9)
@ -155,34 +145,33 @@ export const useLiveBonds = (chainId, chainName) => {
);
const zero = new DecimalBigNumber(0n, 0);
const bondName = `${baseTokenSymbol}/${quoteTokenSymbol}`;
return {
id,
discount,
displayName: getBondNameDisplayName(chainId, quoteTokenAddress, baseTokenSymbol),
displayName: getBondNameDisplayName(chainId, bondName, quoteTokenAddress),
baseToken: {
name: baseTokenSymbol,
purchaseUrl: getTokenPurchaseLink(chainId, "", chainName),
tokenAddress: getTokenAddress(chainId, "FTSO"),
chainNames: getChainTokenNames(chainId, "FTSO"),
purchaseUrl: getTokenPurchaseLink(chainId, ""),
icons: ["FTSO"],
tokenAddress: getTokenAddress(chainId, "FTSO")
},
quoteToken: {
name: tokenNameConverter(chainId, quoteTokenSymbol),
purchaseUrl: getTokenPurchaseLink(chainId, quoteTokenAddress, chainName),
name: quoteTokenSymbol,
purchaseUrl: getTokenPurchaseLink(chainId, quoteTokenAddress),
icons: getTokenIcons(chainId, quoteTokenAddress),
chainNames: getChainTokenNames(chainId, quoteTokenAddress),
decimals: quoteTokenDecimals,
quoteTokenAddress,
},
duration: terms?.at(index).result?.at(3) ? terms.at(index).result.at(3) : 0,
vesting: terms?.at(index).result?.at(2) ? terms.at(index).result.at(2) : 0,
isFixedTerm: terms?.at(index).result?.at(0) ? terms.at(index).result.at(0) : true,
isSoldOut: capacityInBaseToken.eq(zero),
isSoldOut: capacityInBaseToken.eq(zero) || capacityInBaseToken.eq(zero),
price: {
inUsd: priceInUsd,
inBaseToken: quoteTokenPerBaseToken,
marketPriceInUsd: ftsoPrice
marketPriceInUsd: baseTokenPerUsd
},
capacity: {
inBaseToken: capacityInBaseToken,
@ -192,7 +181,7 @@ export const useLiveBonds = (chainId, chainName) => {
inBaseToken: maxPayoutInBaseToken,
inQuoteToken: maxPayoutInQuoteToken,
},
}
};
}).sort((a, b) => (a.id > b.id ? -1 : 1)) : [];
return { liveBonds, refetch };
}
@ -256,9 +245,8 @@ export const useNotes = (chainId, address) => {
return {
id,
quoteToken: {
name: tokenNameConverter(chainId, quoteTokenSymbol),
name: quoteTokenSymbol,
icons: getTokenIcons(chainId, quoteTokenAddress),
chainNames: getChainTokenNames(chainId, quoteTokenAddress),
},
vesting: terms?.at(index).result?.at(2) ? terms.at(index).result.at(2) : 0,
created: notesRaw?.at(index).result?.at(1) ? notesRaw.at(index).result.at(1) : 0,
@ -273,62 +261,75 @@ export const useNotes = (chainId, address) => {
return { notes, refetch };
}
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => {
const args = [bondId, amount, maxPrice, user, referral];
export const purchaseBond = async (chainId, bondId, amount, maxPrice, user, sender, referral) => {
const args = [
bondId,
amount,
maxPrice,
user,
referral
];
const messages = {
replacedMsg: "Bond transaction was replaced. Wait for inclusion please.",
successMsg: `Bond successfully purchased for ${shorten(user)}! Wait until it'll mature before claim.`,
errorMsg: "Bond transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction({
await executeOnChainTransaction(
chainId,
"deposit",
args,
messages,
abi: BondAbi,
address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName: "deposit",
account: user,
value: isNative ? amount : undefined
});
sender,
messages
);
}
export const redeem = async ({ chainId, user, isGhst, indexes }) => {
const args = [user, isGhst, indexes];
export const redeem = async (chainId, user, isGhst, indexes) => {
const args = [
user,
isGhst,
indexes
];
const messages = {
replacedMsg: "Redeem transaction was replaced. Wait for inclusion please.",
successMsg: `Address ${shorten(user)} redeemed ${indexes.length} bonds in ${isGhst ? "GHST" : "STNK"}!`,
errorMsg: `Redeem of ${indexes.length} bonds failed. Check logs for error detalization.`,
};
await executeOnChainTransaction({
await executeOnChainTransaction(
chainId,
"redeem",
args,
messages,
abi: BondAbi,
address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName: "redeem",
account: user,
});
user,
messages
);
}
export const forceRedeem = async ({ chainId, user, receiver, indexes }) => {
const args = [receiver, indexes];
const messages = {
replacedMsg: "Bond breakout transaction was replaced. Wait for inclusion please.",
successMsg: `Address ${shorten(user)} succesfully breakout for ${indexes.length} bonds.`,
errorMsg: `Breakout of ${indexes.length} bonds failed. Check logs for error detalization.`,
};
const executeOnChainTransaction = async (
chainId,
functionName,
args,
account,
messages
) => {
try {
const { request } = await simulateContract(config, {
abi: BondAbi,
address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName,
args,
account,
chainId
});
const txHash = await executeOnChainTransaction({
chainId,
args,
messages,
abi: BondAbi,
address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName: "forceRedeem",
account: user,
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast(messages.replacedMsg),
chainId
});
return txHash;
toast.success(messages.successMsg);
} catch (err) {
console.error(err);
toast.error(messages.errorMsg)
}
}

View File

@ -1,63 +0,0 @@
import { createContext, useContext, useState, useMemo } from "react";
import { claim } from "../../hooks/staking";
const emptyFunction = () => {};
const BreakoutModalContext = createContext();
export const useBreakoutModal = () => useContext(BreakoutModalContext);
export const BreakoutModalProvider = ({ children }) => {
const [isStakingOpened, setIsStakingOpened] = useState(false);
const [isClaimBondOpened, setIsClaimBondOpened] = useState(false);
const [activeTxIndex, setActiveTxIndex] = useState(-1);
const [warmupPeriod, setWarmupPeriod] = useState(0);
const [estimatedAmount, setEstimatedAmount] = useState(0);
const [defaultFunction, setDefaultFunction] = useState(emptyFunction);
const [executableFunction, setExecutableFunction] = useState(emptyFunction);
const breakoutFromStaking = ({ defaultFunction, toExecute, amount, warmupLeft }) => {
setIsStakingOpened(true);
setWarmupPeriod(warmupLeft);
setEstimatedAmount(amount);
setExecutableFunction(() => () => toExecute);
setDefaultFunction(() => () => defaultFunction);
}
const breakoutFromBonding = ({ defaultFunction, toExecute, amount, warmupLeft }) => {
setIsClaimBondOpened(true);
setWarmupPeriod(warmupLeft);
setEstimatedAmount(amount);
setExecutableFunction(() => () => toExecute);
setDefaultFunction(() => () => defaultFunction);
}
const isOpened = useMemo(() =>
isStakingOpened || isClaimBondOpened,
[isStakingOpened, isClaimBondOpened]);
const closeModal = () => {
setIsStakingOpened(false);
setIsClaimBondOpened(false);
setWarmupPeriod(0);
setDefaultFunction(emptyFunction);
setExecutableFunction(emptyFunction);
}
return (
<BreakoutModalContext.Provider value={{
isStakingOpened,
isClaimBondOpened,
activeTxIndex,
setActiveTxIndex,
warmupPeriod,
isOpened,
closeModal,
breakoutFromStaking,
breakoutFromBonding,
defaultFunction,
estimatedAmount,
executableFunction
}}>
{children}
</BreakoutModalContext.Provider>
)
}

View File

@ -8,24 +8,15 @@ import { useUnstableProvider } from "./UnstableProvider"
const MetadataProvider = createContext(null)
export const useMetadata = () => useContext(MetadataProvider)
const CACHE_VERSION = "v2"
export const MetadataProviderProvider = ({ children }) => {
const { client, chainId } = useUnstableProvider()
const { data: metadata } = useSWR(
client && chainId ? ["metadata", client, chainId] : null,
async ([_, client]) => {
const storageKey = `metadata-${chainId}-${CACHE_VERSION}`
const storageKey = `metadata-${chainId}`
const storedMetadata = sessionStorage.getItem(storageKey)
if (storedMetadata) return unifyMetadata(decAnyMetadata(storedMetadata))
Object.keys(sessionStorage).forEach(key => {
if (key.startsWith("metadata-")) {
sessionStorage.removeItem(key);
}
});
const metadata = await new Promise((resolve, reject) =>
client._request("state_getMetadata", [], {
onSuccess: resolve,

View File

@ -1,80 +1,54 @@
import { createContext, useEffect, useContext, useState, useMemo, useCallback, useRef } from "react"
import { createContext, useContext, useState, useMemo } from "react"
import { Unstable } from "@substrate/connect-discovery"
import { createClient } from "@polkadot-api/substrate-client"
import { getObservableClient } from "@polkadot-api/observable-client"
import useSWR from "swr"
const MAX_BLOCK_TIMEOUT = 15000
const DEFAULT_CHAIN_ID = "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3"
const UnstableProvider = createContext(null)
export const useUnstableProvider = () => useContext(UnstableProvider)
export const UnstableProviderProvider = ({ children }) => {
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
const [providerIndex, setProviderIndex] = useState(0);
const { data: providerDetails } = useSWR("getGhostProviders", () =>
Unstable.getSubstrateConnectExtensionProviders()
);
const providerDetail = useMemo(() => providerDetails?.at(providerIndex), [providerDetails, providerIndex]);
const [providerDetail, setProviderDetail] = useState();
const { data: provider } = useSWR(
() => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.provider` : null,
() => providerDetail ? providerDetail.provider : null
);
const { data: accounts } = useSWR(
() => providerDetail ? `providerDetail.${providerDetail.info.uuid}.provider.getAccounts(${chainId})` : null,
async () => {
if (!providerDetail) return []
const activeProvider = await providerDetail.provider;
return activeProvider.getAccounts(chainId);
},
)
const account = useMemo(() => {
if (Array.isArray(accounts)) return accounts?.at(0)?.address;
if (typeof accounts === 'string') return accounts?.address;
return undefined
}, [accounts])
const connectionState = useMemo(() => {
if (!providerDetail) return 'no-extension';
if (!provider) return 'loading';
const chains = provider.getChains();
if (chains[chainId]) return 'connected';
return 'wrong-network';
}, [providerDetail, provider, chainId]);
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
const client = useMemo(() => {
if (!provider || !chainId) return undefined;
const chain = provider.getChains()[chainId];
if (!chain) return undefined;
return createClient(chain.connect)
return createClient(chain.connect);
}, [provider, chainId]);
const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]);
const observableClient = useMemo(() => {
return client ? getObservableClient(client) : undefined;
}, [client]);
const value = useMemo(() => ({
isExtensionMissing: connectionState === "no-extension",
providerDetails,
providerDetail,
connectProviderByIndex: setProviderIndex,
chainId,
account,
client,
setChainId,
chainHead$
}), [providerDetails, providerDetail, chainId, client, account, chainHead$]);
const chainHead$ = useMemo(() => {
return observableClient ? observableClient.chainHead$() : undefined;
}, [observableClient]);
return (
<UnstableProvider.Provider value={value}>
<UnstableProvider.Provider
value={{
providerDetails,
providerDetail,
connectProviderDetail: setProviderDetail,
provider,
chainId,
client,
setChainId,
chainHead$
}}
>
{children}
</UnstableProvider.Provider>
);

View File

@ -12,7 +12,3 @@ export * from "./useBlockCommitments";
export * from "./useApplauseDetails";
export * from "./useBabeSlots";
export * from "./useErasTotalStaked";
export * from "./useLatestBlockNumber";
export * from "./useEraIndex";
export * from "./useSystemAccount";
export * from "./useChainSpec";

View File

@ -1,31 +0,0 @@
import useSWR from "swr"
import { useUnstableProvider } from "./UnstableProvider"
export const useChainSpec = () => {
const { client } = useUnstableProvider()
const { data: chainSpecV1 } = useSWR(
client ? ["chainSpec_v1_", client] : null,
async ([prefix, client]) => {
const methods = ["chainName", "properties", "genesisHash"]
const responses = await Promise.all(methods.map((method) => {
return new Promise((resolve, reject) =>
client._request(`${prefix}${method}`, [], {
onSuccess: resolve,
onError: reject,
}),
)
.catch((_) => undefined)
}))
return {
chainName: responses?.at(0),
properties: responses?.at(1),
genesisHash: responses?.at(2)
}
},
{
revalidateOnFocus: false,
refreshInterval: 0
}
)
return chainSpecV1
}

View File

@ -1,41 +0,0 @@
import useSWRSubscription from "swr/subscription"
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
import { distinct, filter, map, mergeMap } from "rxjs"
import { useUnstableProvider } from "./UnstableProvider"
import { useMetadata } from "./MetadataProvider"
export const useEraIndex = () => {
const { chainHead$, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: eraIndex } = useSWRSubscription(
chainHead$ && chainId && metadata
? ["eraIndex", chainHead$, chainId, metadata]
: null,
([_, chainHead$, chainId, metadata], { next }) => {
const { finalized$, storage$ } = chainHead$
const subscription = finalized$.pipe(
filter(Boolean),
mergeMap((blockInfo) => {
const builder = getDynamicBuilder(getLookupFn(metadata))
const eraIndex = builder.buildStorage("Staking", "ActiveEra")
return storage$(blockInfo?.hash, "value", () =>
eraIndex?.keys.enc()
).pipe(
filter(Boolean),
distinct(),
map((value) => eraIndex?.value.dec(value))
)
}),
)
.subscribe({
next(eraIndex) {
next(null, eraIndex)
},
error: next,
})
return () => subscription.unsubscribe()
}
)
return eraIndex;
}

View File

@ -5,14 +5,14 @@ import { distinct, filter, map, mergeMap } from "rxjs"
import { useUnstableProvider } from "./UnstableProvider"
import { useMetadata } from "./MetadataProvider"
export const useErasTotalStake = ({ epochIndex }) => {
export const useErasTotalStake = ({ eraIndex }) => {
const { chainHead$, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: eraTotalStake } = useSWRSubscription(
chainHead$ && chainId && metadata
? ["eraTotalStake", chainHead$, epochIndex, chainId, metadata]
? ["eraTotalStake", chainHead$, eraIndex, chainId, metadata]
: null,
([_, chainHead$, epochIndex, chainId, metadata], { next }) => {
([_, chainHead$, eraIndex, chainId, metadata], { next }) => {
const { finalized$, storage$ } = chainHead$
const subscription = finalized$.pipe(
filter(Boolean),
@ -20,7 +20,7 @@ export const useErasTotalStake = ({ epochIndex }) => {
const builder = getDynamicBuilder(getLookupFn(metadata))
const eraTotalStake = builder.buildStorage("Staking", "ErasTotalStake")
return storage$(blockInfo?.hash, "value", () =>
eraTotalStake?.keys.enc(epochIndex)
eraTotalStake?.keys.enc(eraIndex)
).pipe(
filter(Boolean),
distinct(),

View File

@ -1,18 +0,0 @@
import { useState, useEffect } from "react";
import useSWRSubscription from "swr/subscription"
import { useUnstableProvider } from "./UnstableProvider"
export const useLatestBlockNumber = () => {
const { chainHead$ } = useUnstableProvider();
const [blockNumber, setBlockNumber] = useState(null);
useEffect(() => {
if (!chainHead$) return;
const subscription = chainHead$.best$.subscribe((block) => {
setBlockNumber(block.number);
});
return () => subscription.unsubscribe();
}, [chainHead$]);
return blockNumber;
}

View File

@ -1,41 +0,0 @@
import useSWRSubscription from "swr/subscription"
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
import { distinct, filter, map, mergeMap } from "rxjs"
import { useUnstableProvider } from "./UnstableProvider"
import { useMetadata } from "./MetadataProvider"
export const useSystemAccount = ({ account }) => {
const { chainHead$, chainId } = useUnstableProvider()
const metadata = useMetadata()
const { data: systemAccount } = useSWRSubscription(
chainHead$ && chainId && account && metadata
? ["systemAccount", chainHead$, chainId, account, metadata]
: null,
([_, chainHead$, chainId, account, metadata], { next }) => {
const { finalized$, storage$ } = chainHead$
const subscription = finalized$.pipe(
filter(Boolean),
mergeMap((blockInfo) => {
const builder = getDynamicBuilder(getLookupFn(metadata))
const storageAccount = builder.buildStorage("System", "Account")
return storage$(blockInfo?.hash, "value", () =>
storageAccount?.keys.enc(account)
).pipe(
filter(Boolean),
distinct(),
map((value) => storageAccount?.value.dec(value))
)
}),
)
.subscribe({
next(systemAccount) {
next(null, systemAccount)
},
error: next,
})
return () => subscription.unsubscribe()
}
)
return systemAccount
}

View File

@ -1,7 +1,9 @@
import { useMemo } from "react";
import { useReadContract, useReadContracts } from "wagmi";
import { keccak256, stringToBytes } from 'viem'
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast";
import { keccak256, stringToBytes } from 'viem'
import { config } from "../../config";
import {
GHOST_GOVERNANCE_ADDRESSES,
@ -12,24 +14,7 @@ import { abi as GovernorStorageAbi } from "../../abi/GovernorStorage.json";
import { abi as GovernorVotesQuorumFractionAbi } from "../../abi/GovernorVotesQuorumFraction.json";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { getTokenDecimals, getTokenAbi, getTokenAddress, executeOnChainTransaction } from "../helpers";
export const getVoteValue = (forVotes, totalVotes) => {
if (totalVotes == 0n) return 0;
const value = forVotes * 100n / totalVotes;
return Math.floor(Number(value.toString()));
}
export const getVoteTarget = (totalVotes, totalSupply) => {
if (totalSupply == 0n) return 80;
const precision = 10n ** 3n;
const valueRaw = (totalVotes * precision) / totalSupply;
const value = Number(valueRaw) / Number(precision);
const result = (5 - Math.sqrt(1 + 80/9 * (value - 0.1) )) / 4;
return Math.floor(result * 100);
}
import { getTokenDecimals, getTokenAbi, getTokenAddress } from "../helpers";
export const useProposalVoteOf = (chainId, proposalId, who) => {
const { data, error } = useReadContract({
@ -37,7 +22,7 @@ export const useProposalVoteOf = (chainId, proposalId, who) => {
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: "voteOf",
args: [proposalId, who],
scopeKey: `voteOf-${chainId}-${proposalId ? proposalId.toString() : "undefined"}-${who}`,
scopeKey: `voteOf-${chainId}-${proposalId?.toString()}-${who}`,
chainId: chainId,
});
const voteOf = data ? BigInt(data) : 0n;
@ -111,7 +96,6 @@ export const useMinQuorum = (chainId) => {
export const useProposalThreshold = (chainId, name) => {
const decimals = getTokenDecimals(name);
const { proposalCount } = useProposalCount(chainId);
const { data } = useReadContract({
abi: GovernorStorageAbi,
@ -129,17 +113,13 @@ export const useProposalThreshold = (chainId, name) => {
chainId: chainId,
});
const lastIndex = proposalCount === 0n ? 0n : proposalCount - 1n;
const { proposalId } = useProposalDetailsAt(chainId, lastIndex, {
enabled: proposalCount !== 0n
});
const { state } = useProposalState(chainId, proposalId, {
enabled: proposalCount !== 0n && !!proposalId
});
let threshold = new DecimalBigNumber(data ?? 0n, decimals);
if (proposalCount !== 0n && state !== undefined && state < 2) {
const { proposalCount } = useProposalCount(chainId);
const { proposalId } = useProposalDetailsAt(chainId, proposalCount === 0n ? 0n : proposalCount - 1n);
const { state } = useProposalState(chainId, proposalId);
if (state < 2) {
threshold = new DecimalBigNumber(activeProposedLock ?? 0n, decimals);
}
@ -340,22 +320,22 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
});
const { data: proposalDetails } = useReadContracts({
contracts: searchedIndexes?.map(proposalId => {
contracts: searchedIndexes?.map(index => {
return {
abi: GovernorStorageAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: "proposalDetails",
args: [proposalId],
scopeKey: `proposalDetails-${chainId}-${proposalId}`,
args: [index],
scopeKey: `proposalDetails-${chainId}-${index}`,
chainId: chainId,
}
})
});
const { data: proposalDeadlines } = useReadContracts({
contracts: indexes?.map((_, index) => {
contracts: indexes?.map(index => {
const proposalId = searchedIndexes
? searchedIndexes?.at(index)
? searchedIndexes?.at(0)
: proposalsDetailsAt?.at(index)?.result?.at(0);
return {
@ -370,9 +350,9 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
});
const { data: proposalVotes } = useReadContracts({
contracts: indexes?.map((_, index) => {
contracts: indexes?.map(index => {
const proposalId = searchedIndexes
? searchedIndexes?.at(index)
? searchedIndexes?.at(0)
: proposalsDetailsAt?.at(index)?.result?.at(0);
return {
@ -387,9 +367,9 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
});
const { data: proposalStates } = useReadContracts({
contracts: indexes?.map((_, index) => {
contracts: indexes?.map(index => {
const proposalId = searchedIndexes
? searchedIndexes?.at(index)
? searchedIndexes?.at(0)
: proposalsDetailsAt?.at(index)?.result?.at(0);
return {
@ -404,9 +384,9 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
});
const { data: proposalSnapshots } = useReadContracts({
contracts: indexes?.map((_, index) => {
contracts: indexes?.map(index => {
const proposalId = searchedIndexes
? searchedIndexes?.at(index)
? searchedIndexes?.at(0)
: proposalsDetailsAt?.at(index)?.result?.at(0);
return {
@ -421,8 +401,8 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
});
const { data: proposalQuorums } = useReadContracts({
contracts: proposalSnapshots?.map((proposal, index) => {
const timepoint = proposal?.result;
contracts: indexes?.map(index => {
const timepoint = proposalSnapshots?.at(index)?.result;
return {
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
@ -435,8 +415,8 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
});
const { data: pastTotalSupplies } = useReadContracts({
contracts: proposalSnapshots?.map((proposal, index) => {
const timepoint = proposal?.result;
contracts: indexes?.map(index => {
const timepoint = proposalSnapshots?.at(index)?.result;
return {
abi: ghstAbi,
address: ghstAddress,
@ -449,9 +429,9 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
});
const { data: proposalProposer } = useReadContracts({
contracts: indexes?.map((_, index) => {
contracts: indexes?.map(index => {
const proposalId = searchedIndexes
? searchedIndexes?.at(index)
? searchedIndexes?.at(0)
: proposalsDetailsAt?.at(index)?.result?.at(0);
return {
@ -465,126 +445,116 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
})
});
const hashes = useMemo(() => {
return indexes?.map((_, index) => {
let result = { short: index + 1, full: undefined };
const proposalId = searchedIndexes
? searchedIndexes?.at(index)
: proposalsDetailsAt?.at(index)?.result?.at(0);
const hashes = indexes?.map(index => {
let result = { short: index + 1, full: undefined };
const proposalId = searchedIndexes
? searchedIndexes?.at(0)
: proposalsDetailsAt?.at(index)?.result?.at(0);
if (proposalId) {
const hash = "0x" + proposalId.toString(16);
result.short = hash.slice(-5);
result.full = hash;
}
return result;
});
}, [indexes, searchedIndexes, proposalsDetailsAt]);
if (proposalId) {
const hash = "0x" + proposalId.toString(16);
result.short = hash.slice(-5);
result.full = hash;
}
return result;
});
const voteValues = useMemo(() => {
return indexes?.map((_, idx) => {
const index = indexes.length - idx - 1;
const againstVotes = proposalVotes?.at(index)?.result?.at(0) ?? 0n
const forVotes = proposalVotes?.at(index)?.result?.at(1) ?? 0n;
const totalVotes = againstVotes + forVotes;
return getVoteValue(forVotes, totalVotes);
}) ?? [];
}, [indexes, proposalVotes]);
const voteTargets = useMemo(() => {
return indexes?.map((_, idx) => {
const index = indexes.length - idx - 1;
const againstVotes = proposalVotes?.at(index)?.result?.at(0) ?? 0n
const forVotes = proposalVotes?.at(index)?.result?.at(1) ?? 0n;
const totalSupply = pastTotalSupplies?.at(index)?.result ?? 0n;
const totalVotes = againstVotes + forVotes;
return getVoteTarget(totalVotes, totalSupply);
}) ?? [];
}, [indexes, proposalVotes, pastTotalSupplies]);
const proposalDetailsPrepared = useMemo(() => {
if (!searchedIndexes) return proposalsDetailsAt;
return proposalDetails?.map((obj, index) => {
if (!obj?.result) return obj;
const proposalId = searchedIndexes.at(index);
return {
...obj,
result: [proposalId, ...obj.result],
};
}) ?? [];
}, [searchedIndexes, proposalsDetailsAt, proposalDetails]);
const proposals = indexes?.map((_, index) => ({
hashes: hashes?.at(index) ?? { short: "", full: "" },
const proposals = indexes?.map(index => ({
hashes: hashes?.at(index),
proposer: proposalProposer?.at(index)?.result,
details: proposalDetailsPrepared?.at(index)?.result,
details: proposalsDetailsAt?.at(index)?.result,
deadline: proposalDeadlines?.at(index)?.result ?? 0n,
snapshot: proposalSnapshots?.at(index)?.result ?? 0n,
state: proposalStates?.at(index)?.result ?? 0,
pastTotalSupply: new DecimalBigNumber(pastTotalSupplies?.at(index)?.result ?? 0n, decimals),
quorum: new DecimalBigNumber(proposalQuorums?.at(index)?.result ?? 0n, decimals),
votes: proposalVotes?.at(index)?.result?.map(vote => new DecimalBigNumber(vote ?? 0n, decimals)),
voteValue: voteValues?.at(index),
voteTarget: voteTargets?.at(index),
snapshot: new DecimalBigNumber(proposalSnapshots?.at(index)?.result ?? 0n, decimals),
votes: proposalVotes?.at(index)?.result?.map(
vote => new DecimalBigNumber(vote ?? 0n, decimals),
),
}));
return { proposals };
}
export const releaseLocked = async (chainId, account, proposalId) => {
const messages = {
replacedMsg: "Release locked transaction was replaced. Wait for inclusion please.",
successMsg: "Successfully release locked funds from the governor.",
errorMsg: "Release locked funds failed. Check logs for error detalization.",
};
try {
const { request } = await simulateContract(config, {
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: 'releaseLocked',
args: [proposalId],
account: account,
chainId: chainId
});
await executeOnChainTransaction({
chainId,
args: [proposalId],
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: "releaseLocked",
account,
messages,
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("Release locked transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success("Successfully release locked funds from the governor.");
return true;
} catch (err) {
console.error(err);
toast.error("Release locked funds failed. Check logs for error detalization.");
return false;
}
}
export const executeProposal = async (chainId, account, proposalId) => {
const messages = {
replacedMsg: "Proposal execution transaction was replaced. Wait for inclusion please.",
successMsg: "Proposal execution was successful, wait for updates.",
errorMsg: "Proposal execution failed. Check logs for error detalization.",
};
try {
const { request } = await simulateContract(config, {
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: 'execute',
args: [proposalId],
account: account,
chainId: chainId
});
await executeOnChainTransaction({
chainId,
args: [proposalId],
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: "execute",
account,
messages,
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("Proposal execution transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success("Proposal execution was successful, wait for updates.");
return true;
} catch (err) {
console.error(err);
toast.error("Proposal execution failed. Check logs for error detalization.");
return false;
}
}
export const castVote = async (chainId, account, proposalId, support) => {
const messages = {
replacedMsg: "Cast vote transaction was replaced. Wait for inclusion please.",
successMsg: "Successfully casted a vote, should be applied the proposal.",
errorMsg: "Vote cast failed. Check logs for error detalization.",
};
try {
const { request } = await simulateContract(config, {
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: 'castVote',
args: [proposalId, support],
account: account,
chainId: chainId
});
await executeOnChainTransaction({
chainId,
args: [proposalId, support],
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: "castVote",
account,
messages,
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("Cast vote transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success("Successfully casted a vote, should be applied the proposal.");
return true;
} catch (err) {
console.error(err);
toast.error("Vote cast failed. Check logs for error detalization.");
return false;
}
}
export const propose = async (chainId, account, functions, description) => {
@ -592,19 +562,28 @@ export const propose = async (chainId, account, functions, description) => {
const values = functions.map(f => f.value);
const calldatas = functions.map(f => f.calldata);
const messages = {
replacedMsg: "Proposal transaction was replaced. Wait for inclusion please.",
successMsg: "Successfully proposed a set of functions to be executed.",
errorMsg: "Proposal creation failed. Check logs for error detalization.",
};
try {
const { request } = await simulateContract(config, {
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: 'propose',
args: [targets, values, calldatas, description],
account: account,
chainId: chainId
});
await executeOnChainTransaction({
chainId,
args: [targets, values, calldatas, description],
abi: GovernorAbi,
address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: "propose",
account,
messages,
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("Proposal transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success("Successfully proposed a set of functions to be executed.");
return true;
} catch (err) {
console.error(err);
toast.error("Proposal creation failed. Check logs for error detalization.");
return false;
}
}

View File

@ -1,6 +1,3 @@
import toast from "react-hot-toast";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import {
RESERVE_ADDRESSES,
FTSO_ADDRESSES,
@ -9,9 +6,6 @@ import {
FTSO_DAI_LP_ADDRESSES,
WETH_ADDRESSES,
} from "../constants/addresses";
import { config } from "../config";
import { tokenNameConverter } from "../helpers/tokenConverter";
import { abi as DaiAbi } from "../abi/Reserve.json";
import { abi as FatsoAbi } from "../abi/Fatso.json";
@ -148,12 +142,6 @@ export const getTokenAddress = (chainId, name) => {
case "WMETC":
address = WETH_ADDRESSES[chainId];
break;
case "ETH":
address = undefined;
break;
case "METC":
address = undefined;
break;
}
return address;
}
@ -162,7 +150,7 @@ export const getTokenIcons = (chainId, address) => {
let icons = [""];
switch (address) {
case RESERVE_ADDRESSES[chainId]:
icons = [tokenNameConverter(chainId, "WETH")];
icons = ["WETH"];
break;
case FTSO_ADDRESSES[chainId]:
icons = ["FTSO"];
@ -174,44 +162,26 @@ export const getTokenIcons = (chainId, address) => {
icons = ["GHST"];
break;
case FTSO_DAI_LP_ADDRESSES[chainId]:
icons = ["WETH", "FTSO"];
icons = ["FTSO", "WETH"];
break;
default:
icons = [""]
}
return icons;
}
export const getChainTokenNames = (chainId, address) => {
let icons = [undefined];
switch (address) {
case FTSO_DAI_LP_ADDRESSES[chainId]:
icons = [undefined, "WETH"];
break;
case RESERVE_ADDRESSES[chainId]:
icons = [undefined]
break;
default:
icons = ["WETH"]
}
return icons;
}
export const getBondNameDisplayName = (chainId, tokenAddress, baseTokenSymbol) => {
let stringValue = tokenNameConverter(chainId, "WETH")
export const getBondNameDisplayName = (chainId, stringValue, tokenAddress) => {
if (tokenAddress.toUpperCase() === FTSO_DAI_LP_ADDRESSES[chainId].toUpperCase()) {
stringValue = `${baseTokenSymbol}-${stringValue} LP`;
stringValue = `LP ${stringValue}`;
}
return stringValue;
}
export const getTokenPurchaseLink = (chainId, tokenAddress, chainName) => {
let purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/dex/uniswap`;
export const getTokenPurchaseLink = (chainId, tokenAddress) => {
let purchaseUrl = "https://app.dao.ghostchain.io/#/dex/uniswap";
switch (tokenAddress) {
case RESERVE_ADDRESSES[chainId]:
purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/faucet`;
purchaseUrl = "https://app.dao.ghostchain.io/#/faucet";
if (chainId == 63) {
purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/wrapper`;
purchaseUrl = "https://app.dao.ghostchain.io/#/wrapper";
}
break;
case FTSO_DAI_LP_ADDRESSES[chainId]:
@ -220,41 +190,3 @@ export const getTokenPurchaseLink = (chainId, tokenAddress, chainName) => {
}
return purchaseUrl;
}
export const executeOnChainTransaction = async ({
chainId,
abi,
address,
functionName,
args,
account,
messages,
value
}) => {
try {
const { request } = await simulateContract(config, {
abi,
address,
functionName,
args,
account,
chainId,
value,
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast(messages.replacedMsg),
chainId
});
toast.success(messages.successMsg);
return txHash;
} catch (err) {
console.error(err);
toast.error(messages.errorMsg)
return undefined;
}
}

View File

@ -1,27 +0,0 @@
import { createContext, useContext, useState, useEffect } from "react";
const LocalStorageContext = createContext();
export const useLocalStorage = () => useContext(LocalStorageContext);
export const LocalStorageProvider = ({ children }) => {
const getStorageKey = (chainId, address, target) => {
return `${chainId}:${address}:${target}`;
}
const getStorageValue = (chainId, address, target, defaultValue) => {
const key = getStorageKey(chainId, address, target);
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
}
const setStorageValue = (chainId, address, target, value) => {
const key = getStorageKey(chainId, address, target);
localStorage.setItem(key, JSON.stringify(value));
}
return (
<LocalStorageContext.Provider value={{ getStorageValue, setStorageValue }}>
{children}
</LocalStorageContext.Provider>
)
}

View File

@ -116,7 +116,7 @@ export const useReservePrice = (chainId) => {
.then(response => {
if ('data' in response) {
const coinPrice = Number(response?.data?.amount ?? 0.0);
const priceInWei = Math.floor(coinPrice * 1e18);
const priceInWei = Math.floor(coinPrice * 1e18 / 0.99);
setReservePrice(new DecimalBigNumber(BigInt(priceInWei), 18));
} else if ('price' in response) {
const coinPrice = Number(response?.price ?? 0.0);

View File

@ -1,75 +1,14 @@
import { useReadContract } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast";
import {
amountInHistory,
amountOutHistory,
feeIn,
feeOut,
stakeRatio
} from "../../constants/gatekeeper";
import { config } from "../../config";
import { STAKING_ADDRESSES } from "../../constants/addresses";
import { abi as StakingAbi } from "../../abi/GhostStaking.json";
import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
import { shorten } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { executeOnChainTransaction } from "../helpers";
import { useCirculatingSupply } from "../tokens";
export const useGatekeeperApy = (chainId) => {
const circulatingSupply = useCirculatingSupply(chainId, "STNK");
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
const { epoch } = useEpoch(chainId);
const { data: metadata, error } = useReadContract({
abi: GatekeeperAbi,
address: gatekeeperAddress,
functionName: "metadata",
scopeKey: `gatekeeperMetadata-${chainId}-${gatekeeperAddress}`,
chainId: chainId,
});
const amountIn = new DecimalBigNumber(
(metadata?.amountIn ?? 0n) + (amountInHistory[chainId] ?? 0n),
18,
);
const amountOut = new DecimalBigNumber(
(metadata?.amountOut ?? 0n) + (amountOutHistory[chainId] ?? 0n),
18,
);
const deployedAt = metadata?.deployedAt ?? 0;
const unixSeconds = Math.floor(Date.now() / 1000);
const power = 365 * 86400 / (unixSeconds - deployedAt);
const feeInBig = new DecimalBigNumber((feeIn[chainId] ?? 0n), 5);
const feeOutBig = new DecimalBigNumber((feeOut[chainId] ?? 0n), 5);
const stakeRatioBig = new DecimalBigNumber((stakeRatio ?? 0n), 5);
const numerator = amountIn.mul(feeInBig).add(amountOut.mul(feeOutBig));
const denominator = amountIn.mul(stakeRatioBig).sub(amountOut.mul(stakeRatioBig));
let apyInner = Infinity;
if ((circulatingSupply?._value ?? 0n) === 0n) {
return { gatekeepedApy: apyInner, apyInner};
}
const value = epoch.distribute.div(circulatingSupply);
apyInner = 100 * (Math.pow(1 + parseFloat(value.toString()), 1095) - 1);
if (apyInner === 0) apyInner = Infinity;
if (!denominator?._value || denominator._value.isZero()) {
return { gatekeepedApy: apyInner, apyInner };
}
const result = Number(numerator.div(denominator).toString());
const gatekeepedApy = Math.pow(1 + result, power) * 100;
return {
gatekeepedApy: apyInner + gatekeepedApy + (apyInner * gatekeepedApy) / 100,
apyInner,
}
}
export const useCurrentIndex = (chainId) => {
const { data: index, refetch } = useReadContract({
@ -80,7 +19,7 @@ export const useCurrentIndex = (chainId) => {
chainId: chainId,
});
const currentIndexRaw = index ? index : 1000000000n;
const currentIndexRaw = index ? index : 0n;
const currentIndex = new DecimalBigNumber(currentIndexRaw, 9);
return { currentIndex, refetch };
@ -129,16 +68,16 @@ export const useWarmupInfo = (chainId, address) => {
const { data: info, refetch } = useReadContract({
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "warmupInfo",
functionName: "getWarmupInfo",
args: [address],
scopeKey: `warmupInfo-${address}-${chainId}`,
scopeKey: `getWarmupInfo-${address}-${chainId}`,
chainId: chainId,
});
const warmupInfo = {
deposit: info ? new DecimalBigNumber(info.at(0), 9) : new DecimalBigNumber(0n, 9),
shares: info ? info.at(1) : 0n,
expiry: info ? Number(info.at(2)) : 0,
deposit: info ? new DecimalBigNumber(info.deposit, 9) : new DecimalBigNumber(0n, 9),
shares: info ? info.shares : 0n,
expiry: info ? Number(info.expiry) : 0,
}
return { warmupInfo, refetch }
@ -175,39 +114,45 @@ export const useGhostedSupply = (chainId) => {
}
export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSymbol, stnkSymbol, ghstSymbol) => {
const args = [amount, account, isRebase, isClaim];
const args = [
amount,
account,
isRebase,
isClaim
];
const messages = {
replacedMsg: "Staking transaction was replaced. Wait for inclusion please.",
successMsg: `${ftsoSymbol} tokens staked successfully! Wait for the warm-up period to claim your ${isRebase ? stnkSymbol : ghstSymbol}.`,
successMsg: `${ftsoSymbol} tokens staked successfully! Wait for the warmup period to claim your ${isRebase ? stnkSymbol : ghstSymbol}.`,
errorMsg: "Staking transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction({
await executeOnChainTransaction(
chainId,
"stake",
args,
messages,
account,
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "stake",
});
messages
);
}
export const unstake = async (chainId, account, amount, isTrigger, isRebase, ftsoSymbol, stnkSymbol, ghstSymbol) => {
const args = [amount, account, isTrigger, isRebase];
const args = [
amount,
account,
isTrigger,
isRebase
];
const messages = {
replacedMsg: "Unstake transaction was replaced. Wait for inclusion please.",
successMsg: `${isRebase ? stnkSymbol : ghstSymbol} tokens unstaked successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
errorMsg: "Unstake transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction({
await executeOnChainTransaction(
chainId,
"unstake",
args,
messages,
account,
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "unstake",
});
messages
);
}
export const forfeit = async (chainId, account, ftsoSymbol) => {
@ -216,15 +161,13 @@ export const forfeit = async (chainId, account, ftsoSymbol) => {
successMsg: `Tokens forfeited successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
errorMsg: "Forfeit transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction({
await executeOnChainTransaction(
chainId,
messages,
"forfeit",
[],
account,
args: [],
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "forfeit",
});
messages
);
}
export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => {
@ -234,89 +177,93 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) =>
successMsg: `${isStnk ? stnkSymbol : ghstSymbol} tokens claimed successfully to ${shorten(account)}.`,
errorMsg: "Claim transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction({
await executeOnChainTransaction(
chainId,
"claim",
args,
messages,
account,
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "claim",
});
}
export const breakout = async (chainId, account, receiver, amount) => {
const args = [receiver, amount];
const messages = {
replacedMsg: "Breakout transaction was replaced. Wait for inclusion please.",
successMsg: `Staking breakout succesfully done. Check tx hash status or wait for slow clap finalization.`,
errorMsg: "Breakout transaction failed. Check logs for error detalization.",
};
const txHash = await executeOnChainTransaction({
chainId,
args,
messages,
account,
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "breakout",
});
return txHash;
messages
);
}
export const unwrap = async (chainId, account, amount, stnkSymbol) => {
const args = [account, amount];
const messages = {
replacedMsg: "Unwrap transaction was replaced. Wait for inclusion please.",
successMsg: `Tokens unwrapped successfully! Check your ${stnkSymbol} balance on ${shorten(account)}.`,
errorMsg: "Unwrap transaction failed. Check logs for error detalization.",
};
const txHash = await executeOnChainTransaction({
await executeOnChainTransaction(
chainId,
args,
messages,
"unwrap",
[account, amount],
account,
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "unwrap",
});
messages
);
}
export const wrap = async (chainId, account, amount, ghstSymbol) => {
const args = [account, amount];
const messages = {
replacedMsg: "Wrap transaction was replaced. Wait for inclusion please.",
successMsg: `Tokens wrapped successfully! Check your ${ghstSymbol} balance on ${shorten(account)}.`,
errorMsg: "Wrap transaction failed. Check logs for error detalization.",
};
const txHash = await executeOnChainTransaction({
await executeOnChainTransaction(
chainId,
args,
messages,
"wrap",
[account, amount],
account,
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "wrap",
});
messages
);
}
export const ghost = async (chainId, account, receiver, amount) => {
const args = [receiver, amount];
const messages = {
replacedMsg: "Bridge transaction was replaced. Wait for inclusion please.",
successMsg: `Amount successfully bridged! Check tx hash status or wait for slow clap finalization.`,
errorMsg: "Bridge transaction failed. Check logs for error detalization.",
};
const txHash = await executeOnChainTransaction({
const txHash = await executeOnChainTransaction(
chainId,
args,
messages,
"ghost",
[receiver, amount],
account,
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "ghost",
});
messages
);
return txHash;
}
const executeOnChainTransaction = async (
chainId,
functionName,
args,
account,
messages
) => {
try {
const { request } = await simulateContract(config, {
abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName,
args,
account,
chainId
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast(messages.replacedMsg),
chainId
});
toast.success(messages.successMsg);
return txHash;
} catch (err) {
console.error(err);
toast.error(messages.errorMsg)
return undefined;
}
}

View File

@ -1,17 +1,13 @@
import { useReadContract, useReadContracts, useToken, useBalance as useInnerBalance } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast";
import {
getTokenAbi,
getTokenAddress,
getTokenDecimals,
executeOnChainTransaction
} from "../helpers";
import { getTokenAbi, getTokenAddress, getTokenDecimals } from "../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { shorten } from "../../helpers";
import { tokenNameConverter } from "../../helpers/tokenConverter";
import { WETH_ADDRESSES } from "../../constants/addresses";
import { config } from "../../config";
export const usePastVotes = (chainId, name, timepoint, address) => {
const decimals = getTokenDecimals(name);
@ -63,29 +59,20 @@ export const useTotalSupply = (chainId, name) => {
};
export const useBalance = (chainId, name, address) => {
let contractAddress = getTokenAddress(chainId, name);
let isNative = false;
let requestObj = {
const contractAddress = getTokenAddress(chainId, name);
const { data, refetch, error } = useInnerBalance({
address,
chainId,
scopeKey: `balance-${contractAddress}-${address}-${chainId}`,
};
token: contractAddress,
});
if (contractAddress !== undefined) {
requestObj.token = contractAddress;
} else {
contractAddress = WETH_ADDRESSES[chainId];
isNative = true;
}
const { data, refetch, error } = useInnerBalance(requestObj);
const balancePrepared = data ? data.value : 0n;
const decimals = data ? data.decimals : getTokenDecimals(name);
const balance = new DecimalBigNumber(balancePrepared, decimals);
return { balance, refetch, contractAddress, isNative };
return { balance, refetch, contractAddress };
}
export const useAllowance = (chainId, name, owner, spender, decimals) => {
@ -108,7 +95,6 @@ export const useAllowance = (chainId, name, owner, spender, decimals) => {
export const useTokenSymbol = (chainId, name) => {
const contractAddress = getTokenAddress(chainId, name);
const { data, refetch } = useReadContract({
abi: getTokenAbi(name),
address: contractAddress,
@ -206,86 +192,127 @@ export const useBalanceForShares = (chainId, name, amount) => {
};
export const approveTokens = async (chainId, name, owner, spender, value) => {
const messages = {
replacedMsg: "Approve transaction was replaced. Wait for inclusion please.",
successMsg: `${name} tokens successfully approved to ${shorten(spender)}`,
errorMsg: `${name} tokens approval failed. Check logs for error detalization.`
};
await executeOnChainTransaction({
chainId,
args: [spender, value],
abi: getTokenAbi(name),
address: getTokenAddress(chainId, name),
functionName: "approve",
account: owner,
messages,
});
try {
const { request } = await simulateContract(config, {
abi: getTokenAbi(name),
address: getTokenAddress(chainId, name),
functionName: 'approve',
args: [spender, value],
account: owner,
chainId: chainId
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("Approve transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success(name + " tokens successfully approved to " + shorten(spender));
} catch (err) {
console.error(err);
toast.error(name + " tokens approval failed. Check logs for error detalization.")
}
}
export const mintDai = async (chainId, account, value) => {
const messages = {
replacedMsg: "Mint transaction was replaced. Wait for inclusion please.",
successMsg: "gDAI successfully minted to your wallet! Funds will be used on Ghost Faucet.",
errorMsg: "Minting gDAI from the faucet failed. Check logs for error detalization."
};
await executeOnChainTransaction({
chainId,
args: [account],
abi: getTokenAbi("GDAI"),
address: getTokenAddress(chainId, "GDAI"),
functionName: "mint",
account,
messages,
});
try {
const { request } = await simulateContract(config, {
abi: getTokenAbi("GDAI"),
address: getTokenAddress(chainId, "GDAI"),
functionName: 'mint',
args: [account],
account: account,
value: value,
chainId: chainId
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("Mint transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success("gDAI successfully minted to your wallet! Funds will be used on Ghost Faucet.");
} catch (err) {
console.error(err);
toast.error("Minting gDAI from the faucet failed. Check logs for error detalization.")
}
}
export const burnDai = async (chainId, account, value) => {
const messages = {
replacedMsg: "Burn transaction was replaced. Wait for inclusion please.",
successMsg: "gDAI successfully burned for native coins! Check your wallet balance.",
errorMsg: "Burning gDAI from the faucet failed. Check logs for error detalization."
};
await executeOnChainTransaction({
chainId,
abi: getTokenAbi("GDAI"),
address: getTokenAddress(chainId, "GDAI"),
functionName: 'burn',
args: [value],
account,
messages,
});
try {
const { request } = await simulateContract(config, {
abi: getTokenAbi("GDAI"),
address: getTokenAddress(chainId, "GDAI"),
functionName: 'burn',
args: [value],
account: account,
chainId: chainId
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("Burn transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success("gDAI successfully burned for native coins! Check your wallet balance.");
} catch (err) {
console.error(err);
toast.error("Burning gDAI from the faucet failed. Check logs for error detalization.")
}
}
export const depositNative = async (chainId, account, value) => {
const messages = {
replacedMsg: "WETH9 deposit transaction was replaced. Wait for inclusion please.",
successMsg: "WETH9 successfully minted to your wallet! Check your wallet balance.",
errorMsg: "WETH9 wrapping failed. Check logs for error detalization."
};
await executeOnChainTransaction({
chainId,
abi: getTokenAbi("WETH"),
address: getTokenAddress(chainId, "WETH"),
functionName: 'deposit',
value,
account,
messages,
});
try {
const { request } = await simulateContract(config, {
abi: getTokenAbi("WETH"),
address: getTokenAddress(chainId, "WETH"),
functionName: 'deposit',
account: account,
chainId: chainId,
value: value
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("WETH9 deposit transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success("WETH9 successfully minted to your wallet! Check your wallet balance.");
} catch (err) {
console.error(err);
toast.error("WETH9 wrapping failed. Check logs for error detalization.")
}
}
export const withdrawWeth = async (chainId, account, value) => {
const messages = {
replacedMsg: "WETH9 withdraw transaction was replaced. Wait for inclusion please.",
successMsg: "WETH9 successfully burned for native coins! Check your wallet balance.",
errorMsg: "WETH9 unwrapping failed. Check logs for error detalization."
};
await executeOnChainTransaction({
chainId,
abi: getTokenAbi("WETH"),
address: getTokenAddress(chainId, "WETH"),
functionName: 'withdraw',
args: [value],
account,
messages,
});
try {
const { request } = await simulateContract(config, {
abi: getTokenAbi("WETH"),
address: getTokenAddress(chainId, "WETH"),
functionName: 'withdraw',
args: [value],
account: account,
chainId: chainId
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast("WETH9 withdraw transaction was replaced. Wait for inclusion please."),
chainId
});
toast.success("WETH9 successfully burned for native coins! Check your wallet balance.");
} catch (err) {
console.error(err);
toast.error("WETH9 unwrapping failed. Check logs for error detalization.")
}
}

View File

@ -5,7 +5,6 @@ import { abi as TreasuryAbi } from "../../abi/GhostTreasury.json";
import { useReservePrice } from "../prices/index";
import { bigIntSqrt } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { getTokenAddress } from "../helpers";
@ -41,9 +40,6 @@ export const useTotalReserves = (chainId) => {
export const useLpValuation = (chainId, pairAddress, pairSupply) => {
const convertedPairAddress = getTokenAddress(chainId, pairAddress);
const originalCoefficient = useOrinalCoefficient(chainId);
const reservePrice = useReservePrice(chainId);
const { data: valuationRaw } = useReadContract({
abi: TreasuryAbi,
address: DAO_TREASURY_ADDRESSES[chainId],
@ -56,18 +52,7 @@ export const useLpValuation = (chainId, pairAddress, pairSupply) => {
chainId,
});
const sqrtReservePrice = reservePrice?._value
? bigIntSqrt(reservePrice._value)
: 0n;
const sqrtOriginalCoefficient = originalCoefficient?._value
? bigIntSqrt(originalCoefficient._value)
: 1n;
const valuationPrepared = valuationRaw
? valuationRaw * sqrtReservePrice / sqrtOriginalCoefficient
: 0n;
const valuationPrepared = valuationRaw ? valuationRaw : 0n;
const valuation = new DecimalBigNumber(valuationPrepared, 9);
return valuation;

View File

@ -4,12 +4,14 @@ import { abi as UniswapV2Factory } from "../../abi/UniswapV2Factory.json";
import { UNISWAP_V2_FACTORY } from "../../constants/addresses";
export const useUniswapV2Pair = (chainId, factoryAddress, token0, token1) => {
const t0 = token0 > token1 ? token0 : token1;
const t1 = token0 > token1 ? token1 : token0;
const { data, refetch } = useReadContract({
abi: UniswapV2Factory,
address: factoryAddress,
functionName: "getPair",
args: [token0, token1],
scopeKey: `getPair-${token0}-${token1}-${chainId}`,
scopeKey: `getPair-${t0}-${t1}-${chainId}`,
chainId: chainId,
});

Some files were not shown because too many files have changed in this diff Show More