make staking breakout works

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-05-04 15:12:31 +03:00
parent bc76372897
commit fcc3d341d9
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
8 changed files with 126 additions and 46 deletions

View File

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

File diff suppressed because one or more lines are too long

View File

@ -24,11 +24,13 @@ import { useTokenSymbol, useCirculatingSupply } from "../../hooks/tokens";
import { useEpoch, useGatekeeperApy, useGatekeeperAddress } from "../../hooks/staking"; import { useEpoch, useGatekeeperApy, useGatekeeperAddress } from "../../hooks/staking";
import { useEvmNetwork, useCurrentIndex, useUnstableProvider } from "../../hooks/ghost"; import { useEvmNetwork, useCurrentIndex, useUnstableProvider } from "../../hooks/ghost";
import { formatNumber, shorten } from "../../helpers"; import { formatNumber, shorten } from "../../helpers";
import { prettifySecondsInDays } from "../../helpers/timeUtil";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
const BreakoutModal = ({ chainId, address }) => { const BreakoutModal = ({ chainId, address }) => {
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [receiver, setReceiver] = useState(""); const [receiver, setReceiver] = useState("");
const [convertedReceiver, setConvertedReceiver] = useState(undefined);
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO"); const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
@ -78,6 +80,18 @@ const BreakoutModal = ({ chainId, address }) => {
return `(${number}, ${number})`; return `(${number}, ${number})`;
}, [chainId]); }, [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 ( return (
<Modal <Modal
headerContent={ headerContent={
@ -113,6 +127,9 @@ const BreakoutModal = ({ chainId, address }) => {
estimatedAmount={estimatedAmount} estimatedAmount={estimatedAmount}
incomingFee={incomingFee} incomingFee={incomingFee}
goNext={() => setStep(2)} goNext={() => setStep(2)}
convertedReceiver={convertedReceiver}
setConvertedReceiver={setConvertedReceiver}
/> />
: <ConfirmStep : <ConfirmStep
chainId={chainId} chainId={chainId}
@ -127,6 +144,7 @@ const BreakoutModal = ({ chainId, address }) => {
setActiveTxIndex={setActiveTxIndex} setActiveTxIndex={setActiveTxIndex}
closeModal={closeModalPure} closeModal={closeModalPure}
evmNetwork={evmNetwork} evmNetwork={evmNetwork}
convertedReceiver={convertedReceiver}
/> />
} }
</Box> </Box>
@ -138,6 +156,8 @@ const BridgeView = ({
chainId, chainId,
receiver, receiver,
setReceiver, setReceiver,
convertedReceiver,
setConvertedReceiver,
bridgeNumbers, bridgeNumbers,
ghstSymbol, ghstSymbol,
estimatedAmount, estimatedAmount,
@ -145,7 +165,6 @@ const BridgeView = ({
incomingFee incomingFee
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const [convertedReceiver, setConvertedReceiver] = useState(undefined);
const config = useConfig(); const config = useConfig();
const { gatekeeperAddress } = useGatekeeperAddress(chainId); const { gatekeeperAddress } = useGatekeeperAddress(chainId);
@ -155,18 +174,6 @@ const BridgeView = ({
return client?.chain?.blockExplorers?.default?.url; return client?.chain?.blockExplorers?.default?.url;
}, [config]); }, [config]);
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 ( return (
<> <>
<Typography>Bridge to start earning {bridgeNumbers} {"Stake\u00B2"} on your {ghstSymbol} balance:</Typography> <Typography>Bridge to start earning {bridgeNumbers} {"Stake\u00B2"} on your {ghstSymbol} balance:</Typography>
@ -283,14 +290,17 @@ const WelcomeView = ({
<Typography variant="h5">{formatNumber(apyInner, 2)}% APY</Typography> <Typography variant="h5">{formatNumber(apyInner, 2)}% APY</Typography>
</Box> </Box>
{warmupPeriod <= 0 && <SecondaryButton <SecondaryButton
onClick={() => callDefaultFunction()} onClick={() => callDefaultFunction()}
disabled={isPending || warmupPeriod > 0} disabled={isPending || warmupPeriod > 0}
loading={isPending} loading={isPending}
fullWidth fullWidth
> >
{`${isPending ? "Claiming..." : "Claim"} ${isStakingOpened ? "(3, 3) Stake" : "(1, 1) Bond"}`} {warmupPeriod > 0
</SecondaryButton>} ? `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"> <Box display="flex" justifyContent="center" flexDirection="column" alignItems="center">
<hr style={{ width: "100%" }} /> <hr style={{ width: "100%" }} />
@ -322,6 +332,7 @@ const ConfirmStep = ({
chainId, chainId,
address, address,
receiver, receiver,
convertedReceiver,
executableFunction, executableFunction,
ghstSymbol, ghstSymbol,
bridgeNumbers, bridgeNumbers,
@ -360,7 +371,7 @@ const ConfirmStep = ({
const execute = useCallback(async () => { const execute = useCallback(async () => {
setIsPending(true); setIsPending(true);
try { try {
const txHash = await executableFunction()(receiver); const txHash = await executableFunction()(convertedReceiver);
if (txHash) { if (txHash) {
const expectedSessionIndex = (currentSession ?? 0) + (evmNetwork const expectedSessionIndex = (currentSession ?? 0) + (evmNetwork
? Number((evmNetwork.avg_block_speed * evmNetwork.finality_delay) / (1000n * 14400n)) ? Number((evmNetwork.avg_block_speed * evmNetwork.finality_delay) / (1000n * 14400n))
@ -369,10 +380,7 @@ const ConfirmStep = ({
const transaction = { const transaction = {
sessionIndex: expectedSessionIndex, sessionIndex: expectedSessionIndex,
transactionHash: txHash, transactionHash: txHash,
receiverAddress: receiver,
amount: estimatedAmount._value.toString(),
chainId: chainId, chainId: chainId,
blockNumber: Number(blockNumber),
bridgeStability: 69, // TODO: avoid stability bridgeStability: 69, // TODO: avoid stability
timestamp: Date.now() timestamp: Date.now()
} }
@ -388,7 +396,17 @@ const ConfirmStep = ({
setIsPending(false); setIsPending(false);
closeModal(); closeModal();
} }
}, [executableFunction, receiver, networkName, chainId, address, blockNumber, evmNetwork, currentSession]); }, [
executableFunction,
convertedReceiver,
receiver,
networkName,
chainId,
address,
blockNumber,
evmNetwork,
currentSession,
]);
return ( return (
<> <>

View File

@ -1,5 +1,6 @@
import { useEffect, useState, useMemo, useCallback } from "react"; import { useEffect, useState, useMemo, useCallback } from "react";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import useSWR from "swr";
import { import {
Box, Box,
@ -11,9 +12,9 @@ import {
} from "@mui/material"; } from "@mui/material";
import { decodeAddress } from "@polkadot/util-crypto"; import { decodeAddress } from "@polkadot/util-crypto";
import { fromHex } from "@polkadot-api/utils"; import { fromHex } from "@polkadot-api/utils";
import { getBlockNumber } from "@wagmi/core"; import { getBlockNumber, getTransaction } from "@wagmi/core";
import { useTransaction } from "wagmi"; import { useTransaction } from "wagmi";
import { keccak256 } from "viem"; import { keccak256, decodeFunctionData } from "viem";
import { u32, u64, u128 } from "scale-ts"; import { u32, u64, u128 } from "scale-ts";
import PendingActionsIcon from '@mui/icons-material/PendingActions'; import PendingActionsIcon from '@mui/icons-material/PendingActions';
@ -23,6 +24,7 @@ import PageTitle from "../../components/PageTitle/PageTitle";
import Paper from "../../components/Paper/Paper"; import Paper from "../../components/Paper/Paper";
import GhostStyledIcon from "../../components/Icon/GhostIcon"; import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { abi as StakingAbi } from "../../abi/GhostStaking.json";
import { networkAvgBlockSpeed } from "../../constants"; import { networkAvgBlockSpeed } from "../../constants";
import { timeConverter } from "../../helpers"; import { timeConverter } from "../../helpers";
@ -100,12 +102,25 @@ const Bridge = ({ chainId, address, config, connect }) => {
hash: watchTransaction?.transactionHash hash: watchTransaction?.transactionHash
}); });
const watchTransactionArgs = useMemo(() => {
if (watchTransactionInfo && watchTransactionInfo.input) {
const { functionName, args } = decodeFunctionData({
abi: StakingAbi,
data: watchTransactionInfo.input,
});
return { receiver: args.at(0), amount: args.at(1) }
}
return { receiver: "", amount: 0n }
}, [watchTransactionInfo]);
const hashedArguments = useMemo(() => { const hashedArguments = useMemo(() => {
if (!watchTransaction) return undefined if (!watchTransaction || !watchTransactionArgs.receiver) {
return undefined;
}
const networkIdEncoded = u64.enc(BigInt(chainId)); const networkIdEncoded = u64.enc(BigInt(chainId));
const amountEncoded = u128.enc(BigInt(watchTransaction.amount)); const amountEncoded = u128.enc(BigInt(watchTransactionArgs.amount));
const addressEncoded = decodeAddress(watchTransaction.receiverAddress, false, 1996); const addressEncoded = decodeAddress(watchTransactionArgs.receiver, false, 1996);
const transactionHashEncoded = fromHex(watchTransaction.transactionHash); const transactionHashEncoded = fromHex(watchTransaction.transactionHash);
const blockNumber = u64.enc(watchTransactionInfo?.blockNumber ?? 0n); const blockNumber = u64.enc(watchTransactionInfo?.blockNumber ?? 0n);
@ -117,7 +132,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
...networkIdEncoded ...networkIdEncoded
]); ]);
return keccak256(clapArgsStr) return keccak256(clapArgsStr)
}, [watchTransaction, watchTransactionInfo]) }, [watchTransaction, watchTransactionInfo, watchTransactionArgs])
const latestBlockNumber = useLatestBlockNumber(); const latestBlockNumber = useLatestBlockNumber();
const eraIndex = useEraIndex(); const eraIndex = useEraIndex();
@ -183,7 +198,8 @@ const Bridge = ({ chainId, address, config, connect }) => {
return sum + countOnesInBigInt(bigIntValue); return sum + countOnesInBigInt(bigIntValue);
}, 0); }, 0);
const finalization = Math.max(0, (finalityDelay + watchTransaction.blockNumber) - Number(blockNumber)); const storedBlockNumber = watchTransactionInfo ? Number(watchTransactionInfo.blockNumber) : 0;
const finalization = Math.max(0, (finalityDelay + storedBlockNumber) - Number(blockNumber));
const applaused = transactionApplaused?.finalized ?? false; const applaused = transactionApplaused?.finalized ?? false;
const clappedAmount = transactionApplaused?.clapped_amount ?? 0n; const clappedAmount = transactionApplaused?.clapped_amount ?? 0n;
const clappedPercentage = clappedAmount * 100n / (totalStakedAmount ?? 1n); const clappedPercentage = clappedAmount * 100n / (totalStakedAmount ?? 1n);
@ -191,6 +207,8 @@ const Bridge = ({ chainId, address, config, connect }) => {
return { return {
...watchTransaction, ...watchTransaction,
receiverAddress: watchTransactionArgs.receiver,
amount: watchTransactionArgs.amount,
finalization, finalization,
applaused, applaused,
numberOfClaps, numberOfClaps,
@ -199,18 +217,49 @@ const Bridge = ({ chainId, address, config, connect }) => {
clapsPercentage, clapsPercentage,
} }
}, [ }, [
watchTransaction,
watchTransactionArgs,
watchTransactionInfo,
transactionApplaused, transactionApplaused,
finalityDelay, finalityDelay,
watchTransaction,
blockNumber, blockNumber,
totalStakedAmount, totalStakedAmount,
authorities authorities
]); ]);
const filteredStoredTransactions = useMemo(() => { const filteredStoredTransactionInfos = useMemo(() => {
return storedTransactions.filter(obj => obj.chainId === chainId); return storedTransactions.filter(obj => obj.chainId === chainId);
}, [storedTransactions, chainId]); }, [storedTransactions, chainId]);
const { data: filteredStoredTransactions } = useSWR(
filteredStoredTransactionInfos.length > 0
? ["filtered-tx", chainId, filteredStoredTransactionInfos.map(t => t.transactionHash)]
: undefined,
async ([,, hashes]) => {
const results = await Promise.all(
hashes.map(hash => getTransaction(config, { hash }).catch(() => undefined))
);
return filteredStoredTransactionInfos.map((tx, index) => {
const txInfo = results.at(index);
let decodedData = { receiverAddress: "unknown", amount: 0n };
if (txInfo && txInfo.input) {
const { args } = decodeFunctionData({
abi: StakingAbi,
data: txInfo.input,
});
if (args && args.at(0) && args.at(1)) {
decodedData = { receiverAddress: args.at(0), amount: args.at(1) };
}
}
return { ...tx, ...decodedData };
})
},
{ revalidateOnFocus: false }
)
const latestCommits = useMemo(() => { const latestCommits = useMemo(() => {
return validators?.map((validator, index) => { return validators?.map((validator, index) => {
const lastUpdatedNumber = Number(blockCommitments?.at(index)?.last_updated ?? 0); const lastUpdatedNumber = Number(blockCommitments?.at(index)?.last_updated ?? 0);

View File

@ -307,7 +307,7 @@ export const BridgeCardHistory = ({
<Box display="flex" flexDirection="column" justifyContent="center"> <Box display="flex" flexDirection="column" justifyContent="center">
<Typography variant="caption"> <Typography variant="caption">
{formatCurrency( {formatCurrency(
new DecimalBigNumber(BigInt(obj.amount), 18).toString(), new DecimalBigNumber(obj.amount, 18).toString(),
isSemiSmallScreen ? 3 : 8, isSemiSmallScreen ? 3 : 8,
ghstSymbol ghstSymbol
)} )}

View File

@ -76,9 +76,16 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
return isPayoutGhst ? toClaim : toClaim.mul(currentIndex); return isPayoutGhst ? toClaim : toClaim.mul(currentIndex);
}, [chainId, claim, currentIndex, balanceForShares]); }, [chainId, claim, currentIndex, balanceForShares]);
const breakoutBalance = useMemo(() => {
if (isNetworkLegacy(chainId)) {
return undefined; // short circuit
}
return isPayoutGhst ? claim.shares : claim.shares.mul(currentIndex);
}, [chainId, claim, currentIndex]);
const setConfirmationModalOpen = useCallback(async (value) => { const setConfirmationModalOpen = useCallback(async (value) => {
if (isNetworkLegacy(chainId) || claim.expiry > epoch.number) { if (isNetworkLegacy(chainId) || value) {
setConfirmationModalOpenInner(value); setConfirmationModalOpenInner(true);
} else { } else {
const defaultFunction = async () => { const defaultFunction = async () => {
await claim(chainId, address, false, stnkSymbol, ghstSymbol); await claim(chainId, address, false, stnkSymbol, ghstSymbol);
@ -86,22 +93,22 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
} }
const warmupLeft = claim.expiry - epoch.number; const warmupLeft = claim.expiry - epoch.number;
const toExecute = async (receiver) => { const toExecute = async (receiver) => {
await breakout(chainId, address, receiver, claimableBalance); const txHash = await breakout(chainId, address, receiver, breakoutBalance);
await claimRefetch(); return txHash;
} }
breakoutFromStaking({ defaultFunction, toExecute, amount: claimableBalance, warmupLeft }) breakoutFromStaking({ defaultFunction, toExecute, amount: claimableBalance, warmupLeft })
} }
}, [claim, epoch, address, chainId, ghstSymbol]); }, [claim, epoch, address, chainId, ghstSymbol, breakoutBalance, claimableBalance]);
const closeConfirmationModal = () => { const closeConfirmationModal = () => {
setConfirmationModalOpen(false); setConfirmationModalOpenInner(false);
claimRefetch(); claimRefetch();
currentIndexRefetch(); currentIndexRefetch();
} }
if (claim.shares === 0n) return <></>; if (claim.shares === 0n) return <></>;
const warmupTooltip = `Your claim earns rebases during warm-up. You can emergency withdraw, but this forfeits the rebases`; const warmupTooltip = `Your claim earns rebases during warm-up. You can emergency withdraw, but this forfeits the rebases`;
return ( return (
<> <>
@ -141,6 +148,7 @@ const warmupTooltip = `Your claim earns rebases during warm-up. You can emergenc
claim={claim} claim={claim}
epoch={epoch} epoch={epoch}
isClaimable={claim.expiry > epoch.number} isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol} stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol} ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice} tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
@ -162,6 +170,7 @@ const warmupTooltip = `Your claim earns rebases during warm-up. You can emergenc
claim={claim} claim={claim}
epoch={epoch} epoch={epoch}
isClaimable={claim.expiry > epoch.number} isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol} stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol} ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice} tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
@ -183,7 +192,8 @@ const ClaimInfo = ({
isPayoutGhst, isPayoutGhst,
stnkSymbol, stnkSymbol,
ghstSymbol, ghstSymbol,
tokenPrice tokenPrice,
isLegacy,
}) => { }) => {
return ( return (
<TableBody> <TableBody>
@ -211,6 +221,7 @@ const ClaimInfo = ({
<ActionButtons <ActionButtons
setConfirmationModalOpen={setConfirmationModalOpen} setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable} isClaimable={isClaimable}
isLegacy={isLegacy}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -228,6 +239,7 @@ const MobileClaimInfo = ({
ghstSymbol, ghstSymbol,
stnkSymbol, stnkSymbol,
tokenPrice, tokenPrice,
isLegacy,
}) => { }) => {
return ( return (
<Box mt="10px"> <Box mt="10px">
@ -257,12 +269,13 @@ const MobileClaimInfo = ({
isSmallScreen={true} isSmallScreen={true}
setConfirmationModalOpen={setConfirmationModalOpen} setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable} isClaimable={isClaimable}
isLegacy={isLegacy}
/> />
</Box> </Box>
); );
}; };
const ActionButtons = ({ setConfirmationModalOpen, isSmallScreen = false, isClaimable = false }) => { const ActionButtons = ({ setConfirmationModalOpen, isLegacy, isSmallScreen = false, isClaimable = false }) => {
return ( return (
<Box <Box
display="flex" display="flex"
@ -283,8 +296,8 @@ const ActionButtons = ({ setConfirmationModalOpen, isSmallScreen = false, isClai
fullWidth={isSmallScreen} fullWidth={isSmallScreen}
loading={false} loading={false}
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
onClick={() => setConfirmationModalOpen(true)} onClick={() => setConfirmationModalOpen(false)}
disabled={isClaimable} disabled={isLegacy && isClaimable}
> >
Claim Claim
</PrimaryButton> </PrimaryButton>

View File

@ -242,7 +242,7 @@ export const executeOnChainTransaction = async ({
}); });
const finalRequest = sanitizeTransactionRequest(request, isLegacy); const finalRequest = sanitizeTransactionRequest(request, isLegacy);
const txHash = await writeContract(config, finalRequest); const txHash = await writeContract(config, { ...finalRequest });
await waitForTransactionReceipt(config, { await waitForTransactionReceipt(config, {
hash: txHash, hash: txHash,
onReplaced: () => toast(messages.replacedMsg), onReplaced: () => toast(messages.replacedMsg),

View File

@ -226,7 +226,7 @@ export const breakout = async (chainId, account, receiver, amount) => {
const args = [receiver, amount]; const args = [receiver, amount];
const messages = { const messages = {
replacedMsg: "Breakout transaction was replaced. Wait for inclusion please.", replacedMsg: "Breakout transaction was replaced. Wait for inclusion please.",
successMsg: `Staking breakout succesfully bridged to ${shorten(receiver)}.`, successMsg: `Staking breakout succesfully done. Check tx hash status or wait for slow clap finalization.`,
errorMsg: "Breakout transaction failed. Check logs for error detalization.", errorMsg: "Breakout transaction failed. Check logs for error detalization.",
}; };
const txHash = await executeOnChainTransaction({ const txHash = await executeOnChainTransaction({