Compare commits

...

25 Commits

Author SHA1 Message Date
787b7e6322
fix gatekeeper metadata read call for double staking apy
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-06 14:12:23 +03:00
bcbcbf5518
add blockNumber to make estimation of transaction time works
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-06 14:10:48 +03:00
08bf24f90b
make bond force redeem works
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-04 15:58:28 +03:00
fcc3d341d9
make staking breakout works
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-04 15:12:31 +03:00
bc76372897
make connection state more explicit
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-03 20:02:32 +03:00
c38628c107
fix text typos
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-03 17:48:01 +03:00
508150f202
remove ghost connect from the top bar
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-03 17:28:38 +03:00
a9f34987f2
change url to the ghost-connect
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-03 17:18:48 +03:00
b11330bd23
make emergency withdraw available when is not claimable
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-05-03 17:09:14 +03:00
7a14ace641
remove typos from the breakout modal
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 16:14:25 +03:00
b022a3c64c
make all actionable buttons to show loading state with appropriate text
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 15:06:12 +03:00
d9aff4dc6a
add checkbox if warmup exists during bond purchase
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 14:32:51 +03:00
650feb01d7
disable bond button on incorrect data
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 14:11:11 +03:00
ff9ca1bb79
make max button works during the bond purchase
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 13:49:24 +03:00
191e1f0c6e
add pageview into the sidebar
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 13:39:13 +03:00
2308d5dbab
fix sold out bonds representation on the left menu panel
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 13:32:09 +03:00
cdf1f7cabf
implement new breakout logic
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 13:17:09 +03:00
f237e2c037
put breakout and bridge logic to be a context
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 13:15:52 +03:00
68545b40f1
make local storage logic to be context
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-28 13:14:28 +03:00
5dc0bb3a1b
unite onchain tx as one function with gas sanitization
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-27 21:54:48 +03:00
37831d1a1c
corrections for treasury dashboard
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-27 16:43:41 +03:00
73f9014ade
use actual max bond discount on the dashboard
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-14 14:25:34 +03:00
e90691620b
fix typos
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-13 19:11:30 +03:00
bbf278468c
fix my highlighting
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-13 19:10:23 +03:00
7cf8ebcf0b
governance exists on all chains
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-12 11:58:09 +03:00
45 changed files with 1214 additions and 616 deletions

View File

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

View File

