Compare commits

...

6 Commits

Author SHA1 Message Date
421f2cef27
add flexibility during bond purchase
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-07 21:54:48 +03:00
a4b801efee
make network change based on the url params on the first render
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-07 21:01:30 +03:00
6acd4e92e2
apply naming fixes for the bond names based on the ui revision 14
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-07 19:56:03 +03:00
1c25be075c
enable claims from the warmup based on new smart contract logic
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-07 19:55:07 +03:00
ff43a26e0d
disable click on sold out bonds
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-07 19:53:46 +03:00
40b58f320a
visual fixes for the sidebar
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-04-07 19:52:44 +03:00
13 changed files with 263 additions and 190 deletions

View File

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

View File

@ -160,7 +160,7 @@ function App() {
setWrongNetworkToastId(null);
}
}
}, [chainId, addressChainId, isConnected])
}, [chainId, addressChainId, isConnected, wrongNetworkToastId])
useEffect(() => {
if (errorMessage) {
@ -215,7 +215,7 @@ function App() {
<Route path="/:network" element={<AvailableNetworkGuard allowedNetworks={chains.map(chain => chain.name.toLowerCase())} /> }>
<Route path="dashboard" element={<TreasuryDashboard chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bonds" element={<Bonds connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bonds/:id" element={<BondModalContainer connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bonds/:id" element={<BondModalContainer config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="stake" element={<StakeContainer 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} />} />

View File

@ -144,9 +144,9 @@ const NavContent = ({ chainId, addressChainId }) => {
to={`/${chainName}/bonds`}
children={
<AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}>
<Box width="180px" mb="10px" ml="auto">
{ghostBonds.length > 0 && <Box width="180px" mb="10px" ml="auto">
<Typography component="span" variant="body2">Bond Discounts</Typography>
</Box>
</Box>}
{sortBondsByDiscount(ghostBonds).map((bond, index) => {
return (
<Link
@ -192,8 +192,8 @@ const NavContent = ({ chainId, addressChainId }) => {
</div>
<Box>
<NavItem href="https://ghostchain.io/game-theory-3-3" icon={CasinoIcon} label={`${bridgeNumbers} Game Theory`} />
<NavItem href="http://ecosystem.ghostchain.io" icon={PublicIcon} label={`Ecosystem`} />
<NavItem href="https://ghostchain.io/game-theory-3-3" icon={CasinoIcon} label={`(3, 3) Game Theory`} />
<NavItem href="https://docs.ghostchain.io/" icon={BookIcon} label={`Documentation`} />
<StyledBox display="flex" justifyContent="space-around" paddingY="24px">
<Link href="https://git.ghostchain.io/ghostchain/ghost-dao-contracts" target="_blank" rel="noopener noreferrer">

View File

@ -12,8 +12,8 @@ import { isNetworkAvailable } from "../../constants";
import { parseKnownToken } from "../../components/Token/Token";
import { useSwitchChain } from 'wagmi';
import toast from "react-hot-toast";
import { useNavigate, useLocation } from 'react-router-dom';
import toast from "react-hot-toast";
function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, small }) {
const theme = useTheme();
@ -25,29 +25,43 @@ function SelectNetwork({ chainId, wrongNetworkToastId, setWrongNetworkToastId, s
const [networkId, setNetworkId] = useState(chainId);
useEffect(() => {
if (chainId !== networkId) setNetworkId(chainId);
}, [chainId])
useEffect(() => {
const chainName = chains.find((chain) => chain.id === chainId).name;
const parts = pathname.split('/');
if (parts.length > 2) {
parts[1] = chainName.toLowerCase();
const newPath = parts.join("/");
navigate(newPath);
let targetChain = chains.at(0);
const chainName = parts[1].toLowerCase();
const chain = chains.find(chain => chain.name.toLowerCase() === chainName);
if (chain && targetChain.name !== chain.name) {
targetChain = chain;
}
}, [chains, chainId])
changeNetworkId(targetChain.name, targetChain.id)
}
}, [])
const handleChange = (event) => {
const chainName = chains.find((chain) => chain.id === event.target.value).name;
useEffect(() => {
if (chainId !== networkId) setNetworkId(chainId);
}, [chainId, networkId])
const changeNetworkId = (chainName, newNetworkId) => {
toast.dismiss(wrongNetworkToastId);
const toastId = toast.loading(`Trying to connect to ${chainName} network... Wait please`, {
position: 'bottom-right'
});
setWrongNetworkToastId(toastId);
setNetworkId(event.target.value);
switchChain({ chainId: event.target.value });
setNetworkId(newNetworkId);
switchChain({ chainId: newNetworkId });
}
const handleChange = (event) => {
const chainName = chains.find((chain) => chain.id === event.target.value).name;
changeNetworkId(chainName, event.target.value);
const parts = pathname.split('/');
if (parts.length > 2) {
parts[1] = chainName.toLowerCase();
const newPath = parts.join("/");
navigate(newPath, { replace: true });
}
}
return(

View File

@ -18,24 +18,24 @@ import BondSettingsModal from "./components/BondSettingsModal";
import NotFound from "../NotFound/NotFound";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { NetworkId } from "../../constants";
import { isNetworkLegacy } from "../../constants";
import { formatCurrency } from "../../helpers";
import { useLocalStorage } from "../../hooks/localstorage";
import { useLiveBonds } from "../../hooks/bonds";
import { useFtsoPrice } from "../../hooks/prices";
const BondModalContainer = ({ chainId, address, connect }) => {
const BondModalContainer = ({ chainId, address, config, connect }) => {
const { id, network } = useParams();
const { liveBonds } = useLiveBonds(chainId, network);
const bond = liveBonds.find(bond => bond.id === Number(id));
if (!bond) return <NotFound />;
return <BondModal chainId={chainId} bond={bond} address={address} connect={connect} />;
return <BondModal config={config} chainId={chainId} bond={bond} address={address} connect={connect} />;
};
export const BondModal = ({ bond, chainId, address, connect }) => {
export const BondModal = ({ bond, chainId, address, config, connect }) => {
const navigate = useNavigate();
const { network } = useParams();
const { pathname } = useLocation();
@ -81,6 +81,23 @@ export const BondModal = ({ bond, chainId, address, connect }) => {
return () => window.removeEventListener("keydown", handleKeyDown);
}, [network, navigate, isSettingsOpen]);
const chainSymbol = useMemo(() => {
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
if (chainSymbol) return chainSymbol;
return "WTF";
}, [config]);
const preparedQuoteToken = useMemo(() => {
if (isNetworkLegacy(chainId)) {
return {
address: bond.quoteToken.quoteTokenAddress,
icons: bond.quoteToken.icons,
name: bond.quoteToken.name,
}
}
return { address: undefined, icons: [chainSymbol], name: chainSymbol };
}, [bond, chainSymbol, chainId])
return (
<Box>
<PageTitle
@ -95,10 +112,10 @@ export const BondModal = ({ bond, chainId, address, connect }) => {
</Box>
</Link>
<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "27px" }} />
<TokenStack tokens={preparedQuoteToken.icons} sx={{ fontSize: "27px" }} />
<Box display="flex" flexDirection="column" ml={1} justifyContent="center" alignItems="center">
<Typography variant="h4" fontWeight={500}>
{bond.quoteToken.name}
{preparedQuoteToken.name}
</Typography>
</Box>
</Box>
@ -144,10 +161,12 @@ export const BondModal = ({ bond, chainId, address, connect }) => {
<BondInputArea
chainId={chainId}
bond={bond}
config={config}
connect={connect}
address={address}
slippage={slippage}
recipientAddress={recipientAddress}
preparedQuoteToken={preparedQuoteToken}
handleSettingsOpen={() => setSettingsOpen(true)}
formatDecimals={formatDecimals}
/>

View File

@ -37,6 +37,9 @@ const BondConfirmModal = ({
sender,
handleSettingsOpen,
isOpen,
isNative,
bondQuoteTokenName,
bondQuoteTokenIcons,
handleConfirmClose
}) => {
const theme = useTheme();
@ -53,15 +56,16 @@ const BondConfirmModal = ({
const maxPrice = bond.price.inBaseToken._value * bigIntSlippage / one;
const referral = import.meta.env.VITE_APP_REFERRAL_ADDRESS;
await purchaseBond(
await purchaseBond({
chainId,
bond.id,
spendAmountValue._value.toBigInt(),
bondId: bond.id,
amount: spendAmountValue._value.toBigInt(),
maxPrice,
recipientAddress,
user: recipientAddress,
sender,
referral
);
referral,
isNative
});
setIsPending(false);
handleConfirmClose();
@ -74,9 +78,9 @@ const BondConfirmModal = ({
open={isOpen}
headerContent={
<Box display="flex" flexDirection="row">
<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "27px" }} />
<TokenStack tokens={bondQuoteTokenIcons} sx={{ fontSize: "27px" }} />
<Typography variant="h4" sx={{ marginLeft: "6px" }}>
{bond.quoteToken.name}
{bondQuoteTokenName}
</Typography>
</Box>
}
@ -91,7 +95,7 @@ const BondConfirmModal = ({
metric={spendAmount}
/>
<Box display="flex" flexDirection="row" justifyContent="center">
<Typography>{bond.quoteToken.name}</Typography>
<Typography>{bondQuoteTokenName}</Typography>
</Box>
</Box>
<GhostStyledIcon sx={{ transform: "rotate(-90deg)" }} component={ArrowDropDownIcon} />

View File

@ -30,6 +30,7 @@ const BondInputArea = ({
formatDecimals,
handleSettingsOpen,
address,
preparedQuoteToken,
connect
}) => {
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
@ -37,7 +38,7 @@ const BondInputArea = ({
const { pathname } = useLocation();
const { currentIndex } = useCurrentIndex(chainId);
const { balance, refetch } = useBalance(chainId, bond.quoteToken.quoteTokenAddress, address);
const { balance, refetch } = useBalance(chainId, preparedQuoteToken.address, address);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
@ -100,15 +101,14 @@ const BondInputArea = ({
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
id="from"
token={<TokenStack tokens={bond.quoteToken.icons} sx={{ fontSize: "21px" }} />}
tokenName={bond.quoteToken.name}
info={formatCurrency(balance, formatDecimals, bond.quoteToken.name)}
endString="Max"
tokenName={preparedQuoteToken.name}
info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)}
endString={preparedQuoteToken.address && "Max"}
endStringOnClick={setMax}
value={amount}
onChange={event => setAmount(event.currentTarget.value)}
inputProps={{ "data-testid": "fromInput" }}
/>
}
/>}
LowerSwapCard={
<SwapCard
maxWidth="476px"
@ -134,6 +134,7 @@ const BondInputArea = ({
connect={connect}
width="100%"
height="60px"
isNative={preparedQuoteToken.address === undefined}
isVertical
message={
<>
@ -221,6 +222,9 @@ const BondInputArea = ({
spendAmountValue={parsedAmount}
spendAmount={formatNumber(parsedAmount, formatDecimals)}
receiveAmount={formatNumber(amountInBaseToken, formatDecimals)}
bondQuoteTokenName={preparedQuoteToken.name}
bondQuoteTokenIcons={preparedQuoteToken.icons}
isNative={preparedQuoteToken.address === undefined}
handleSettingsOpen={handleSettingsOpen}
isOpen={confirmOpen}
sender={address}

View File

@ -132,9 +132,10 @@ const BondCard = ({ bond, secondsTo, chainName }) => {
<Link
component={NavLink}
to={`/${chainName}/bonds/${bond.id}`}
sx={{ pointerEvents: bond.isSoldOut ? 'none' : 'auto' }}
>
<TertiaryButton fullWidth>
Bond
<TertiaryButton fullWidth disabled={bond.isSoldOut}>
{bond.isSoldOut ? "Sold Out" : `Bond`}
</TertiaryButton>
</Link>
</Box>
@ -223,6 +224,7 @@ const BondRow = ({ bond, secondsTo, chainName }) => {
<Link
component={NavLink}
to={`/${chainName}/bonds/${bond.id}`}
sx={{ pointerEvents: bond.isSoldOut ? 'none' : 'auto' }}
>
<TertiaryButton fullWidth disabled={bond.isSoldOut}>
{bond.isSoldOut ? "Sold Out" : `Bond`}

View File

@ -56,12 +56,12 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
setIsWapmup(true);
} else {
setIsPending(true);
await redeem(
await redeem({
chainId,
address,
isPayoutGhst,
user: address,
isGhst: isPayoutGhst,
indexes
);
});
await notesRefetch();
setIsPending(false);
}
@ -99,8 +99,9 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
? formatCurrency(totalClaimableBalance, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(totalClaimableBalance), 5, stnkSymbol)
}
&nbsp;
({formatCurrency(totalClaimableBalance * ghstPrice, 2)})
</Typography>
<Typography variant="subtitle1" align="center">
{formatCurrency(totalClaimableBalance * ghstPrice, 2)}
</Typography>
</Box>

View File

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

View File

@ -28,7 +28,8 @@ import { formatNumber, formatCurrency } from "../../../helpers";
import { STAKING_ADDRESSES } from "../../../constants/addresses";
import { useCurrentIndex, useWarmupInfo } from "../../../hooks/staking";
import { useBalanceForShares, useTokenSymbol } from "../../../hooks/tokens";
import { useGhstPrice } from "../../../hooks/prices";
import { useGhstPrice, useStnkPrice } from "../../../hooks/prices";
import { isNetworkLegacy } from "../../../constants";
import ClaimConfirmationModal from "./ClaimConfirmationModal";
@ -55,6 +56,8 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
const [isPayoutGhst, _] = useState(true);
const ghstPrice = useGhstPrice(chainId);
const stnkPrice = useStnkPrice(chainId);
const { warmupInfo: claim, refetch: claimRefetch } = useWarmupInfo(chainId, address);
const { currentIndex, refetch: currentIndexRefetch } = useCurrentIndex(chainId);
const { balanceForShares } = useBalanceForShares(chainId, "STNK", claim.shares);
@ -63,6 +66,14 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const claimableBalance = useMemo(() => {
if (isNetworkLegacy(chainId)) {
return isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares;
}
const toClaim = new DecimalBigNumber(claim.shares, 18);
return isPayoutGhst ? toClaim : toClaim.mul(currentIndex);
}, [chainId, claim, currentIndex, balanceForShares]);
const closeConfirmationModal = () => {
setConfirmationModalOpen(false);
claimRefetch();
@ -80,7 +91,7 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
onClose={() => closeConfirmationModal()}
chainid={chainId}
receiver={address}
receiveAmount={claim.expiry > epoch.number ? claim.deposit : isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
receiveAmount={claim.expiry > epoch.number ? claim.deposit : claimableBalance}
outputToken={claim.expiry > epoch.number ? ftsoSymbol : isPayoutGhst ? ghstSymbol : stnkSymbol}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
@ -107,34 +118,35 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
{isSmallScreen ? (
<MobileClaimInfo
setConfirmationModalOpen={setConfirmationModalOpen}
prepareBalance={isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
prepareBalance={claimableBalance}
isPayoutGhst={isPayoutGhst}
claim={claim}
epoch={epoch}
isClaimable={claim.expiry > epoch.number}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
/>
) : (
<Table>
<StyledTableHeader className={classes.stakePoolHeaderText}>
<TableRow>
<TableCell style={{ width: "200px", padding: "8px 0" }}>Asset</TableCell>
<TableCell style={{ width: "200px", padding: "8px 0" }}>Amount</TableCell>
<TableCell style={{ width: "200px", padding: "8px 0" }}>Staked Amount</TableCell>
<TableCell style={{ width: "150px", padding: "8px 0" }}>Claimable In</TableCell>
<TableCell></TableCell>
</TableRow>
</StyledTableHeader>
<ClaimInfo
setConfirmationModalOpen={setConfirmationModalOpen}
prepareBalance={isPayoutGhst ? balanceForShares.div(currentIndex) : balanceForShares}
prepareBalance={claimableBalance}
isPayoutGhst={isPayoutGhst}
claim={claim}
epoch={epoch}
isClaimable={claim.expiry > epoch.number}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
ghstPrice={ghstPrice}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
/>
</Table>
@ -153,7 +165,7 @@ const ClaimInfo = ({
isPayoutGhst,
stnkSymbol,
ghstSymbol,
ghstPrice
tokenPrice
}) => {
return (
<TableBody>
@ -171,7 +183,7 @@ const ClaimInfo = ({
{formatCurrency(prepareBalance, 5, isPayoutGhst ? ghstSymbol : stnkSymbol)}
</Typography>
<Typography variant="body2" gutterBottom={false} style={{ lineHeight: 1.4 }}>
{formatCurrency(prepareBalance * ghstPrice, 2)}
{formatCurrency(prepareBalance * tokenPrice, 2)}
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 8px 8px 0" }}>
@ -188,7 +200,17 @@ const ClaimInfo = ({
);
};
const MobileClaimInfo = ({ setConfirmationModalOpen, prepareBalance, epoch, claim, isPayoutGhst, isClaimable, ghstSymbol, stnkSymbol }) => {
const MobileClaimInfo = ({
setConfirmationModalOpen,
prepareBalance,
epoch,
claim,
isPayoutGhst,
isClaimable,
ghstSymbol,
stnkSymbol,
tokenPrice,
}) => {
return (
<Box mt="10px">
<Box display="flex" flexDirection="row" alignItems="center" style={{ whiteSpace: "nowrap" }}>
@ -199,10 +221,15 @@ const MobileClaimInfo = ({ setConfirmationModalOpen, prepareBalance, epoch, clai
</Box>
<DataRow
title="Amount"
title="Staked Amount"
balance={formatNumber(prepareBalance, 7)}
/>
<DataRow
title="Price Estimation"
balance={formatCurrency(prepareBalance * tokenPrice, 2)}
/>
<DataRow
title="Claimed in"
balance={prettifySecondsInDays(epoch.length * (claim.expiry - epoch.number))}

View File

@ -151,12 +151,11 @@ export const useLiveBonds = (chainId, chainName) => {
);
const zero = new DecimalBigNumber(0n, 0);
const bondName = `${baseTokenSymbol}/${tokenNameConverter(chainId, "WETH")}`;
return {
id,
discount,
displayName: getBondNameDisplayName(chainId, bondName, quoteTokenAddress),
displayName: getBondNameDisplayName(chainId, quoteTokenAddress, baseTokenSymbol),
baseToken: {
name: baseTokenSymbol,
purchaseUrl: getTokenPurchaseLink(chainId, "", chainName),
@ -267,7 +266,7 @@ export const useNotes = (chainId, address) => {
return { notes, refetch };
}
export const purchaseBond = async (chainId, bondId, amount, maxPrice, user, sender, referral) => {
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => {
const args = [
bondId,
amount,
@ -285,11 +284,12 @@ export const purchaseBond = async (chainId, bondId, amount, maxPrice, user, send
"deposit",
args,
sender,
messages
messages,
isNative ? amount : undefined
);
}
export const redeem = async (chainId, user, isGhst, indexes) => {
export const redeem = async ({ chainId, user, isGhst, indexes }) => {
const args = [
user,
isGhst,
@ -314,7 +314,8 @@ const executeOnChainTransaction = async (
functionName,
args,
account,
messages
messages,
value
) => {
try {
const { request } = await simulateContract(config, {
@ -325,6 +326,7 @@ const executeOnChainTransaction = async (
account,
chainId,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
value: value,
});
const txHash = await writeContract(config, request);

View File

@ -178,9 +178,10 @@ export const getTokenIcons = (chainId, address) => {
return icons;
}
export const getBondNameDisplayName = (chainId, stringValue, tokenAddress) => {
export const getBondNameDisplayName = (chainId, tokenAddress, baseTokenSymbol) => {
let stringValue = tokenNameConverter(chainId, "WETH")
if (tokenAddress.toUpperCase() === FTSO_DAI_LP_ADDRESSES[chainId].toUpperCase()) {
stringValue = `${stringValue} LP`;
stringValue = `${baseTokenSymbol}-${stringValue} LP`;
}
return stringValue;
}