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",
"private": true,
"version": "0.7.15",
"version": "0.7.40",
"type": "module",
"scripts": {
"dev": "vite",

View File

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

View File

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

View File

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

View File

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

View File

@ -71,7 +71,7 @@ function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, s
}
return(
<FormControl sx={{ width: small ? "auto" : "155px" }}>
<FormControl sx={{ width: small ? "100px" : "155px" }}>
<Select
labelId="network-select-helper-label"
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 Wallet from "./Wallet"
import SelectNetwork from "./SelectNetwork";
import GhostChainSelect from "./GhostChainSelect";
const PREFIX = "TopBar";
@ -23,8 +22,8 @@ function TopBar({
setWrongNetworkToastId
}) {
const themeColor = useTheme();
const desktop = useMediaQuery(themeColor.breakpoints.up(1130));
const small = useMediaQuery(themeColor.breakpoints.down(600));
const desktop = useMediaQuery(themeColor.breakpoints.up(1048));
const small = useMediaQuery(themeColor.breakpoints.down(400));
return (
<Box
display="flex"
@ -38,10 +37,9 @@ function TopBar({
display="flex"
justifyContent="space-between"
alignItems="center"
width={small ? "calc(100vw - 78px)" : "500px"}
width={small ? "calc(100vw - 78px)" : "320px"}
height="40px"
>
<GhostChainSelect small={small} />
<SelectNetwork
wrongNetworkToastId={wrongNetworkToastId}
setWrongNetworkToastId={setWrongNetworkToastId}

View File

@ -205,7 +205,10 @@ export const useWallet = (chainId, userAddress) => {
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: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { useState } from "react";
import { Box, Typography } from "@mui/material";
import { useState, useEffect } from "react";
import { Box, Typography, Checkbox, FormControlLabel } from "@mui/material";
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
import { styled, useTheme } from "@mui/material/styles";
import SettingsIcon from '@mui/icons-material/Settings';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
@ -10,13 +11,14 @@ import Metric from "../../../components/Metric/Metric";
import Modal from "../../../components/Modal/Modal";
import TokenStack from "../../../components/TokenStack/TokenStack";
import DataRow from "../../../components/DataRow/DataRow";
import { PrimaryButton } from "../../../components/Button";
import { PrimaryButton, SecondaryButton } from "../../../components/Button";
import BondDiscount from "./BondDiscount";
import BondVesting from "./BondVesting";
import BondSlippage from "./BondSlippage";
import { purchaseBond } from "../../../hooks/bonds";
import { useWarmupLength } from "../../../hooks/staking";
const StyledBox = styled(Box, {
shouldForwardProp: prop => prop !== "template",
@ -43,8 +45,57 @@ const BondConfirmModal = ({
handleConfirmClose
}) => {
const theme = useTheme();
const { warmupLength, warmupExists: needsWarmup } = useWarmupLength(chainId);
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
const [isPending, setIsPending] = useState(false);
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup]);
const AcknowledgeWarmupCheckbox = () => {
if (!needsWarmup) return <></>;
return (
<Box sx={{ marginBottom: "1rem" }}>
<FormControlLabel
control={
<Checkbox
data-testid="acknowledge-bond-warm-up"
checked={acknowledgedWarmup}
onChange={event => setAcknowledgedWarmup(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={
<Typography variant="body2">
{`I understand the ${bondQuoteTokenName} Im bonding will only be available to claim ${warmupLength.toString()} epochs after my transaction is confirmed, and the warm-up extends with each bond purchase`}
</Typography>
}
/>
</Box>
);
};
const NeedsWarmupDetails = () => {
if (!needsWarmup) return <></>;
return (
<>
<AcknowledgeWarmupCheckbox />
<SecondaryButton
fullWidth
href="https://ghostchain.io/ghostdao_litepaper"
>
Why is there a warm-up?
</SecondaryButton>
</>
);
};
const handleConfirmCloseMaster = () => {
setAcknowledgedWarmup(false);
handleConfirmClose()
}
const onSubmit = async () => {
setIsPending(true);
@ -68,7 +119,7 @@ const BondConfirmModal = ({
});
setIsPending(false);
handleConfirmClose();
handleConfirmCloseMaster();
}
return (
@ -84,7 +135,7 @@ const BondConfirmModal = ({
</Typography>
</Box>
}
onClose={!isPending && handleConfirmClose}
onClose={!isPending && handleConfirmCloseMaster}
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="Bond Slippage" balance={<BondSlippage slippage={slippage} textOnly />} />
<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"}
</PrimaryButton>
</PrimaryButton>}
</>
</Modal>
);

View File

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

View File

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

View File

@ -56,14 +56,14 @@ const WarmupConfirmModal = ({
? <FormControlLabel
control={
<Checkbox
data-testid="acknowledge-warmup"
data-testid="acknowledge-warm-up"
checked={isChecked}
onChange={event => setIsChecked(event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={`I acknowledge that I am releasing 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.`
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import {
Skeleton,
} from "@mui/material";
import { styled } from "@mui/material/styles";
import { useState, useMemo } from "react";
import { useState, useMemo, useCallback } from "react";
import useMediaQuery from "@mui/material/useMediaQuery";
@ -26,9 +26,10 @@ import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { prettifySecondsInDays } from "../../../helpers/timeUtil";
import { formatNumber, formatCurrency } from "../../../helpers";
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 { useGhstPrice, useStnkPrice } from "../../../hooks/prices";
import { useBreakoutModal } from "../../../hooks/breakoutModal";
import { isNetworkLegacy } from "../../../constants";
import ClaimConfirmationModal from "./ClaimConfirmationModal";
@ -52,7 +53,8 @@ const StyledTableHeader = styled(TableHead)(({ theme }) => ({
export const ClaimsArea = ({ chainId, address, epoch }) => {
const isSmallScreen = useMediaQuery("(max-width: 745px)");
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
const { breakoutFromStaking } = useBreakoutModal();
const [confirmationModalOpen, setConfirmationModalOpenInner] = useState(false);
const [isPayoutGhst, _] = useState(true);
const ghstPrice = useGhstPrice(chainId);
@ -74,22 +76,45 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
return isPayoutGhst ? toClaim : toClaim.mul(currentIndex);
}, [chainId, claim, currentIndex, balanceForShares]);
const breakoutBalance = useMemo(() => {
if (isNetworkLegacy(chainId)) {
return undefined; // short circuit
}
return isPayoutGhst ? claim.shares : claim.shares.mul(currentIndex);
}, [chainId, claim, currentIndex]);
const setConfirmationModalOpen = useCallback(async (value) => {
if (isNetworkLegacy(chainId) || value) {
setConfirmationModalOpenInner(true);
} else {
const defaultFunction = async () => {
await claim(chainId, address, false, stnkSymbol, ghstSymbol);
await claimRefetch();
}
const warmupLeft = claim.expiry - epoch.number;
const toExecute = async (receiver) => {
const txHash = await breakout(chainId, address, receiver, breakoutBalance);
return txHash;
}
breakoutFromStaking({ defaultFunction, toExecute, amount: claimableBalance, warmupLeft })
}
}, [claim, epoch, address, chainId, ghstSymbol, breakoutBalance, claimableBalance]);
const closeConfirmationModal = () => {
setConfirmationModalOpen(false);
setConfirmationModalOpenInner(false);
claimRefetch();
currentIndexRefetch();
}
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 (
<>
<ClaimConfirmationModal
open={confirmationModalOpen}
onClose={() => closeConfirmationModal()}
chainid={chainId}
receiver={address}
receiveAmount={claim.expiry > epoch.number ? claim.deposit : claimableBalance}
outputToken={claim.expiry > epoch.number ? ftsoSymbol : isPayoutGhst ? ghstSymbol : stnkSymbol}
@ -123,6 +148,7 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
claim={claim}
epoch={epoch}
isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
@ -144,6 +170,7 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
claim={claim}
epoch={epoch}
isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
@ -165,7 +192,8 @@ const ClaimInfo = ({
isPayoutGhst,
stnkSymbol,
ghstSymbol,
tokenPrice
tokenPrice,
isLegacy,
}) => {
return (
<TableBody>
@ -193,6 +221,7 @@ const ClaimInfo = ({
<ActionButtons
setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable}
isLegacy={isLegacy}
/>
</TableCell>
</TableRow>
@ -210,6 +239,7 @@ const MobileClaimInfo = ({
ghstSymbol,
stnkSymbol,
tokenPrice,
isLegacy,
}) => {
return (
<Box mt="10px">
@ -239,12 +269,13 @@ const MobileClaimInfo = ({
isSmallScreen={true}
setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable}
isLegacy={isLegacy}
/>
</Box>
);
};
const ActionButtons = ({ setConfirmationModalOpen, isSmallScreen = false, isClaimable = false }) => {
const ActionButtons = ({ setConfirmationModalOpen, isLegacy, isSmallScreen = false, isClaimable = false }) => {
return (
<Box
display="flex"
@ -265,8 +296,8 @@ const ActionButtons = ({ setConfirmationModalOpen, isSmallScreen = false, isClai
fullWidth={isSmallScreen}
loading={false}
sx={{ flexGrow: 1 }}
onClick={() => setConfirmationModalOpen(true)}
disabled={isClaimable}
onClick={() => setConfirmationModalOpen(false)}
disabled={isLegacy && isClaimable}
>
Claim
</PrimaryButton>

View File

@ -32,14 +32,18 @@ const StakeConfirmationModal = (props) => {
<FormControlLabel
control={
<Checkbox
data-testid="acknowledge-warmup"
data-testid="acknowledge-stake-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={`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>
</>
@ -55,7 +59,7 @@ const StakeConfirmationModal = (props) => {
fullWidth
href="https://ghostchain.io/ghostdao_litepaper"
>
Why is there a warmup?
Why is there a warm-up?
</SecondaryButton>
</>
);

View File

@ -35,7 +35,7 @@ const StakeSettingsModal = props => {
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={<Typography variant="body2">Always try to claim during stake, works only if 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>

View File

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

View File

@ -1,10 +1,6 @@
import { useReadContract, useReadContracts } from "wagmi";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast";
import { isNetworkLegacyType } from "../../constants";
import { config } from "../../config";
import {
BOND_DEPOSITORY_ADDRESSES,
DAO_TREASURY_ADDRESSES,
@ -17,7 +13,14 @@ import { abi as BondingCalculatorAbi } from "../../abi/GhostBondingCalculator.js
import { useReservePrice, useFtsoPrice } from "../prices";
import { useOrinalCoefficient } from "../treasury";
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 { shorten } from "../../helpers";
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 }) => {
const args = [
bondId,
amount,
maxPrice,
user,
referral
];
const args = [bondId, amount, maxPrice, user, referral];
const messages = {
replacedMsg: "Bond transaction was replaced. Wait for inclusion please.",
successMsg: `Bond successfully purchased for ${shorten(user)}! Wait until it'll mature before claim.`,
errorMsg: "Bond transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction(
await executeOnChainTransaction({
chainId,
"deposit",
args,
sender,
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 }) => {
const args = [
user,
isGhst,
indexes
];
const args = [user, isGhst, indexes];
const messages = {
replacedMsg: "Redeem transaction was replaced. Wait for inclusion please.",
successMsg: `Address ${shorten(user)} redeemed ${indexes.length} bonds in ${isGhst ? "GHST" : "STNK"}!`,
errorMsg: `Redeem of ${indexes.length} bonds failed. Check logs for error detalization.`,
};
await executeOnChainTransaction(
chainId,
"redeem",
args,
user,
messages
);
}
const executeOnChainTransaction = async (
await executeOnChainTransaction({
chainId,
functionName,
args,
account,
messages,
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,
functionName: "redeem",
account: user,
});
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)
}
}
export const forceRedeem = async ({ chainId, user, receiver, indexes }) => {
const args = [receiver, indexes];
const messages = {
replacedMsg: "Bond breakout transaction was replaced. Wait for inclusion please.",
successMsg: `Address ${shorten(user)} succesfully breakout for ${indexes.length} bonds.`,
errorMsg: `Breakout of ${indexes.length} bonds failed. Check logs for error detalization.`,
};
const txHash = await executeOnChainTransaction({
chainId,
args,
messages,
abi: BondAbi,
address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName: "forceRedeem",
account: user,
});
return txHash;
}

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 }) => {
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
const [isConnected, setIsConnected] = useState(false);
const [reconnectTicket, setReconnectTicket] = useState(0);
const [providerIndex, setProviderIndex] = useState(0);
const { data: providerDetails } = useSWR("getGhostProviders", () =>
Unstable.getSubstrateConnectExtensionProviders()
);
const [providerDetail, setProviderDetail] = useState();
const providerDetail = useMemo(() => providerDetails?.at(providerIndex), [providerDetails, providerIndex]);
const { data: provider } = useSWR(
() => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.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(() => {
if (!provider || !chainId) return undefined;
@ -31,56 +41,21 @@ export const UnstableProviderProvider = ({ children }) => {
if (!chain) return undefined;
return createClient(chain.connect)
}, [provider, chainId, reconnectTicket]);
}, [provider, chainId]);
const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
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(() => ({
isConnected,
isExtensionMissing: connectionState === "no-extension",
providerDetails,
providerDetail,
connectProviderDetail: setProviderDetail,
connectProviderByIndex: setProviderIndex,
chainId,
client,
setChainId,
chainHead$
}), [isConnected, providerDetails, providerDetail, chainId, client, chainHead$]);
}), [providerDetails, providerDetail, chainId, client, chainHead$]);
return (
<UnstableProvider.Provider value={value}>

View File

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

View File

@ -1,3 +1,6 @@
import toast from "react-hot-toast";
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import {
RESERVE_ADDRESSES,
FTSO_ADDRESSES,
@ -6,6 +9,8 @@ import {
FTSO_DAI_LP_ADDRESSES,
WETH_ADDRESSES,
} from "../constants/addresses";
import { isNetworkLegacyType } from "../constants";
import { config } from "../config";
import { tokenNameConverter } from "../helpers/tokenConverter";
@ -201,3 +206,55 @@ export const getTokenPurchaseLink = (chainId, tokenAddress, chainName) => {
}
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 key = getStorageKey(prefix, chainId, address, target);
const key = getStorageKey(chainId, address, target);
localStorage.setItem(key, JSON.stringify(value));
}

View File

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

View File

@ -1,12 +1,8 @@
import { simulateContract, writeContract, waitForTransactionReceipt } from "@wagmi/core";
import toast from "react-hot-toast";
import { isNetworkLegacyType } from "../../constants";
import { UNISWAP_V2_ROUTER, WETH_ADDRESSES } from "../../constants/addresses";
import { abi as RouterAbi } from "../../abi/UniswapV2Router.json";
import { getTokenAddress } from "../helpers";
import { config } from "../../config";
import { getTokenAddress, executeOnChainTransaction } from "../helpers";
const swapMessages = {
replacedMsg: "Swap transaction was replaced. Wait for inclusion please.",
@ -27,6 +23,7 @@ export const swapExactETHForTokens = async ({
tokenNameTop,
tokenNameBottom,
destination,
address,
deadline
}) => {
const args = [
@ -38,9 +35,11 @@ export const swapExactETHForTokens = async ({
await executeOnChainTransaction({
chainId,
functionName: "swapExactETHForTokens",
args,
account: destination,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "swapExactETHForTokens",
account: address,
messages: swapMessages,
value: amountADesired,
});
@ -66,8 +65,10 @@ export const swapExactTokensForETH = async ({
await executeOnChainTransaction({
chainId,
functionName: "swapExactTokensForETH",
args,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "swapExactTokensForETH",
account: address,
messages: swapMessages,
});
@ -93,10 +94,12 @@ export const swapExactTokensForTokens = async ({
await executeOnChainTransaction({
chainId,
functionName: "swapExactTokensForTokens",
args,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "swapExactTokensForTokens",
account: address,
messages: swapMessages
messages: swapMessages,
});
}
@ -125,10 +128,12 @@ export const addLiquidity = async ({
await executeOnChainTransaction({
chainId,
functionName: "addLiquidity",
args,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "addLiquidity",
account: address,
messages: addMessages
messages: addMessages,
});
}
@ -169,44 +174,12 @@ export const addLiquidityETH = async ({
await executeOnChainTransaction({
chainId,
functionName: "addLiquidityETH",
args,
abi: RouterAbi,
address: UNISWAP_V2_ROUTER[chainId],
functionName: "addLiquidityETH",
account: address,
messages: addMessages,
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 { UnstableProviderProvider, MetadataProviderProvider } from "./hooks/ghost"
import { LocalStorageProvider } from "./hooks/localstorage";
import { BreakoutModalProvider } from "./hooks/breakoutModal";
import ReactGA from "react-ga4";
const queryClient = new QueryClient();
@ -26,7 +27,9 @@ ReactDOM.createRoot(document.getElementById('root')).render(
<UnstableProviderProvider>
<MetadataProviderProvider>
<LocalStorageProvider>
<BreakoutModalProvider>
<App />
</BreakoutModalProvider>
</LocalStorageProvider>
</MetadataProviderProvider>
</UnstableProviderProvider>