@ -15,16 +15,17 @@ import {
useSwitchChain, useSwitchChain,
injected injected
} from "wagmi"; } from "wagmi";
import { watchChainId } from '@wagmi/core'
import Messages from "./components/Messages/Messages"; import Messages from "./components/Messages/Messages";
import NavDrawer from "./components/Sidebar/NavDrawer"; import NavDrawer from "./components/Sidebar/NavDrawer";
import Sidebar from "./components/Sidebar/Sidebar"; import Sidebar from "./components/Sidebar/Sidebar";
import TopBar from "./components/TopBar/TopBar"; import TopBar from "./components/TopBar/TopBar";
import BreakoutModal from "./containers/Breakout/BreakoutModal";
import { shouldTriggerSafetyCheck } from "./helpers"; import { shouldTriggerSafetyCheck } from "./helpers";
import { isNetworkAvailable, isGovernanceAvailable } from "./constants"; import { isNetworkAvailable } from "./constants";
import useTheme from "./hooks/useTheme"; import useTheme from "./hooks/useTheme";
import { useUnstableProvider } from "./hooks/ghost";
import { dark as darkTheme } from "./themes/dark.js"; import { dark as darkTheme } from "./themes/dark.js";
import { girth as gTheme } from "./themes/girth.js"; import { girth as gTheme } from "./themes/girth.js";
import { light as lightTheme } from "./themes/light.js"; import { light as lightTheme } from "./themes/light.js";
@ -121,27 +122,9 @@ function App() {
const provider = usePublicClient(); const provider = usePublicClient();
const chainId = useChainId(); const chainId = useChainId();
const isSmallerScreen = useMediaQuery("(max-width: 1130px)"); const isSmallerScreen = useMediaQuery("(max-width: 1047px)");
const isSmallScreen = useMediaQuery("(max-width: 600px)"); 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(() => { useEffect(() => {
if (shouldTriggerSafetyCheck()) { if (shouldTriggerSafetyCheck()) {
toast.success("Safety Check: Always verify you're on app.dao.ghostchain.io!", { duration: 5000 }); toast.success("Safety Check: Always verify you're on app.dao.ghostchain.io!", { duration: 5000 });
@ -150,15 +133,15 @@ function App() {
useEffect(() => { useEffect(() => {
if (isConnected && chainId !== addressChainId) { if (isConnected && chainId !== addressChainId) {
const toastId = toast.loading("You are connected to wrong network. Use one of the deployed networks please.", { if (wrongNetworkToastId === null) {
position: 'bottom-right' const toastId = toast.loading("You are connected to wrong network. Use one of the deployed networks please.", {
}); position: 'bottom-right'
setWrongNetworkToastId(toastId); });
} else { setWrongNetworkToastId(toastId);
if (wrongNetworkToastId) {
toast.dismiss(wrongNetworkToastId);
setWrongNetworkToastId(null);
} }
} else {
toast.dismiss(wrongNetworkToastId);
setWrongNetworkToastId(null);
} }
}, [chainId, addressChainId, isConnected, wrongNetworkToastId]) }, [chainId, addressChainId, isConnected, wrongNetworkToastId])
@ -209,6 +192,7 @@ function App() {
<div className={`${classes.content} ${isSmallerScreen && classes.contentShift}`}> <div className={`${classes.content} ${isSmallerScreen && classes.contentShift}`}>
<Suspense fallback={<div></div>}> <Suspense fallback={<div></div>}>
<BreakoutModal chainId={chainId} address={address} />
<Routes> <Routes>
<Route path="/" element={<Navigate to={chainExists ? `/${chains.at(0).name.toLowerCase()}/dashboard` : "/empty"} />} /> <Route path="/" element={<Navigate to={chainExists ? `/${chains.at(0).name.toLowerCase()}/dashboard` : "/empty"} />} />
{chainExists && {chainExists &&
@ -219,9 +203,9 @@ function App() {
<Route path="stake" element={<StakeContainer connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} /> <Route path="stake" element={<StakeContainer 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="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="dex/:name" element={<Dex config={config} 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} />} />} <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} />} />} <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="governance/create" element={<NewProposal config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
</Route> </Route>
} }
<Route path="/empty" element={<NotFound <Route path="/empty" element={<NotFound

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
const PageTitle = ({ name, subtitle, noMargin }) => { const PageTitle = ({ name, subtitle, noMargin }) => {
const theme = useTheme(); const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down("900")); const mobile = useMediaQuery(theme.breakpoints.down("700"));
return ( return (
<Box <Box

View File

@ -1,4 +1,4 @@
import { useMemo } from "react"; import { useMemo, useEffect } from "react";
import "./Sidebar.scss"; import "./Sidebar.scss";
@ -17,6 +17,7 @@ import {
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { useSwitchChain } from "wagmi"; import { useSwitchChain } from "wagmi";
import ReactGA from "react-ga4";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import GitHubIcon from '@mui/icons-material/GitHub'; import GitHubIcon from '@mui/icons-material/GitHub';
@ -40,7 +41,7 @@ import BondIcon from "../Icon/BondIcon";
import StakeIcon from "../Icon/StakeIcon"; import StakeIcon from "../Icon/StakeIcon";
import WrapIcon from "../Icon/WrapIcon"; import WrapIcon from "../Icon/WrapIcon";
import { isNetworkAvailable, isGovernanceAvailable } from "../../constants"; import { isNetworkAvailable } from "../../constants";
import { AVAILABLE_DEXES } from "../../constants/dexes"; import { AVAILABLE_DEXES } from "../../constants/dexes";
import { GATEKEEPER_ADDRESSES } from "../../constants/addresses"; import { GATEKEEPER_ADDRESSES } from "../../constants/addresses";
import { ECOSYSTEM } from "../../constants/ecosystem"; import { ECOSYSTEM } from "../../constants/ecosystem";
@ -79,12 +80,17 @@ const NavContent = ({ chainId, addressChainId }) => {
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO"); const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const sortedGhostBonds = useMemo(() => sortBondsByDiscount(ghostBonds), [ghostBonds]);
const bridgeNumbers = useMemo(() => { const bridgeNumbers = useMemo(() => {
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length; const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
const number = 1 + connectedNetworks * 3; const number = 1 + connectedNetworks * 3;
return `(${number}, ${number})`; return `(${number}, ${number})`;
}, [chainId]); }, [chainId]);
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: "/sidebar" });
}, []);
return ( return (
<Paper className="dapp-sidebar"> <Paper className="dapp-sidebar">
<Box className="dapp-sidebar-inner" display="flex" justifyContent="space-between" flexDirection="column"> <Box className="dapp-sidebar-inner" display="flex" justifyContent="space-between" flexDirection="column">
@ -144,10 +150,10 @@ const NavContent = ({ chainId, addressChainId }) => {
to={`/${chainName}/bonds`} to={`/${chainName}/bonds`}
children={ children={
<AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}> <AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}>
{ghostBonds.length > 0 && <Box width="180px" mb="10px" ml="auto"> {sortedGhostBonds.length > 0 && <Box width="180px" mb="10px" ml="auto">
<Typography component="span" variant="body2">Bond Discounts</Typography> <Typography component="span" variant="body2">Bond Discounts</Typography>
</Box>} </Box>}
{sortBondsByDiscount(ghostBonds).map((bond, index) => { {sortedGhostBonds.map((bond, index) => {
return ( return (
<Link <Link
component={NavLink} component={NavLink}
@ -168,7 +174,7 @@ const NavContent = ({ chainId, addressChainId }) => {
variant="body2" variant="body2"
> >
{bond.displayName} {bond.displayName}
{bond.soldOut {bond.isSoldOut
? <Chip label="Sold Out" template="darkGray" /> ? <Chip label="Sold Out" template="darkGray" />
: <BondDiscount discount={bond.discount} /> : <BondDiscount discount={bond.discount} />
} }
@ -181,7 +187,7 @@ const NavContent = ({ chainId, addressChainId }) => {
} }
/> />
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} /> <NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
{isGovernanceAvailable(chainId, addressChainId) && <NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />} <NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />
<Box className="menu-divider"> <Box className="menu-divider">
<Divider /> <Divider />
</Box> </Box>

View File

@ -86,8 +86,8 @@ const Token = ({ chainTokenName, name, viewBox = "0 0 260 260", fontSize = "larg
position: "absolute", position: "absolute",
marginLeft: "70%", marginLeft: "70%",
marginTop: "45%", marginTop: "45%",
width: "65%", width: "45%",
height: "65%", height: "45%",
border: "1px solid #fff", border: "1px solid #fff",
borderRadius: "100%" borderRadius: "100%"
}} }}

View File

@ -4,7 +4,7 @@ import { parseKnownToken } from "../../components/Token/Token";
import { useUnstableProvider } from "../../hooks/ghost"; import { useUnstableProvider } from "../../hooks/ghost";
import { PrimaryButton } from "../Button" import { PrimaryButton } from "../Button"
const GHOST_CONNECT = 'https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases'; import { GHOST_CONNECT } from "../../constants/ecosystem";
function GhostChainSelect({ small }) { function GhostChainSelect({ small }) {
const { providerDetail, isConnected } = useUnstableProvider(); const { providerDetail, isConnected } = useUnstableProvider();

View File

@ -71,7 +71,7 @@ function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, s
} }
return( return(
<FormControl sx={{ width: small ? "auto" : "155px" }}> <FormControl sx={{ width: small ? "100px" : "155px" }}>
<Select <Select
labelId="network-select-helper-label" labelId="network-select-helper-label"
id="network-select-helper" id="network-select-helper"

View File

@ -2,7 +2,6 @@ import { Box, Button, SvgIcon, useMediaQuery, useTheme } from "@mui/material";
import MenuIcon from "../../assets/icons/hamburger.svg?react"; import MenuIcon from "../../assets/icons/hamburger.svg?react";
import Wallet from "./Wallet" import Wallet from "./Wallet"
import SelectNetwork from "./SelectNetwork"; import SelectNetwork from "./SelectNetwork";
import GhostChainSelect from "./GhostChainSelect";
const PREFIX = "TopBar"; const PREFIX = "TopBar";
@ -23,8 +22,8 @@ function TopBar({
setWrongNetworkToastId setWrongNetworkToastId
}) { }) {
const themeColor = useTheme(); const themeColor = useTheme();
const desktop = useMediaQuery(themeColor.breakpoints.up(1130)); const desktop = useMediaQuery(themeColor.breakpoints.up(1048));
const small = useMediaQuery(themeColor.breakpoints.down(600)); const small = useMediaQuery(themeColor.breakpoints.down(400));
return ( return (
<Box <Box
display="flex" display="flex"
@ -38,10 +37,9 @@ function TopBar({
display="flex" display="flex"
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
width={small ? "calc(100vw - 78px)" : "500px"} width={small ? "calc(100vw - 78px)" : "320px"}
height="40px" height="40px"
> >
<GhostChainSelect small={small} />
<SelectNetwork <SelectNetwork
wrongNetworkToastId={wrongNetworkToastId} wrongNetworkToastId={wrongNetworkToastId}
setWrongNetworkToastId={setWrongNetworkToastId} setWrongNetworkToastId={setWrongNetworkToastId}

View File

@ -205,7 +205,10 @@ export const useWallet = (chainId, userAddress) => {
const config = useConfig(); const config = useConfig();
const nativeSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol; const nativeSymbol = useMemo(() => {
return config?.getClient()?.chain?.nativeCurrency?.symbol;
}, [config]);
const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE"); const { symbol: reserveSymbol } = useTokenSymbol(chainId, "RESERVE");
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO"); const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");

View File

@ -11,7 +11,7 @@ const WalletButton = ({ openWallet, connect }) => {
const { isConnected, chain } = useAccount(); const { isConnected, chain } = useAccount();
const theme = useTheme(); const theme = useTheme();
const onClick = isConnected ? openWallet : connect; const onClick = isConnected ? openWallet : connect;
const label = isConnected ? "Wallet" : `Connect`; const label = `${isConnected ? "Open" : "Connect"} Wallet`;
return ( return (
<Button <Button
id="fatso-menu-button" id="fatso-menu-button"

View File

@ -4,22 +4,6 @@ export enum NetworkId {
TESTNET_MORDOR = 63, TESTNET_MORDOR = 63,
} }
export const isGovernanceAvailable = (chainId, addressChainId) => {
chainId = addressChainId ? addressChainId : chainId;
let exists = false;
switch (chainId) {
case 11155111:
exists = true
break;
case 63:
exists = true
break;
default:
break;
}
return exists;
}
export const isNetworkAvailable = (chainId, addressChainId) => { export const isNetworkAvailable = (chainId, addressChainId) => {
chainId = addressChainId ? addressChainId : chainId; chainId = addressChainId ? addressChainId : chainId;
let exists = false; let exists = false;

View File

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

View File

@ -1,5 +1,6 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { Box, Typography } from "@mui/material"; import { Box, Typography, Checkbox, FormControlLabel } from "@mui/material";
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
import { styled, useTheme } from "@mui/material/styles"; import { styled, useTheme } from "@mui/material/styles";
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
@ -10,13 +11,14 @@ import Metric from "../../../components/Metric/Metric";
import Modal from "../../../components/Modal/Modal"; import Modal from "../../../components/Modal/Modal";
import TokenStack from "../../../components/TokenStack/TokenStack"; import TokenStack from "../../../components/TokenStack/TokenStack";
import DataRow from "../../../components/DataRow/DataRow"; import DataRow from "../../../components/DataRow/DataRow";
import { PrimaryButton } from "../../../components/Button"; import { PrimaryButton, SecondaryButton } from "../../../components/Button";
import BondDiscount from "./BondDiscount"; import BondDiscount from "./BondDiscount";
import BondVesting from "./BondVesting"; import BondVesting from "./BondVesting";
import BondSlippage from "./BondSlippage"; import BondSlippage from "./BondSlippage";
import { purchaseBond } from "../../../hooks/bonds"; import { purchaseBond } from "../../../hooks/bonds";
import { useWarmupLength } from "../../../hooks/staking";
const StyledBox = styled(Box, { const StyledBox = styled(Box, {
shouldForwardProp: prop => prop !== "template", shouldForwardProp: prop => prop !== "template",
@ -43,8 +45,57 @@ const BondConfirmModal = ({
handleConfirmClose handleConfirmClose
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { warmupLength, warmupExists: needsWarmup } = useWarmupLength(chainId);
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
const [isPending, setIsPending] = 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 () => { const onSubmit = async () => {
setIsPending(true); setIsPending(true);
@ -68,7 +119,7 @@ const BondConfirmModal = ({
}); });
setIsPending(false); setIsPending(false);
handleConfirmClose(); handleConfirmCloseMaster();
} }
return ( return (
@ -84,7 +135,7 @@ const BondConfirmModal = ({
</Typography> </Typography>
</Box> </Box>
} }
onClose={!isPending && handleConfirmClose} onClose={!isPending && handleConfirmCloseMaster}
topLeft={<GhostStyledIcon viewBox="0 0 23 23" component={SettingsIcon} style={{ cursor: "pointer" }} onClick={handleSettingsOpen} />} topLeft={<GhostStyledIcon viewBox="0 0 23 23" component={SettingsIcon} style={{ cursor: "pointer" }} onClick={handleSettingsOpen} />}
> >
<> <>
@ -110,9 +161,13 @@ const BondConfirmModal = ({
<DataRow title="ROI" balance={<BondDiscount discount={bond.discount} textOnly />} /> <DataRow title="ROI" balance={<BondDiscount discount={bond.discount} textOnly />} />
<DataRow title="Bond Slippage" balance={<BondSlippage slippage={slippage} textOnly />} /> <DataRow title="Bond Slippage" balance={<BondSlippage slippage={slippage} textOnly />} />
<DataRow title="Vesting Term" balance={<BondVesting vesting={bond.vesting} />} /> <DataRow title="Vesting Term" balance={<BondVesting vesting={bond.vesting} />} />
<PrimaryButton fullWidth onClick={onSubmit} disabled={isPending} loading={isPending}> {!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}>
{isPending ? "Bonding..." : "Confirm Bond Purchase"} {isPending ? "Bonding..." : "Confirm Bond Purchase"}
</PrimaryButton> </PrimaryButton>}
</> </>
</Modal> </Modal>
); );

View File

@ -1,6 +1,6 @@
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material"; import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
import { Box, Checkbox, FormControlLabel, useMediaQuery } from "@mui/material"; import { Box, Checkbox, FormControlLabel, useMediaQuery } from "@mui/material";
import { useState, useMemo } from "react"; import { useState, useMemo, useCallback } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses"; import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses";
@ -66,7 +66,7 @@ const BondInputArea = ({
refetch(); refetch();
} }
const setMax = () => { const setMax = useCallback(() => {
if (!balance) return; if (!balance) return;
if (bond.capacity.inQuoteToken.lt(bond.maxPayout.inQuoteToken)) { if (bond.capacity.inQuoteToken.lt(bond.maxPayout.inQuoteToken)) {
@ -82,12 +82,17 @@ const BondInputArea = ({
? bond.maxPayout.inQuoteToken.toString() // Payout is the smallest ? bond.maxPayout.inQuoteToken.toString() // Payout is the smallest
: balance.toString(), : balance.toString(),
); );
}; }, [bond, balance]);
const baseTokenString = (bond.maxPayout.inBaseToken.lt(bond.capacity.inBaseToken) const baseTokenString = useMemo(() => (bond.maxPayout.inBaseToken.lt(bond.capacity.inBaseToken)
? bond.maxPayout.inBaseToken ? bond.maxPayout.inBaseToken
: bond.capacity.inBaseToken : bond.capacity.inBaseToken
); ), [bond]);
const incorrectInputAmount = useMemo(() => {
if (!balance) return false;
return balance.lt(parsedAmount) || baseTokenString.lt(amountInBaseToken);
}, [amountInBaseToken, baseTokenString, balance, parsedAmount]);
return ( return (
<Box minHeight="calc(100vh - 210px)" display="flex" flexDirection="column" justifyContent="center"> <Box minHeight="calc(100vh - 210px)" display="flex" flexDirection="column" justifyContent="center">
@ -103,7 +108,7 @@ const BondInputArea = ({
token={<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "21px" }} />} token={<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "21px" }} />}
tokenName={preparedQuoteToken.name} tokenName={preparedQuoteToken.name}
info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)} info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)}
endString={preparedQuoteToken.address && "Max"} endString="Max"
endStringOnClick={setMax} endStringOnClick={setMax}
value={amount} value={amount}
onChange={event => setAmount(event.currentTarget.value)} onChange={event => setAmount(event.currentTarget.value)}
@ -158,7 +163,7 @@ const BondInputArea = ({
)} )}
<PrimaryButton <PrimaryButton
fullWidth fullWidth
disabled={bond.isSoldOut || (showDisclaimer && !checked)} disabled={incorrectInputAmount || bond.isSoldOut || (showDisclaimer && !checked)}
onClick={() => setConfirmOpen(true)} onClick={() => setConfirmOpen(true)}
> >
Bond Bond

View File

@ -13,23 +13,27 @@ import { PrimaryButton, TertiaryButton } from "../../../components/Button";
import { useScreenSize } from "../../../hooks/useScreenSize"; import { useScreenSize } from "../../../hooks/useScreenSize";
import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses"; import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses";
import { isNetworkLegacy } from "../../../constants";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { formatCurrency } from "../../../helpers"; import { formatCurrency } from "../../../helpers";
import { useCurrentIndex, useEpoch, useWarmupLength, useWarmupInfo } from "../../../hooks/staking"; import { useCurrentIndex, useEpoch, useWarmupLength, useWarmupInfo } from "../../../hooks/staking";
import { useNotes, redeem } from "../../../hooks/bonds"; import { useNotes, redeem, forceRedeem } from "../../../hooks/bonds";
import { useTokenSymbol } from "../../../hooks/tokens"; import { useTokenSymbol } from "../../../hooks/tokens";
import { useGhstPrice } from "../../../hooks/prices"; import { useGhstPrice } from "../../../hooks/prices";
import { useBreakoutModal } from "../../../hooks/breakoutModal";
export const ClaimBonds = ({ chainId, address, secondsTo }) => { export const ClaimBonds = ({ chainId, address, secondsTo }) => {
const isSmallScreen = useScreenSize("md"); const isSmallScreen = useScreenSize("md");
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const [pendingIndexes, setPendingIndexes] = useState([]);
const [isWarmup, setIsWapmup] = useState(false); const [isWarmup, setIsWapmup] = useState(false);
const [isPreClaimConfirmed, setPreClaimConfirmed] = useState(false); const [isPreClaimConfirmed, setPreClaimConfirmed] = useState(false);
const [isPayoutGhst, _] = useState(true); const [isPayoutGhst, _] = useState(true);
const ghstPrice = useGhstPrice(chainId); const ghstPrice = useGhstPrice(chainId);
const { breakoutFromBonding } = useBreakoutModal();
const { epoch } = useEpoch(chainId); const { epoch } = useEpoch(chainId);
const { warmupExists } = useWarmupLength(chainId); const { warmupExists } = useWarmupLength(chainId);
const { warmupInfo } = useWarmupInfo(chainId, BOND_DEPOSITORY_ADDRESSES[chainId]); const { warmupInfo } = useWarmupInfo(chainId, BOND_DEPOSITORY_ADDRESSES[chainId]);
@ -51,20 +55,37 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
); );
const onSubmit = async (indexes) => { const onSubmit = async (indexes) => {
const isFundsInWarmup = warmupInfo.deposit._value > 0n; setIsPending(true);
if (warmupExists && isFundsInWarmup && !isPreClaimConfirmed) { setPendingIndexes(indexes);
setIsWapmup(true);
} else { const defaultFunction = async () => {
setIsPending(true); await redeem({ chainId, user: address, isGhst: isPayoutGhst, indexes });
await redeem({
chainId,
user: address,
isGhst: isPayoutGhst,
indexes
});
await notesRefetch(); await notesRefetch();
setIsPending(false); };
if (isNetworkLegacy(chainId)) {
const isFundsInWarmup = warmupInfo.deposit._value > 0n;
if (warmupExists && isFundsInWarmup && !isPreClaimConfirmed) {
setIsWapmup(true);
} else {
await defaultFunction();
}
} 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 })
} }
setPendingIndexes([]);
setIsPending(false);
} }
return ( return (
@ -159,9 +180,10 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
<TertiaryButton <TertiaryButton
fullWidth fullWidth
disabled={isPending || secondsTo < note.matured} disabled={isPending || secondsTo < note.matured}
loading={isPending && pendingIndexes.includes(note.id)}
onClick={() => onSubmit([note.id])} onClick={() => onSubmit([note.id])}
> >
Claim {isPending && pendingIndexes.includes(note.id) ? "Claiming" : "Claim"}
</TertiaryButton> </TertiaryButton>
</Box> </Box>
</Box> </Box>
@ -218,9 +240,10 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
<TertiaryButton <TertiaryButton
fullWidth fullWidth
disabled={isPending || secondsTo < note.matured} disabled={isPending || secondsTo < note.matured}
loading={isPending && pendingIndexes.includes(note.id)}
onClick={() => onSubmit([note.id])} onClick={() => onSubmit([note.id])}
> >
Claim {isPending && pendingIndexes.includes(note.id) ? "Claiming" : "Claim"}
</TertiaryButton> </TertiaryButton>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -56,14 +56,14 @@ const WarmupConfirmModal = ({
? <FormControlLabel ? <FormControlLabel
control={ control={
<Checkbox <Checkbox
data-testid="acknowledge-warmup" data-testid="acknowledge-warm-up"
checked={isChecked} checked={isChecked}
onChange={event => setIsChecked(event.target.checked)} onChange={event => setIsChecked(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />} icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />} checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/> />
} }
label={`I acknowledge that I am releasing warmup funds for the bonding contract on behalf of the collective.`} label={`I acknowledge that I am releasing warm-up 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.` : `Bonding address is in a warm-up period and cannot be claimed now. It'll be available for claim in ${warmupLength} epochs.`
} }

View File

@ -0,0 +1,506 @@
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 CheckCircleIcon from '@mui/icons-material/CheckCircle';
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, useCirculatingSupply } 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 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%"}
value={convertedReceiver ? shorten(receiver, 15, -10) : receiver}
onChange={event => setReceiver(convertedReceiver ? "" : event.currentTarget.value)}
inputProps={{ "data-testid": "fromInput" }}
placeholder="GHOST address (sf prefixed)"
endString={convertedReceiver
? <GhostStyledIcon color="success" viewBox="0 0 25 25" component={CheckCircleIcon} />
: 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 [isPending, setIsPending] = useState(false);
const { epoch } = useEpoch(chainId);
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
const circulatingSupply = useCirculatingSupply(chainId, "STNK");
const gatekeepedApy = useGatekeeperApy(chainId);
const { isExtensionMissing } = useUnstableProvider();
const getConnect = () => {
window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer');
closeModal();
}
const apyInner = useMemo(() => {
let apy = Infinity;
if (circulatingSupply._value > 0n) {
const value = epoch.distribute.div(circulatingSupply);
apy = 100 * (Math.pow(1 + parseFloat(value.toString()), 1095) - 1);
if (apy === 0) apy = Infinity;
}
return apy;
}, [circulatingSupply, epoch]);
const callDefaultFunction = useCallback(async () => {
setIsPending(true);
await defaultFunction()();
setIsPending(false);
closeModal();
}, [defaultFunction]);
return (
<>
<Typography>{warmupPeriod <= 0
? `You've 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} ${isStakingOpened ? "(3, 3)" : "(1, 1)"} is 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(apyInner * gatekeepedApy, 2)}% APY</Typography>
</Box>
<PrimaryButton
disabled={isPending || gatekeeperAddress === EMPTY_ADDRESS}
onClick={isExtensionMissing ? getConnect : goNext}
fullWidth
>
{isExtensionMissing ? "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: 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 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..." : "I Confirm"}
</PrimaryButton>
</>
)
}
export default BreakoutModal;

View File

@ -13,7 +13,7 @@ import { decodeAddress } from "@polkadot/util-crypto";
import { fromHex } from "@polkadot-api/utils"; import { fromHex } from "@polkadot-api/utils";
import { getBlockNumber } from "@wagmi/core"; import { getBlockNumber } from "@wagmi/core";
import { useTransaction } from "wagmi"; import { useTransaction } from "wagmi";
import { keccak256 } from "viem"; import { keccak256, decodeFunctionData } from "viem";
import { u32, u64, u128 } from "scale-ts"; import { u32, u64, u128 } from "scale-ts";
import PendingActionsIcon from '@mui/icons-material/PendingActions'; import PendingActionsIcon from '@mui/icons-material/PendingActions';
@ -23,6 +23,7 @@ import PageTitle from "../../components/PageTitle/PageTitle";
import Paper from "../../components/Paper/Paper"; import Paper from "../../components/Paper/Paper";
import GhostStyledIcon from "../../components/Icon/GhostIcon"; import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { abi as StakingAbi } from "../../abi/GhostStaking.json";
import { networkAvgBlockSpeed } from "../../constants"; import { networkAvgBlockSpeed } from "../../constants";
import { timeConverter } from "../../helpers"; import { timeConverter } from "../../helpers";
@ -47,6 +48,7 @@ import {
useEraIndex, useEraIndex,
} from "../../hooks/ghost"; } from "../../hooks/ghost";
import { useLocalStorage } from "../../hooks/localstorage"; import { useLocalStorage } from "../../hooks/localstorage";
import { useBreakoutModal } from "../../hooks/breakoutModal";
import { ValidatorTable } from "./ValidatorTable"; import { ValidatorTable } from "./ValidatorTable";
import { BridgeModal, BridgeConfirmModal } from "./BridgeModal"; import { BridgeModal, BridgeConfirmModal } from "./BridgeModal";
@ -61,12 +63,12 @@ const Bridge = ({ chainId, address, config, connect }) => {
const [bridgeModalOpen, setBridgeModalOpen] = useState(false); const [bridgeModalOpen, setBridgeModalOpen] = useState(false);
const [isConfirmed, setIsConfirmed] = useState(false); const [isConfirmed, setIsConfirmed] = useState(false);
const [activeTxIndex, setActiveTxIndex] = useState(-1);
const [blockNumber, setBlockNumber] = useState(0n); const [blockNumber, setBlockNumber] = useState(0n);
const [bridgeAction, setBridgeAction] = useState(true); const [bridgeAction, setBridgeAction] = useState(true);
const [currentTime, setCurrentTime] = useState(Date.now()); const [currentTime, setCurrentTime] = useState(Date.now());
const { getStorageValue, setStorageValue } = useLocalStorage(); const { getStorageValue, setStorageValue } = useLocalStorage();
const { activeTxIndex, setActiveTxIndex } = useBreakoutModal();
useEffect(() => { useEffect(() => {
const interval = setInterval(() => setCurrentTime(Date.now()), 1000); const interval = setInterval(() => setCurrentTime(Date.now()), 1000);
@ -100,7 +102,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
}); });
const hashedArguments = useMemo(() => { const hashedArguments = useMemo(() => {
if (!watchTransaction) return undefined if (!watchTransaction) return undefined;
const networkIdEncoded = u64.enc(BigInt(chainId)); const networkIdEncoded = u64.enc(BigInt(chainId));
const amountEncoded = u128.enc(BigInt(watchTransaction.amount)); const amountEncoded = u128.enc(BigInt(watchTransaction.amount));
@ -182,7 +184,8 @@ const Bridge = ({ chainId, address, config, connect }) => {
return sum + countOnesInBigInt(bigIntValue); return sum + countOnesInBigInt(bigIntValue);
}, 0); }, 0);
const finalization = Math.max(0, (finalityDelay + watchTransaction.blockNumber) - Number(blockNumber)); const storedBlockNumber = watchTransactionInfo ? Number(watchTransactionInfo.blockNumber) : 0;
const finalization = Math.max(0, (finalityDelay + storedBlockNumber) - Number(blockNumber));
const applaused = transactionApplaused?.finalized ?? false; const applaused = transactionApplaused?.finalized ?? false;
const clappedAmount = transactionApplaused?.clapped_amount ?? 0n; const clappedAmount = transactionApplaused?.clapped_amount ?? 0n;
const clappedPercentage = clappedAmount * 100n / (totalStakedAmount ?? 1n); const clappedPercentage = clappedAmount * 100n / (totalStakedAmount ?? 1n);
@ -198,9 +201,10 @@ const Bridge = ({ chainId, address, config, connect }) => {
clapsPercentage, clapsPercentage,
} }
}, [ }, [
watchTransaction,
watchTransactionInfo,
transactionApplaused, transactionApplaused,
finalityDelay, finalityDelay,
watchTransaction,
blockNumber, blockNumber,
totalStakedAmount, totalStakedAmount,
authorities authorities

View File

@ -246,7 +246,7 @@ export const BridgeCardAction = ({
loading={isPending} loading={isPending}
onClick={() => ghostOrConnect()} onClick={() => ghostOrConnect()}
> >
{address === "" ? "Connect" : "Bridge" } {address === "" ? "Connect" : isPending ? "Bridging..." : "Bridge" }
</PrimaryButton> </PrimaryButton>
</Box> </Box>
) )
@ -307,7 +307,7 @@ export const BridgeCardHistory = ({
<Box display="flex" flexDirection="column" justifyContent="center"> <Box display="flex" flexDirection="column" justifyContent="center">
<Typography variant="caption"> <Typography variant="caption">
{formatCurrency( {formatCurrency(
new DecimalBigNumber(BigInt(obj.amount), 18).toString(), new DecimalBigNumber(BigInt(obj.amount ?? "0"), 18).toString(),
isSemiSmallScreen ? 3 : 8, isSemiSmallScreen ? 3 : 8,
ghstSymbol ghstSymbol
)} )}

View File

@ -24,6 +24,7 @@ import { PrimaryButton, TertiaryButton, SecondaryButton } from "../../components
import { formatCurrency } from "../../helpers"; import { formatCurrency } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { GATEKEEPER_ADDRESSES } from "../../constants/addresses"; import { GATEKEEPER_ADDRESSES } from "../../constants/addresses";
import { GHOST_CONNECT } from "../../constants/ecosystem";
export const BridgeModal = ({ export const BridgeModal = ({
providerDetail, providerDetail,
@ -113,15 +114,8 @@ export const BridgeModal = ({
> >
<SecondaryButton <SecondaryButton
fullWidth fullWidth
sx={{ sx={{ marginTop: "0 !important", marginBottom: "0 !important" }}
marginTop: "0 !important", onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}
marginBottom: "0 !important"
}}
onClick={() => window.open(
'https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases',
'_blank',
'noopener,noreferrer'
)}
> >
Get GHOST Connect Get GHOST Connect
</SecondaryButton> </SecondaryButton>
@ -375,7 +369,7 @@ export const BridgeModal = ({
<Typography variant="body2">Bridged Amount:</Typography> <Typography variant="body2">Bridged Amount:</Typography>
<Typography variant="body2">{formatCurrency( <Typography variant="body2">{formatCurrency(
new DecimalBigNumber( new DecimalBigNumber(
BigInt(currentRecord ? currentRecord.amount : "0"), BigInt(currentRecord && currentRecord.amount ? currentRecord.amount : "0"),
18 18
).toString(), 9, ghstSymbol) ).toString(), 9, ghstSymbol)
}</Typography> }</Typography>

View File

@ -25,6 +25,8 @@ import GhostStyledIcon from "../../components/Icon/GhostIcon";
import InfoTooltip from "../../components/Tooltip/InfoTooltip"; import InfoTooltip from "../../components/Tooltip/InfoTooltip";
import { PrimaryButton } from "../../components/Button"; import { PrimaryButton } from "../../components/Button";
import { GHOST_CONNECT } from "../../constants/ecosystem";
export const ValidatorTable = ({ export const ValidatorTable = ({
currentTime, currentTime,
currentBlock, currentBlock,
@ -108,7 +110,7 @@ export const ValidatorTable = ({
<Typography sx={{ textAlign: "center" }} variant="h6">GHOST Connect is not detected on your browser!</Typography> <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">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> <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('https://git.ghostchain.io/ghostchain/ghost-extension-wallet/releases', '_blank', 'noopener,noreferrer')}> <PrimaryButton onClick={() => window.open(GHOST_CONNECT, '_blank', 'noopener,noreferrer')}>
Get GHOST Connect Get GHOST Connect
</PrimaryButton> </PrimaryButton>
</Box> </Box>

View File

@ -74,7 +74,7 @@ const Dex = ({ chainId, address, connect, config }) => {
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol; const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
if (chainSymbol) return chainSymbol; if (chainSymbol) return chainSymbol;
return "WTF"; return "WTF";
}, [config]) }, [config, chainId])
const tokenNameTop = useMemo(() => { const tokenNameTop = useMemo(() => {
if (chainSymbol && tokenAddressTop === EMPTY_ADDRESS) { if (chainSymbol && tokenAddressTop === EMPTY_ADDRESS) {

View File

@ -317,9 +317,9 @@ const PoolContainer = ({
"Connect" "Connect"
: :
pairAddress === "0x0000000000000000000000000000000000000000" ? pairAddress === "0x0000000000000000000000000000000000000000" ?
"Create Pool" isPending ? "Creating Pool..." : "Create Pool"
: :
"Add Liquidity" isPending ? "Adding Liquidity..." : "Add Liquidity"
} }
</SecondaryButton> </SecondaryButton>
</TokenAllowanceGuard> </TokenAllowanceGuard>

View File

@ -147,8 +147,12 @@ const SwapContainer = ({
if (isWrapping) text = "Wrap"; if (isWrapping) text = "Wrap";
else if (isUnwrapping) text = "Unwrap"; else if (isUnwrapping) text = "Unwrap";
else if (pairAddress === EMPTY_ADDRESS) text = "Create Pool"; else if (pairAddress === EMPTY_ADDRESS) text = "Create Pool";
if (isPending) text = `${text}ping...`
if (pairAddress === EMPTY_ADDRESS && isPending) text = "Creating Pool..."
return text; return text;
}, [isWrapping, isUnwrapping, pairAddress]); }, [isPending, isWrapping, isUnwrapping, pairAddress]);
const swapTokens = async () => { const swapTokens = async () => {
setIsPending(true); setIsPending(true);

View File

@ -178,6 +178,7 @@ const NewProposal = ({ config, address, connect, chainId }) => {
isPending isPending
} }
fullWidth fullWidth
loading={isPending}
onClick={() => submitProposal()} onClick={() => submitProposal()}
> >
{isPending ? "Submitting..." : "Submit Proposal"} {isPending ? "Submitting..." : "Submit Proposal"}

View File

@ -80,7 +80,9 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
const { id } = useParams(); const { id } = useParams();
const proposalId = BigInt(id); const proposalId = BigInt(id);
const [isPending, setIsPending] = useState(false); const [isPendingVote, setIsPendingVote] = useState(-1);
const [isPendingExecute, setIsPendingExecute] = useState(false);
const [isPendingRelease, setIsPendingRelease] = useState(false);
const [selectedDiscussionUrl, setSelectedDiscussionUrl] = useState(undefined); const [selectedDiscussionUrl, setSelectedDiscussionUrl] = useState(undefined);
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)"); const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
@ -150,8 +152,12 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
return url; return url;
}, [proposalProposer, config]); }, [proposalProposer, config]);
const isPending = useMemo(() => {
return isPendingExecute || isPendingRelease || isPendingVote > -1;
}, [isPendingExecute, isPendingRelease, isPendingVote]);
const handleVote = useCallback(async (support) => { const handleVote = useCallback(async (support) => {
setIsPending(true); setIsPendingVote(support);
const result = await castVote(chainId, address, proposalId, support); const result = await castVote(chainId, address, proposalId, support);
if (result) { if (result) {
@ -161,21 +167,21 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
setStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, proposals.map(id => id.toString())); setStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, proposals.map(id => id.toString()));
await queryClient.invalidateQueries(); await queryClient.invalidateQueries();
} }
setIsPending(false); setIsPendingVote(-1);
}, [chainId, address, proposalId]); }, [chainId, address, proposalId]);
const handleExecute = async () => { const handleExecute = async () => {
setIsPending(true); setIsPendingExecute(true);
await executeProposal(chainId, address, proposalId); await executeProposal(chainId, address, proposalId);
await queryClient.invalidateQueries(); await queryClient.invalidateQueries();
setIsPending(false); setIsPendingExecute(false);
} }
const handleRelease = async () => { const handleRelease = async () => {
setIsPending(true); setIsPendingRelease(true);
await releaseLocked(chainId, address, proposalId); await releaseLocked(chainId, address, proposalId);
await queryClient.invalidateQueries(); await queryClient.invalidateQueries();
setIsPending(false); setIsPendingRelease(false);
} }
return ( return (
@ -292,16 +298,18 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
<SecondaryButton <SecondaryButton
fullWidth fullWidth
disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending} disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending}
loading={isPendingVote === 1}
onClick={() => handleVote(1)} onClick={() => handleVote(1)}
> >
{isPending ? "Voting..." : "For"} {isPendingVote === 1 ? "Voting For..." : "For"}
</SecondaryButton> </SecondaryButton>
<SecondaryButton <SecondaryButton
fullWidth fullWidth
disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending} disabled={proposalState !== 1 || pastVotes?._value === 0n || isPending}
loading={isPendingVote === 0}
onClick={() => handleVote(0)} onClick={() => handleVote(0)}
> >
{isPending ? "Voting..." : "Against"} {isPendingVote === 0 ? "Voting Against..." : "Against"}
</SecondaryButton> </SecondaryButton>
</> </>
: <SecondaryButton : <SecondaryButton
@ -337,6 +345,8 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
chainId={chainId} chainId={chainId}
proposalId={id} proposalId={id}
isPending={isPending} isPending={isPending}
isPendingExecute={isPendingExecute}
isPendingRelease={isPendingRelease}
/> />
</Paper> </Paper>
</Box> </Box>
@ -392,7 +402,20 @@ const ProposalDetails = ({ chainId, address, connect, config }) => {
) )
} }
const VotingTimeline = ({ connect, handleExecute, handleRelease, proposalLocked, proposalId, chainId, state, address, isProposer, isPending }) => { const VotingTimeline = ({
connect,
handleExecute,
handleRelease,
proposalLocked,
proposalId,
chainId,
state,
address,
isProposer,
isPending,
isPendingExecute,
isPendingRelease,
}) => {
const { delay: propsalVotingDelay } = useProposalVotingDelay(chainId, proposalId); const { delay: propsalVotingDelay } = useProposalVotingDelay(chainId, proposalId);
const { snapshot: proposalSnapshot } = useProposalSnapshot(chainId, proposalId); const { snapshot: proposalSnapshot } = useProposalSnapshot(chainId, proposalId);
const { deadline: proposalDeadline } = useProposalDeadline(chainId, proposalId); const { deadline: proposalDeadline } = useProposalDeadline(chainId, proposalId);
@ -414,13 +437,15 @@ const VotingTimeline = ({ connect, handleExecute, handleRelease, proposalLocked,
<Box width="100%" display="flex" gap="10px"> <Box width="100%" display="flex" gap="10px">
{(isProposer && (proposalLocked?._value ?? 0n) > 0n) && <SecondaryButton {(isProposer && (proposalLocked?._value ?? 0n) > 0n) && <SecondaryButton
fullWidth fullWidth
loading={isPendingRelease}
disabled={isPending || state < 2} disabled={isPending || state < 2}
onClick={() => address === "" ? connect() : handleRelease()} onClick={() => address === "" ? connect() : handleRelease()}
> >
{address === "" ? "Connect" : "Release"} {address === "" ? "Connect" : isPendingRelease ? "Releasing..." : "Release"}
</SecondaryButton>} </SecondaryButton>}
<SecondaryButton <SecondaryButton
fullWidth fullWidth
loading={isPending}
disabled={isPending || state !== 4} disabled={isPending || state !== 4}
onClick={() => address === "" ? connect() : handleExecute()} onClick={() => address === "" ? connect() : handleExecute()}
> >
@ -428,7 +453,7 @@ const VotingTimeline = ({ connect, handleExecute, handleRelease, proposalLocked,
? "Connect" ? "Connect"
: state !== 4 : state !== 4
? convertStatusToLabel(state) ? convertStatusToLabel(state)
: isPending ? "Executing..." : "Execute" : isPendingExecute ? "Executing..." : "Execute"
} }
</SecondaryButton> </SecondaryButton>
</Box> </Box>

View File

@ -30,8 +30,6 @@ const ClaimConfirmationModal = (props) => {
props.ghstSymbol props.ghstSymbol
); );
break; break;
default:
console.log("Unknown action")
} }
setIsPending(false); setIsPending(false);

View File

@ -9,7 +9,7 @@ import {
Skeleton, Skeleton,
} from "@mui/material"; } from "@mui/material";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { useState, useMemo } from "react"; import { useState, useMemo, useCallback } from "react";
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from "@mui/material/useMediaQuery";
@ -26,9 +26,10 @@ import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { prettifySecondsInDays } from "../../../helpers/timeUtil"; import { prettifySecondsInDays } from "../../../helpers/timeUtil";
import { formatNumber, formatCurrency } from "../../../helpers"; import { formatNumber, formatCurrency } from "../../../helpers";
import { STAKING_ADDRESSES } from "../../../constants/addresses"; import { STAKING_ADDRESSES } from "../../../constants/addresses";
import { useCurrentIndex, useWarmupInfo } from "../../../hooks/staking"; import { useCurrentIndex, useWarmupInfo, claim, breakout } from "../../../hooks/staking";
import { useBalanceForShares, useTokenSymbol } from "../../../hooks/tokens"; import { useBalanceForShares, useTokenSymbol } from "../../../hooks/tokens";
import { useGhstPrice, useStnkPrice } from "../../../hooks/prices"; import { useGhstPrice, useStnkPrice } from "../../../hooks/prices";
import { useBreakoutModal } from "../../../hooks/breakoutModal";
import { isNetworkLegacy } from "../../../constants"; import { isNetworkLegacy } from "../../../constants";
import ClaimConfirmationModal from "./ClaimConfirmationModal"; import ClaimConfirmationModal from "./ClaimConfirmationModal";
@ -52,7 +53,8 @@ const StyledTableHeader = styled(TableHead)(({ theme }) => ({
export const ClaimsArea = ({ chainId, address, epoch }) => { export const ClaimsArea = ({ chainId, address, epoch }) => {
const isSmallScreen = useMediaQuery("(max-width: 745px)"); const isSmallScreen = useMediaQuery("(max-width: 745px)");
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false); const { breakoutFromStaking } = useBreakoutModal();
const [confirmationModalOpen, setConfirmationModalOpenInner] = useState(false);
const [isPayoutGhst, _] = useState(true); const [isPayoutGhst, _] = useState(true);
const ghstPrice = useGhstPrice(chainId); const ghstPrice = useGhstPrice(chainId);
@ -74,22 +76,45 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
return isPayoutGhst ? toClaim : toClaim.mul(currentIndex); return isPayoutGhst ? toClaim : toClaim.mul(currentIndex);
}, [chainId, claim, currentIndex, balanceForShares]); }, [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 = () => { const closeConfirmationModal = () => {
setConfirmationModalOpen(false); setConfirmationModalOpenInner(false);
claimRefetch(); claimRefetch();
currentIndexRefetch(); currentIndexRefetch();
} }
if (claim.shares === 0n) return <></>; if (claim.shares === 0n) return <></>;
const warmupTooltip = `Your claim earns rebases during warmup. You can emergency withdraw, but this forfeits the rebases`; const warmupTooltip = `Your claim earns rebases during warm-up. You can emergency withdraw, but this forfeits the rebases`;
return ( return (
<> <>
<ClaimConfirmationModal <ClaimConfirmationModal
open={confirmationModalOpen} open={confirmationModalOpen}
onClose={() => closeConfirmationModal()} onClose={() => closeConfirmationModal()}
chainid={chainId}
receiver={address} receiver={address}
receiveAmount={claim.expiry > epoch.number ? claim.deposit : claimableBalance} receiveAmount={claim.expiry > epoch.number ? claim.deposit : claimableBalance}
outputToken={claim.expiry > epoch.number ? ftsoSymbol : isPayoutGhst ? ghstSymbol : stnkSymbol} outputToken={claim.expiry > epoch.number ? ftsoSymbol : isPayoutGhst ? ghstSymbol : stnkSymbol}
@ -123,6 +148,7 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
claim={claim} claim={claim}
epoch={epoch} epoch={epoch}
isClaimable={claim.expiry > epoch.number} isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol} stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol} ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice} tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
@ -144,6 +170,7 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
claim={claim} claim={claim}
epoch={epoch} epoch={epoch}
isClaimable={claim.expiry > epoch.number} isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol} stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol} ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice} tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
@ -165,7 +192,8 @@ const ClaimInfo = ({
isPayoutGhst, isPayoutGhst,
stnkSymbol, stnkSymbol,
ghstSymbol, ghstSymbol,
tokenPrice tokenPrice,
isLegacy,
}) => { }) => {
return ( return (
<TableBody> <TableBody>
@ -193,6 +221,7 @@ const ClaimInfo = ({
<ActionButtons <ActionButtons
setConfirmationModalOpen={setConfirmationModalOpen} setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable} isClaimable={isClaimable}
isLegacy={isLegacy}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -210,6 +239,7 @@ const MobileClaimInfo = ({
ghstSymbol, ghstSymbol,
stnkSymbol, stnkSymbol,
tokenPrice, tokenPrice,
isLegacy,
}) => { }) => {
return ( return (
<Box mt="10px"> <Box mt="10px">
@ -239,12 +269,13 @@ const MobileClaimInfo = ({
isSmallScreen={true} isSmallScreen={true}
setConfirmationModalOpen={setConfirmationModalOpen} setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable} isClaimable={isClaimable}
isLegacy={isLegacy}
/> />
</Box> </Box>
); );
}; };
const ActionButtons = ({ setConfirmationModalOpen, isSmallScreen = false, isClaimable = false }) => { const ActionButtons = ({ setConfirmationModalOpen, isLegacy, isSmallScreen = false, isClaimable = false }) => {
return ( return (
<Box <Box
display="flex" display="flex"
@ -265,8 +296,8 @@ const ActionButtons = ({ setConfirmationModalOpen, isSmallScreen = false, isClai
fullWidth={isSmallScreen} fullWidth={isSmallScreen}
loading={false} loading={false}
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
onClick={() => setConfirmationModalOpen(true)} onClick={() => setConfirmationModalOpen(false)}
disabled={isClaimable} disabled={isLegacy && isClaimable}
> >
Claim Claim
</PrimaryButton> </PrimaryButton>

View File

@ -32,15 +32,19 @@ const StakeConfirmationModal = (props) => {
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
data-testid="acknowledge-warmup" data-testid="acknowledge-stake-warm-up"
checked={acknowledgedWarmup} checked={acknowledgedWarmup}
onChange={event => setAcknowledgedWarmup(event.target.checked)} onChange={event => setAcknowledgedWarmup(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />} icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />} checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/> />
} }
label={`I understand the ${props.ftsoSymbol} Im staking will only be available to claim ${warmupLength.toString()} epochs after my transaction is confirmed`} 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>
}
/>
</Box> </Box>
</> </>
); );
@ -55,7 +59,7 @@ const StakeConfirmationModal = (props) => {
fullWidth fullWidth
href="https://ghostchain.io/ghostdao_litepaper" href="https://ghostchain.io/ghostdao_litepaper"
> >
Why is there a warmup? Why is there a warm-up?
</SecondaryButton> </SecondaryButton>
</> </>
); );

View File

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

View File

@ -4,6 +4,7 @@ import { useConfig } from "wagmi";
import { useNavigate, createSearchParams } from "react-router-dom"; import { useNavigate, createSearchParams } from "react-router-dom";
import { formatNumber } from "../../../helpers"; import { formatNumber } from "../../../helpers";
import { useLiveBonds } from "../../../hooks/bonds/index";
import { useEpoch, useGatekeeperApy } from "../../../hooks/staking"; import { useEpoch, useGatekeeperApy } from "../../../hooks/staking";
import { useTokenSymbol, useBalance, useCirculatingSupply } from "../../../hooks/tokens"; import { useTokenSymbol, useBalance, useCirculatingSupply } from "../../../hooks/tokens";
import { SecondaryButton } from "../../../components/Button"; import { SecondaryButton } from "../../../components/Button";
@ -66,11 +67,22 @@ const ProtocolDetails = ({ chainId, isMobileScreen, theme, }) => {
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO"); const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: csprSymbol } = useTokenSymbol(chainId, "CSPR"); const { symbol: csprSymbol } = useTokenSymbol(chainId, "CSPR");
const { contractAddress: ftsoAddress } = useBalance(chainId, "FTSO", EMPTY_ADDRESS); const { contractAddress: ftsoAddress } = useBalance(chainId, "FTSO", EMPTY_ADDRESS);
const { liveBonds } = useLiveBonds(chainId);
const circulatingSupply = useCirculatingSupply(chainId, "STNK"); const circulatingSupply = useCirculatingSupply(chainId, "STNK");
const gatekeepedApy = useGatekeeperApy(chainId); const gatekeepedApy = useGatekeeperApy(chainId);
const { epoch } = useEpoch(chainId); const { epoch } = useEpoch(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 apyInner = useMemo(() => { const apyInner = useMemo(() => {
let apy = Infinity; let apy = Infinity;
if (circulatingSupply._value > 0n) { if (circulatingSupply._value > 0n) {
@ -108,8 +120,8 @@ const ProtocolDetails = ({ chainId, isMobileScreen, theme, }) => {
theme={theme} theme={theme}
url={`/${networkName.toLowerCase()}/bonds`} url={`/${networkName.toLowerCase()}/bonds`}
name="(1, 1) Bond" name="(1, 1) Bond"
sideName="Up to 40% Discount" sideName={maxBondDiscountTest}
description={`Bonding strategy grows Treasury and builds Protocol Owned Liquidity (POL) by allowing usersto acquire ${csprSymbol} at a discount. Take advantage of the next available bond.`} 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 <ProtocolDetail
@ -118,7 +130,7 @@ const ProtocolDetails = ({ chainId, isMobileScreen, theme, }) => {
url={`/${networkName.toLowerCase()}/stake`} url={`/${networkName.toLowerCase()}/stake`}
name="(3, 3) Stake" name="(3, 3) Stake"
sideName={`${formatNumber(apyInner, 0)}% APY`} sideName={`${formatNumber(apyInner, 0)}% APY`}
description={`Staking enables (3, 3) coordination by aligning long-term incentives, sustainably backed (1, 1) Bonds, LP fees, and Farming. Stake ${ftsoSymbol} to earn rewards and unlock ${bridgeNumbers} Stake\u00B2.`} 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 <ProtocolDetail
@ -127,7 +139,7 @@ const ProtocolDetails = ({ chainId, isMobileScreen, theme, }) => {
url={`/${networkName.toLowerCase()}/bridge`} url={`/${networkName.toLowerCase()}/bridge`}
name={`${bridgeNumbers} Stake\u00B2`} name={`${bridgeNumbers} Stake\u00B2`}
sideName={`${formatNumber(apyInner * gatekeepedApy, 0)}% APY`} sideName={`${formatNumber(apyInner * gatekeepedApy, 0)}% APY`}
description={`Staking\u00B2 strategy further deepens long-term incentives powered by sustainable cross-chain bridging revenue. ${bridgeNumbers} Stake\u00B2 your ${csprSymbol} to receive organic APY with no warmup and dillution.`} description={`Staking\u00B2 strategy further deepens long-term incentives powered by sustainable crosschain bridging revenue. ${bridgeNumbers} Stake\u00B2 your ${csprSymbol} to receive organic APY with no warm-up and no dilution.`}
/> />
</Box> </Box>
</Grid> </Grid>

View File

@ -1,6 +1,6 @@
export function shorten(str) { export function shorten(str, first = 6, second = -4) {
if (str.length < 10) return str; if (str.length < 10) return str;
return `${str.slice(0, 6)}...${str.slice(str.length - 4)}`; return `${str.slice(0, first)}...${str.slice(second)}`;
} }
export function capitalize(str) { export function capitalize(str) {
@ -39,7 +39,7 @@ export const formatNumber = (number, precision = 0) => {
}; };
export const sortBondsByDiscount = (bonds) => { export const sortBondsByDiscount = (bonds) => {
return Array.from(bonds).filter((bond) => !bond.isSoldOut).sort((a, b) => (a.discount.gt(b.discount) ? -1 : 1)); return Array.from(bonds).sort((a, b) => (a.discount.gt(b.discount) ? -1 : 1));
}; };
export const timeConverter = (time, max = 7200, maxText = "long ago") => { export const timeConverter = (time, max = 7200, maxText = "long ago") => {

View File

@ -1,10 +1,6 @@
import { useReadContract, useReadContracts } from "wagmi"; import { useReadContract, useReadContracts } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { isNetworkLegacyType } from "../../constants";
import { config } from "../../config";
import { import {
BOND_DEPOSITORY_ADDRESSES, BOND_DEPOSITORY_ADDRESSES,
DAO_TREASURY_ADDRESSES, DAO_TREASURY_ADDRESSES,
@ -17,7 +13,14 @@ import { abi as BondingCalculatorAbi } from "../../abi/GhostBondingCalculator.js
import { useReservePrice, useFtsoPrice } from "../prices"; import { useReservePrice, useFtsoPrice } from "../prices";
import { useOrinalCoefficient } from "../treasury"; import { useOrinalCoefficient } from "../treasury";
import { useTokenSymbol, useTokenSymbols } from "../tokens"; import { useTokenSymbol, useTokenSymbols } from "../tokens";
import { getTokenAddress, getTokenIcons, getBondNameDisplayName, getTokenPurchaseLink } from "../helpers"; import {
getTokenAddress,
getTokenIcons,
getBondNameDisplayName,
getTokenPurchaseLink,
executeOnChainTransaction
} from "../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { shorten } from "../../helpers"; import { shorten } from "../../helpers";
import { tokenNameConverter } from "../../helpers/tokenConverter"; import { tokenNameConverter } from "../../helpers/tokenConverter";
@ -267,78 +270,61 @@ export const useNotes = (chainId, address) => {
} }
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => { export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => {
const args = [ const args = [bondId, amount, maxPrice, user, referral];
bondId,
amount,
maxPrice,
user,
referral
];
const messages = { const messages = {
replacedMsg: "Bond transaction was replaced. Wait for inclusion please.", replacedMsg: "Bond transaction was replaced. Wait for inclusion please.",
successMsg: `Bond successfully purchased for ${shorten(user)}! Wait until it'll mature before claim.`, successMsg: `Bond successfully purchased for ${shorten(user)}! Wait until it'll mature before claim.`,
errorMsg: "Bond transaction failed. Check logs for error detalization.", errorMsg: "Bond transaction failed. Check logs for error detalization.",
}; };
await executeOnChainTransaction(
await executeOnChainTransaction({
chainId, chainId,
"deposit",
args, args,
sender,
messages, messages,
isNative ? amount : undefined abi: BondAbi,
); address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName: "deposit",
account: user,
value: isNative ? amount : undefined
});
} }
export const redeem = async ({ chainId, user, isGhst, indexes }) => { export const redeem = async ({ chainId, user, isGhst, indexes }) => {
const args = [ const args = [user, isGhst, indexes];
user,
isGhst,
indexes
];
const messages = { const messages = {
replacedMsg: "Redeem transaction was replaced. Wait for inclusion please.", replacedMsg: "Redeem transaction was replaced. Wait for inclusion please.",
successMsg: `Address ${shorten(user)} redeemed ${indexes.length} bonds in ${isGhst ? "GHST" : "STNK"}!`, successMsg: `Address ${shorten(user)} redeemed ${indexes.length} bonds in ${isGhst ? "GHST" : "STNK"}!`,
errorMsg: `Redeem of ${indexes.length} bonds failed. Check logs for error detalization.`, errorMsg: `Redeem of ${indexes.length} bonds failed. Check logs for error detalization.`,
}; };
await executeOnChainTransaction(
await executeOnChainTransaction({
chainId, chainId,
"redeem",
args, args,
user, messages,
messages abi: BondAbi,
); address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName: "redeem",
account: user,
});
} }
const executeOnChainTransaction = async ( export const forceRedeem = async ({ chainId, user, receiver, indexes }) => {
chainId, const args = [receiver, indexes];
functionName, const messages = {
args, replacedMsg: "Bond breakout transaction was replaced. Wait for inclusion please.",
account, successMsg: `Address ${shorten(user)} succesfully breakout for ${indexes.length} bonds.`,
messages, errorMsg: `Breakout of ${indexes.length} bonds failed. Check logs for error detalization.`,
value };
) => {
try {
const { request } = await simulateContract(config, {
abi: BondAbi,
address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName,
args,
account,
chainId,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
value: value,
});
const txHash = await writeContract(config, request); const txHash = await executeOnChainTransaction({
await waitForTransactionReceipt(config, { chainId,
hash: txHash, args,
onReplaced: () => toast(messages.replacedMsg), messages,
chainId abi: BondAbi,
}); address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName: "forceRedeem",
account: user,
});
toast.success(messages.successMsg); return txHash;
} catch (err) {
console.error(err);
toast.error(messages.errorMsg)
}
} }

View File

@ -0,0 +1,63 @@
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

@ -11,19 +11,29 @@ export const useUnstableProvider = () => useContext(UnstableProvider)
export const UnstableProviderProvider = ({ children }) => { export const UnstableProviderProvider = ({ children }) => {
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID); const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
const [isConnected, setIsConnected] = useState(false); const [providerIndex, setProviderIndex] = useState(0);
const [reconnectTicket, setReconnectTicket] = useState(0);
const { data: providerDetails } = useSWR("getGhostProviders", () => const { data: providerDetails } = useSWR("getGhostProviders", () =>
Unstable.getSubstrateConnectExtensionProviders() Unstable.getSubstrateConnectExtensionProviders()
); );
const [providerDetail, setProviderDetail] = useState(); const providerDetail = useMemo(() => providerDetails?.at(providerIndex), [providerDetails, providerIndex]);
const { data: provider } = useSWR( const { data: provider } = useSWR(
() => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.provider` : null, () => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.provider` : null,
() => providerDetail ? providerDetail.provider : null () => providerDetail ? providerDetail.provider : null
); );
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 client = useMemo(() => { const client = useMemo(() => {
if (!provider || !chainId) return undefined; if (!provider || !chainId) return undefined;
@ -31,56 +41,21 @@ export const UnstableProviderProvider = ({ children }) => {
if (!chain) return undefined; if (!chain) return undefined;
return createClient(chain.connect) return createClient(chain.connect)
}, [provider, chainId, reconnectTicket]); }, [provider, chainId]);
const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]); const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]); const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]);
const lastBlockNumber = useRef(0);
useEffect(() => {
if (!chainHead$) return;
lastBlockNumber.current = 0;
let timeoutId;
const sub = chainHead$.bestBlocks$.subscribe({
next: (blocks) => {
const currentHeight = blocks.at(0)?.number ?? -1;
if (currentHeight > lastBlockNumber.current) {
lastBlockNumber.current = currentHeight;
setIsConnected(true);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
setIsConnected(false);
setReconnectTicket(t => t + 1);
}, MAX_BLOCK_TIMEOUT);
}
},
error: (err) => {
setIsConnected(false);
setTimeout(() => setReconnectTicket(t => t + 1), 1000);
}
});
return () => {
sub.unsubscribe();
clearTimeout(timeoutId);
};
}, [chainHead$]);
const value = useMemo(() => ({ const value = useMemo(() => ({
isConnected, isExtensionMissing: connectionState === "no-extension",
providerDetails, providerDetails,
providerDetail, providerDetail,
connectProviderDetail: setProviderDetail, connectProviderByIndex: setProviderIndex,
chainId, chainId,
client, client,
setChainId, setChainId,
chainHead$ chainHead$
}), [isConnected, providerDetails, providerDetail, chainId, client, chainHead$]); }), [providerDetails, providerDetail, chainId, client, chainHead$]);
return ( return (
<UnstableProvider.Provider value={value}> <UnstableProvider.Provider value={value}>

View File

@ -1,11 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useReadContract, useReadContracts } from "wagmi"; import { useReadContract, useReadContracts } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast";
import { keccak256, stringToBytes } from 'viem' import { keccak256, stringToBytes } from 'viem'
import toast from "react-hot-toast";
import { isNetworkLegacyType } from "../../constants";
import { config } from "../../config";
import { import {
GHOST_GOVERNANCE_ADDRESSES, GHOST_GOVERNANCE_ADDRESSES,
@ -16,7 +12,7 @@ import { abi as GovernorStorageAbi } from "../../abi/GovernorStorage.json";
import { abi as GovernorVotesQuorumFractionAbi } from "../../abi/GovernorVotesQuorumFraction.json"; import { abi as GovernorVotesQuorumFractionAbi } from "../../abi/GovernorVotesQuorumFraction.json";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { getTokenDecimals, getTokenAbi, getTokenAddress } from "../helpers"; import { getTokenDecimals, getTokenAbi, getTokenAddress, executeOnChainTransaction } from "../helpers";
export const getVoteValue = (forVotes, totalVotes) => { export const getVoteValue = (forVotes, totalVotes) => {
if (totalVotes == 0n) return 0; if (totalVotes == 0n) return 0;
@ -41,7 +37,7 @@ export const useProposalVoteOf = (chainId, proposalId, who) => {
address: GHOST_GOVERNANCE_ADDRESSES[chainId], address: GHOST_GOVERNANCE_ADDRESSES[chainId],
functionName: "voteOf", functionName: "voteOf",
args: [proposalId, who], args: [proposalId, who],
scopeKey: `voteOf-${chainId}-${proposalId?.toString()}-${who}`, scopeKey: `voteOf-${chainId}-${proposalId ? proposalId.toString() : "undefined"}-${who}`,
chainId: chainId, chainId: chainId,
}); });
const voteOf = data ? BigInt(data) : 0n; const voteOf = data ? BigInt(data) : 0n;
@ -538,87 +534,57 @@ export const useProposals = (chainId, depth, searchedIndexes) => {
} }
export const releaseLocked = async (chainId, account, proposalId) => { export const releaseLocked = async (chainId, account, proposalId) => {
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "Release locked transaction was replaced. Wait for inclusion please.",
abi: GovernorAbi, successMsg: "Successfully release locked funds from the governor.",
address: GHOST_GOVERNANCE_ADDRESSES[chainId], errorMsg: "Release locked funds failed. Check logs for error detalization.",
functionName: 'releaseLocked', };
args: [proposalId],
account: account,
chainId: chainId,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
});
const txHash = await writeContract(config, request); await executeOnChainTransaction({
await waitForTransactionReceipt(config, { chainId,
hash: txHash, args: [proposalId],
onReplaced: () => toast("Release locked transaction was replaced. Wait for inclusion please."), abi: GovernorAbi,
chainId address: GHOST_GOVERNANCE_ADDRESSES[chainId],
}); functionName: "releaseLocked",
account,
toast.success("Successfully release locked funds from the governor."); messages,
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) => { export const executeProposal = async (chainId, account, proposalId) => {
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "Proposal execution transaction was replaced. Wait for inclusion please.",
abi: GovernorAbi, successMsg: "Proposal execution was successful, wait for updates.",
address: GHOST_GOVERNANCE_ADDRESSES[chainId], errorMsg: "Proposal execution failed. Check logs for error detalization.",
functionName: 'execute', };
args: [proposalId],
account: account,
chainId: chainId,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
});
const txHash = await writeContract(config, request); await executeOnChainTransaction({
await waitForTransactionReceipt(config, { chainId,
hash: txHash, args: [proposalId],
onReplaced: () => toast("Proposal execution transaction was replaced. Wait for inclusion please."), abi: GovernorAbi,
chainId address: GHOST_GOVERNANCE_ADDRESSES[chainId],
}); functionName: "execute",
account,
toast.success("Proposal execution was successful, wait for updates."); messages,
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) => { export const castVote = async (chainId, account, proposalId, support) => {
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "Cast vote transaction was replaced. Wait for inclusion please.",
abi: GovernorAbi, successMsg: "Successfully casted a vote, should be applied the proposal.",
address: GHOST_GOVERNANCE_ADDRESSES[chainId], errorMsg: "Vote cast failed. Check logs for error detalization.",
functionName: 'castVote', };
args: [proposalId, support],
account: account,
chainId: chainId,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
});
const txHash = await writeContract(config, request); await executeOnChainTransaction({
await waitForTransactionReceipt(config, { chainId,
hash: txHash, args: [proposalId, support],
onReplaced: () => toast("Cast vote transaction was replaced. Wait for inclusion please."), abi: GovernorAbi,
chainId address: GHOST_GOVERNANCE_ADDRESSES[chainId],
}); functionName: "castVote",
account,
toast.success("Successfully casted a vote, should be applied the proposal."); messages,
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) => { export const propose = async (chainId, account, functions, description) => {
@ -626,29 +592,19 @@ export const propose = async (chainId, account, functions, description) => {
const values = functions.map(f => f.value); const values = functions.map(f => f.value);
const calldatas = functions.map(f => f.calldata); const calldatas = functions.map(f => f.calldata);
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "Proposal transaction was replaced. Wait for inclusion please.",
abi: GovernorAbi, successMsg: "Successfully proposed a set of functions to be executed.",
address: GHOST_GOVERNANCE_ADDRESSES[chainId], errorMsg: "Proposal creation failed. Check logs for error detalization.",
functionName: 'propose', };
args: [targets, values, calldatas, description],
account: account,
chainId: chainId,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
});
const txHash = await writeContract(config, request); await executeOnChainTransaction({
await waitForTransactionReceipt(config, { chainId,
hash: txHash, args: [targets, values, calldatas, description],
onReplaced: () => toast("Proposal transaction was replaced. Wait for inclusion please."), abi: GovernorAbi,
chainId address: GHOST_GOVERNANCE_ADDRESSES[chainId],
}); functionName: "propose",
account,
toast.success("Successfully proposed a set of functions to be executed."); messages,
return true; });
} catch (err) {
console.error(err);
toast.error("Proposal creation failed. Check logs for error detalization.");
return false;
}
} }

View File

@ -1,3 +1,6 @@
import toast from "react-hot-toast";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import { import {
RESERVE_ADDRESSES, RESERVE_ADDRESSES,
FTSO_ADDRESSES, FTSO_ADDRESSES,
@ -6,6 +9,8 @@ import {
FTSO_DAI_LP_ADDRESSES, FTSO_DAI_LP_ADDRESSES,
WETH_ADDRESSES, WETH_ADDRESSES,
} from "../constants/addresses"; } from "../constants/addresses";
import { isNetworkLegacyType } from "../constants";
import { config } from "../config";
import { tokenNameConverter } from "../helpers/tokenConverter"; import { tokenNameConverter } from "../helpers/tokenConverter";
@ -201,3 +206,55 @@ export const getTokenPurchaseLink = (chainId, tokenAddress, chainName) => {
} }
return purchaseUrl; return purchaseUrl;
} }
const sanitizeTransactionRequest = (request, isLegacy) => {
const finalRequest = { ...request };
if (isLegacy) {
delete finalRequest.maxFeePerGas;
delete finalRequest.maxPriorityFeePerGas;
} else {
delete finalRequest.gasPrice;
}
return finalRequest;
}
export const executeOnChainTransaction = async ({
chainId,
abi,
address,
functionName,
args,
account,
messages,
value
}) => {
try {
const isLegacy = isNetworkLegacyType(chainId);
const { request } = await simulateContract(config, {
abi,
address,
functionName,
args,
account,
chainId,
value,
type: isLegacy ? 'legacy' : 'eip1559',
});
const finalRequest = sanitizeTransactionRequest(request, isLegacy);
const txHash = await writeContract(config, { ...finalRequest });
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

@ -15,7 +15,7 @@ export const LocalStorageProvider = ({ children }) => {
} }
const setStorageValue = (chainId, address, target, value) => { const setStorageValue = (chainId, address, target, value) => {
const key = getStorageKey(prefix, chainId, address, target); const key = getStorageKey(chainId, address, target);
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
} }

View File

@ -1,16 +1,13 @@
import { useReadContract } from "wagmi"; import { useReadContract } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { config } from "../../config";
import { isNetworkLegacyType } from "../../constants";
import { STAKING_ADDRESSES } from "../../constants/addresses"; import { STAKING_ADDRESSES } from "../../constants/addresses";
import { abi as StakingAbi } from "../../abi/GhostStaking.json"; import { abi as StakingAbi } from "../../abi/GhostStaking.json";
import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json"; import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
import { shorten } from "../../helpers"; import { shorten } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { executeOnChainTransaction } from "../helpers";
export const useGatekeeperApy = (chainId) => { export const useGatekeeperApy = (chainId) => {
const { data: gatekeeper, refetch } = useReadContract({ const { data: gatekeeper, refetch } = useReadContract({
@ -24,7 +21,7 @@ export const useGatekeeperApy = (chainId) => {
const { data: metadata, error } = useReadContract({ const { data: metadata, error } = useReadContract({
abi: GatekeeperAbi, abi: GatekeeperAbi,
address: gatekeeper, address: gatekeeper,
functionName: "gatekeeperMetadata", functionName: "metadata",
scopeKey: `gatekeeperMetadata-${chainId}-${gatekeeper}`, scopeKey: `gatekeeperMetadata-${chainId}-${gatekeeper}`,
chainId: chainId, chainId: chainId,
}); });
@ -155,45 +152,39 @@ export const useGhostedSupply = (chainId) => {
} }
export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSymbol, stnkSymbol, ghstSymbol) => { export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSymbol, stnkSymbol, ghstSymbol) => {
const args = [ const args = [amount, account, isRebase, isClaim];
amount,
account,
isRebase,
isClaim
];
const messages = { const messages = {
replacedMsg: "Staking transaction was replaced. Wait for inclusion please.", replacedMsg: "Staking transaction was replaced. Wait for inclusion please.",
successMsg: `${ftsoSymbol} tokens staked successfully! Wait for the warmup period to claim your ${isRebase ? stnkSymbol : ghstSymbol}.`, successMsg: `${ftsoSymbol} tokens staked successfully! Wait for the warm-up period to claim your ${isRebase ? stnkSymbol : ghstSymbol}.`,
errorMsg: "Staking transaction failed. Check logs for error detalization.", errorMsg: "Staking transaction failed. Check logs for error detalization.",
}; };
await executeOnChainTransaction( await executeOnChainTransaction({
chainId, chainId,
"stake",
args, args,
messages,
account, account,
messages abi: StakingAbi,
); address: STAKING_ADDRESSES[chainId],
functionName: "stake",
});
} }
export const unstake = async (chainId, account, amount, isTrigger, isRebase, ftsoSymbol, stnkSymbol, ghstSymbol) => { export const unstake = async (chainId, account, amount, isTrigger, isRebase, ftsoSymbol, stnkSymbol, ghstSymbol) => {
const args = [ const args = [amount, account, isTrigger, isRebase];
amount,
account,
isTrigger,
isRebase
];
const messages = { const messages = {
replacedMsg: "Unstake transaction was replaced. Wait for inclusion please.", replacedMsg: "Unstake transaction was replaced. Wait for inclusion please.",
successMsg: `${isRebase ? stnkSymbol : ghstSymbol} tokens unstaked successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`, successMsg: `${isRebase ? stnkSymbol : ghstSymbol} tokens unstaked successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
errorMsg: "Unstake transaction failed. Check logs for error detalization.", errorMsg: "Unstake transaction failed. Check logs for error detalization.",
}; };
await executeOnChainTransaction( await executeOnChainTransaction({
chainId, chainId,
"unstake",
args, args,
messages,
account, account,
messages abi: StakingAbi,
); address: STAKING_ADDRESSES[chainId],
functionName: "unstake",
});
} }
export const forfeit = async (chainId, account, ftsoSymbol) => { export const forfeit = async (chainId, account, ftsoSymbol) => {
@ -202,13 +193,15 @@ export const forfeit = async (chainId, account, ftsoSymbol) => {
successMsg: `Tokens forfeited successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`, successMsg: `Tokens forfeited successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
errorMsg: "Forfeit transaction failed. Check logs for error detalization.", errorMsg: "Forfeit transaction failed. Check logs for error detalization.",
}; };
await executeOnChainTransaction( await executeOnChainTransaction({
chainId, chainId,
"forfeit", messages,
[],
account, account,
messages args: [],
); abi: StakingAbi,
address: STAKING_ADDRESSES[chainId],
functionName: "forfeit",
});
} }
export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => { export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => {
@ -218,94 +211,89 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) =>
successMsg: `${isStnk ? stnkSymbol : ghstSymbol} tokens claimed successfully to ${shorten(account)}.`, successMsg: `${isStnk ? stnkSymbol : ghstSymbol} tokens claimed successfully to ${shorten(account)}.`,
errorMsg: "Claim transaction failed. Check logs for error detalization.", errorMsg: "Claim transaction failed. Check logs for error detalization.",
}; };
await executeOnChainTransaction( await executeOnChainTransaction({
chainId, chainId,
"claim",
args, args,
messages,
account, account,
messages 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;
} }
export const unwrap = async (chainId, account, amount, stnkSymbol) => { export const unwrap = async (chainId, account, amount, stnkSymbol) => {
const args = [account, amount];
const messages = { const messages = {
replacedMsg: "Unwrap transaction was replaced. Wait for inclusion please.", replacedMsg: "Unwrap transaction was replaced. Wait for inclusion please.",
successMsg: `Tokens unwrapped successfully! Check your ${stnkSymbol} balance on ${shorten(account)}.`, successMsg: `Tokens unwrapped successfully! Check your ${stnkSymbol} balance on ${shorten(account)}.`,
errorMsg: "Unwrap transaction failed. Check logs for error detalization.", errorMsg: "Unwrap transaction failed. Check logs for error detalization.",
}; };
await executeOnChainTransaction( const txHash = await executeOnChainTransaction({
chainId, chainId,
"unwrap", args,
[account, amount], messages,
account, account,
messages abi: StakingAbi,
); address: STAKING_ADDRESSES[chainId],
functionName: "unwrap",
});
} }
export const wrap = async (chainId, account, amount, ghstSymbol) => { export const wrap = async (chainId, account, amount, ghstSymbol) => {
const args = [account, amount];
const messages = { const messages = {
replacedMsg: "Wrap transaction was replaced. Wait for inclusion please.", replacedMsg: "Wrap transaction was replaced. Wait for inclusion please.",
successMsg: `Tokens wrapped successfully! Check your ${ghstSymbol} balance on ${shorten(account)}.`, successMsg: `Tokens wrapped successfully! Check your ${ghstSymbol} balance on ${shorten(account)}.`,
errorMsg: "Wrap transaction failed. Check logs for error detalization.", errorMsg: "Wrap transaction failed. Check logs for error detalization.",
}; };
await executeOnChainTransaction( const txHash = await executeOnChainTransaction({
chainId, chainId,
"wrap", args,
[account, amount], messages,
account, account,
messages abi: StakingAbi,
); address: STAKING_ADDRESSES[chainId],
functionName: "wrap",
});
} }
export const ghost = async (chainId, account, receiver, amount) => { export const ghost = async (chainId, account, receiver, amount) => {
const ars = [receiver, amount];
const messages = { const messages = {
replacedMsg: "Bridge transaction was replaced. Wait for inclusion please.", replacedMsg: "Bridge transaction was replaced. Wait for inclusion please.",
successMsg: `Amount successfully bridged! Check tx hash status or wait for slow clap finalization.`, successMsg: `Amount successfully bridged! Check tx hash status or wait for slow clap finalization.`,
errorMsg: "Bridge transaction failed. Check logs for error detalization.", errorMsg: "Bridge transaction failed. Check logs for error detalization.",
}; };
const txHash = await executeOnChainTransaction({
const txHash = await executeOnChainTransaction(
chainId, chainId,
"ghost", args,
[receiver, amount], messages,
account, account,
messages abi: StakingAbi,
); address: STAKING_ADDRESSES[chainId],
functionName: "ghost",
});
return txHash; 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,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
});
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,14 +1,16 @@
import { useReadContract, useReadContracts, useToken, useBalance as useInnerBalance } from "wagmi"; import { useReadContract, useReadContracts, useToken, useBalance as useInnerBalance } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { getTokenAbi, getTokenAddress, getTokenDecimals } from "../helpers"; import {
getTokenAbi,
getTokenAddress,
getTokenDecimals,
executeOnChainTransaction
} from "../helpers";
import { isNetworkLegacyType } from "../../constants";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { shorten } from "../../helpers"; import { shorten } from "../../helpers";
import { tokenNameConverter } from "../../helpers/tokenConverter"; import { tokenNameConverter } from "../../helpers/tokenConverter";
import { config } from "../../config";
import { WETH_ADDRESSES } from "../../constants/addresses"; import { WETH_ADDRESSES } from "../../constants/addresses";
export const usePastVotes = (chainId, name, timepoint, address) => { export const usePastVotes = (chainId, name, timepoint, address) => {
@ -204,132 +206,86 @@ export const useBalanceForShares = (chainId, name, amount) => {
}; };
export const approveTokens = async (chainId, name, owner, spender, value) => { export const approveTokens = async (chainId, name, owner, spender, value) => {
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "Approve transaction was replaced. Wait for inclusion please.",
abi: getTokenAbi(name), successMsg: `${name} tokens successfully approved to ${shorten(spender)}`,
address: getTokenAddress(chainId, name), errorMsg: `${name} tokens approval failed. Check logs for error detalization.`
functionName: 'approve', };
args: [spender, value], await executeOnChainTransaction({
account: owner, chainId,
chainId: chainId, args: [spender, value],
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559', abi: getTokenAbi(name),
}); address: getTokenAddress(chainId, name),
functionName: "approve",
const txHash = await writeContract(config, request); account: owner,
await waitForTransactionReceipt(config, { messages,
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) => { export const mintDai = async (chainId, account, value) => {
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "Mint transaction was replaced. Wait for inclusion please.",
abi: getTokenAbi("GDAI"), successMsg: "gDAI successfully minted to your wallet! Funds will be used on Ghost Faucet.",
address: getTokenAddress(chainId, "GDAI"), errorMsg: "Minting gDAI from the faucet failed. Check logs for error detalization."
functionName: 'mint', };
args: [account], await executeOnChainTransaction({
account: account, chainId,
value: value, args: [account],
chainId: chainId, abi: getTokenAbi("GDAI"),
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559', address: getTokenAddress(chainId, "GDAI"),
}); functionName: "mint",
account,
const txHash = await writeContract(config, request); messages,
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) => { export const burnDai = async (chainId, account, value) => {
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "Burn transaction was replaced. Wait for inclusion please.",
abi: getTokenAbi("GDAI"), successMsg: "gDAI successfully burned for native coins! Check your wallet balance.",
address: getTokenAddress(chainId, "GDAI"), errorMsg: "Burning gDAI from the faucet failed. Check logs for error detalization."
functionName: 'burn', };
args: [value], await executeOnChainTransaction({
account: account, chainId,
chainId: chainId, abi: getTokenAbi("GDAI"),
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559', address: getTokenAddress(chainId, "GDAI"),
}); functionName: 'burn',
args: [value],
const txHash = await writeContract(config, request); account,
await waitForTransactionReceipt(config, { messages,
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) => { export const depositNative = async (chainId, account, value) => {
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "WETH9 deposit transaction was replaced. Wait for inclusion please.",
abi: getTokenAbi("WETH"), successMsg: "WETH9 successfully minted to your wallet! Check your wallet balance.",
address: getTokenAddress(chainId, "WETH"), errorMsg: "WETH9 wrapping failed. Check logs for error detalization."
functionName: 'deposit', };
account: account, await executeOnChainTransaction({
chainId: chainId, chainId,
value: value, abi: getTokenAbi("WETH"),
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559', address: getTokenAddress(chainId, "WETH"),
}); functionName: 'deposit',
const txHash = await writeContract(config, request); value,
account,
await waitForTransactionReceipt(config, { messages,
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) => { export const withdrawWeth = async (chainId, account, value) => {
try { const messages = {
const { request } = await simulateContract(config, { replacedMsg: "WETH9 withdraw transaction was replaced. Wait for inclusion please.",
abi: getTokenAbi("WETH"), successMsg: "WETH9 successfully burned for native coins! Check your wallet balance.",
address: getTokenAddress(chainId, "WETH"), errorMsg: "WETH9 unwrapping failed. Check logs for error detalization."
functionName: 'withdraw', };
args: [value], await executeOnChainTransaction({
account: account, chainId,
chainId: chainId, abi: getTokenAbi("WETH"),
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559', address: getTokenAddress(chainId, "WETH"),
}); functionName: 'withdraw',
args: [value],
const txHash = await writeContract(config, request); account,
await waitForTransactionReceipt(config, { messages,
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

@ -1,12 +1,8 @@
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { isNetworkLegacyType } from "../../constants";
import { UNISWAP_V2_ROUTER, WETH_ADDRESSES } from "../../constants/addresses"; import { UNISWAP_V2_ROUTER, WETH_ADDRESSES } from "../../constants/addresses";
import { abi as RouterAbi } from "../../abi/UniswapV2Router.json"; import { abi as RouterAbi } from "../../abi/UniswapV2Router.json";
import { getTokenAddress } from "../helpers"; import { getTokenAddress, executeOnChainTransaction } from "../helpers";
import { config } from "../../config";
const swapMessages = { const swapMessages = {
replacedMsg: "Swap transaction was replaced. Wait for inclusion please.", replacedMsg: "Swap transaction was replaced. Wait for inclusion please.",
@ -27,6 +23,7 @@ export const swapExactETHForTokens = async ({
tokenNameTop, tokenNameTop,
tokenNameBottom, tokenNameBottom,
destination, destination,
address,
deadline deadline
}) => { }) => {
const args = [ const args = [
@ -38,9 +35,11 @@ export const swapExactETHForTokens = async ({
await executeOnChainTransaction({ await executeOnChainTransaction({
chainId, chainId,
functionName: "swapExactETHForTokens",
args, args,
account: destination, abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "swapExactETHForTokens",
account: address,
messages: swapMessages, messages: swapMessages,
value: amountADesired, value: amountADesired,
}); });
@ -66,8 +65,10 @@ export const swapExactTokensForETH = async ({
await executeOnChainTransaction({ await executeOnChainTransaction({
chainId, chainId,
functionName: "swapExactTokensForETH",
args, args,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "swapExactTokensForETH",
account: address, account: address,
messages: swapMessages, messages: swapMessages,
}); });
@ -93,10 +94,12 @@ export const swapExactTokensForTokens = async ({
await executeOnChainTransaction({ await executeOnChainTransaction({
chainId, chainId,
functionName: "swapExactTokensForTokens",
args, args,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "swapExactTokensForTokens",
account: address, account: address,
messages: swapMessages messages: swapMessages,
}); });
} }
@ -125,10 +128,12 @@ export const addLiquidity = async ({
await executeOnChainTransaction({ await executeOnChainTransaction({
chainId, chainId,
functionName: "addLiquidity",
args, args,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "addLiquidity",
account: address, account: address,
messages: addMessages messages: addMessages,
}); });
} }
@ -169,44 +174,12 @@ export const addLiquidityETH = async ({
await executeOnChainTransaction({ await executeOnChainTransaction({
chainId, chainId,
functionName: "addLiquidityETH",
args, args,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "addLiquidityETH",
account: address, account: address,
messages: addMessages, messages: addMessages,
value: amountETHDesired, value: amountETHDesired,
}); });
} }
const executeOnChainTransaction = async ({
chainId,
functionName,
args,
account,
messages,
value,
}) => {
try {
const { request } = await simulateContract(config, {
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName,
args,
account,
chainId,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
value: value ?? 0n,
});
const txHash = await writeContract(config, request);
await waitForTransactionReceipt(config, {
hash: txHash,
onReplaced: () => toast(messages.replacedMsg),
chainId
});
toast.success(messages.successMsg);
} catch (err) {
console.error(err);
toast.error(messages.errorMsg)
}
}

View File

@ -10,6 +10,7 @@ import { WagmiProvider } from "wagmi";
import { config } from "./config"; import { config } from "./config";
import { UnstableProviderProvider, MetadataProviderProvider } from "./hooks/ghost" import { UnstableProviderProvider, MetadataProviderProvider } from "./hooks/ghost"
import { LocalStorageProvider } from "./hooks/localstorage"; import { LocalStorageProvider } from "./hooks/localstorage";
import { BreakoutModalProvider } from "./hooks/breakoutModal";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -26,7 +27,9 @@ ReactDOM.createRoot(document.getElementById('root')).render(
<UnstableProviderProvider> <UnstableProviderProvider>
<MetadataProviderProvider> <MetadataProviderProvider>
<LocalStorageProvider> <LocalStorageProvider>
<App /> <BreakoutModalProvider>
<App />
</BreakoutModalProvider>
</LocalStorageProvider> </LocalStorageProvider>
</MetadataProviderProvider> </MetadataProviderProvider>
</UnstableProviderProvider> </UnstableProviderProvider>