add flexibility during bond purchase

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-04-07 21:54:48 +03:00
parent a4b801efee
commit 421f2cef27
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
7 changed files with 186 additions and 155 deletions

View File

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

View File

@ -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

@ -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

@ -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,136 +99,137 @@ export const ClaimBonds = ({ chainId, address, secondsTo }) => {
? formatCurrency(totalClaimableBalance, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(totalClaimableBalance), 5, stnkSymbol)
}
&nbsp;
({formatCurrency(totalClaimableBalance * ghstPrice, 2)})
</Typography>
</Box>
<PrimaryButton
disabled={isPending || totalClaimableBalance._value === 0n}
fullWidth
className=""
onClick={() => onSubmit(notes.filter((note) => secondsTo > note.matured).map(note => note.id))}
>
Claim All
</PrimaryButton>
</Typography>
<Typography variant="subtitle1" align="center">
{formatCurrency(totalClaimableBalance * ghstPrice, 2)}
</Typography>
</Box>
<PrimaryButton
disabled={isPending || totalClaimableBalance._value === 0n}
fullWidth
className=""
onClick={() => onSubmit(notes.filter((note) => secondsTo > note.matured).map(note => note.id))}
>
Claim All
</PrimaryButton>
</Box>
</Box>
<Box mt="48px">
{isSmallScreen ? (
<>
{notes.map((note, index) => (
<Box key={index} mt="32px">
<Box display="flex" alignItems="center">
<TokenStack tokens={note.quoteToken.icons} />
<Box ml="8px">
<Typography>{note.quoteToken.name}</Typography>
</Box>
</Box>
<Box display="flex" justifyContent="space-between" mt="16px">
<Typography>Duration</Typography>
<Typography>
<BondVesting vesting={note.vesting} />
</Typography>
</Box>
<Box display="flex" justifyContent="space-between" mt="8px">
<Typography>Remaining</Typography>
<Typography>
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
</Typography>
</Box>
<Box display="flex" justifyContent="space-between" mt="8px">
<Typography>Payout</Typography>
<Box display="flex" flexDirection="column" alignItems="flex-end">
<Typography>
{isPayoutGhst
? formatCurrency(note.payout, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(note.payout), 5, stnkSymbol)
}
</Typography>
<Typography>{formatCurrency(note.payout * ghstPrice, 2)}</Typography>
</Box>
</Box>
<Box mt="16px">
<TertiaryButton
fullWidth
disabled={isPending || secondsTo < note.matured}
onClick={() => onSubmit([note.id])}
>
Claim
</TertiaryButton>
<Box mt="48px">
{isSmallScreen ? (
<>
{notes.map((note, index) => (
<Box key={index} mt="32px">
<Box display="flex" alignItems="center">
<TokenStack tokens={note.quoteToken.icons} />
<Box ml="8px">
<Typography>{note.quoteToken.name}</Typography>
</Box>
</Box>
))}
</>
) : (
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell style={{ width: "180px", padding: "8px 0" }}>Bond</TableCell>
<TableCell style={{ padding: "8px 0" }}>Duration</TableCell>
<TableCell style={{ padding: "8px 0" }}>Remaining</TableCell>
<TableCell style={{ padding: "8px 0" }}>Payout</TableCell>
<Box display="flex" justifyContent="space-between" mt="16px">
<Typography>Duration</Typography>
<Typography>
<BondVesting vesting={note.vesting} />
</Typography>
</Box>
<Box display="flex" justifyContent="space-between" mt="8px">
<Typography>Remaining</Typography>
<Typography>
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
</Typography>
</Box>
<Box display="flex" justifyContent="space-between" mt="8px">
<Typography>Payout</Typography>
<Box display="flex" flexDirection="column" alignItems="flex-end">
<Typography>
{isPayoutGhst
? formatCurrency(note.payout, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(note.payout), 5, stnkSymbol)
}
</Typography>
<Typography>{formatCurrency(note.payout * ghstPrice, 2)}</Typography>
</Box>
</Box>
<Box mt="16px">
<TertiaryButton
fullWidth
disabled={isPending || secondsTo < note.matured}
onClick={() => onSubmit([note.id])}
>
Claim
</TertiaryButton>
</Box>
</Box>
))}
</>
) : (
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell style={{ width: "180px", padding: "8px 0" }}>Bond</TableCell>
<TableCell style={{ padding: "8px 0" }}>Duration</TableCell>
<TableCell style={{ padding: "8px 0" }}>Remaining</TableCell>
<TableCell style={{ padding: "8px 0" }}>Payout</TableCell>
</TableRow>
</TableHead>
<TableBody>
{notes.map((note, index) => (
<TableRow key={index}>
<TableCell style={{ padding: "8px 0" }}>
<Box display="flex" alignItems="center">
<TokenStack tokens={note.quoteToken.icons} />
<Box display="flex" flexDirection="column" ml="16px">
<Typography>{note.quoteToken.name}</Typography>
</Box>
</Box>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Typography>
<BondVesting vesting={note.vesting} />
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Typography>
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Box display="flex" flexDirection="column" alignItems="flex-start">
<Typography>
{isPayoutGhst
? formatCurrency(note.payout, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(note.payout), 5, stnkSymbol)
}
</Typography>
<Typography variant="body2">{formatCurrency(note.payout * ghstPrice, 2)}</Typography>
</Box>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<TertiaryButton
fullWidth
disabled={isPending || secondsTo < note.matured}
onClick={() => onSubmit([note.id])}
>
Claim
</TertiaryButton>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{notes.map((note, index) => (
<TableRow key={index}>
<TableCell style={{ padding: "8px 0" }}>
<Box display="flex" alignItems="center">
<TokenStack tokens={note.quoteToken.icons} />
<Box display="flex" flexDirection="column" ml="16px">
<Typography>{note.quoteToken.name}</Typography>
</Box>
</Box>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Typography>
<BondVesting vesting={note.vesting} />
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Typography>
<BondDuration msg="Matured" secondsTo={secondsTo} duration={note.matured} />
</Typography>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<Box display="flex" flexDirection="column" alignItems="flex-start">
<Typography>
{isPayoutGhst
? formatCurrency(note.payout, 5, ghstSymbol)
: formatCurrency(currentIndex.mul(note.payout), 5, stnkSymbol)
}
</Typography>
<Typography variant="body2">{formatCurrency(note.payout * ghstPrice, 2)}</Typography>
</Box>
</TableCell>
<TableCell style={{ padding: "8px 0" }}>
<TertiaryButton
fullWidth
disabled={isPending || secondsTo < note.matured}
onClick={() => onSubmit([note.id])}
>
Claim
</TertiaryButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
</Paper>
</>
);

View File

@ -266,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,
@ -284,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,
@ -313,7 +314,8 @@ const executeOnChainTransaction = async (
functionName,
args,
account,
messages
messages,
value
) => {
try {
const { request } = await simulateContract(config, {
@ -324,6 +326,7 @@ const executeOnChainTransaction = async (
account,
chainId,
type: isNetworkLegacyType(chainId) ? 'legacy' : 'eip1559',
value: value,
});
const txHash = await writeContract(config, request);