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",
"private": true,
"version": "0.7.36",
"version": "0.7.37",
"type": "module",
"scripts": {
"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 { 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");
@ -78,6 +80,18 @@ const BreakoutModal = ({ chainId, address }) => {
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={
@ -113,6 +127,9 @@ const BreakoutModal = ({ chainId, address }) => {
estimatedAmount={estimatedAmount}
incomingFee={incomingFee}
goNext={() => setStep(2)}
convertedReceiver={convertedReceiver}
setConvertedReceiver={setConvertedReceiver}
/>
: <ConfirmStep
chainId={chainId}
@ -127,6 +144,7 @@ const BreakoutModal = ({ chainId, address }) => {
setActiveTxIndex={setActiveTxIndex}
closeModal={closeModalPure}
evmNetwork={evmNetwork}
convertedReceiver={convertedReceiver}
/>
}
</Box>
@ -138,6 +156,8 @@ const BridgeView = ({
chainId,
receiver,
setReceiver,
convertedReceiver,
setConvertedReceiver,
bridgeNumbers,
ghstSymbol,
estimatedAmount,
@ -145,7 +165,6 @@ const BridgeView = ({
incomingFee
}) => {
const theme = useTheme();
const [convertedReceiver, setConvertedReceiver] = useState(undefined);
const config = useConfig();
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
@ -155,18 +174,6 @@ const BridgeView = ({
return client?.chain?.blockExplorers?.default?.url;
}, [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 (
<>
<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>
</Box>
{warmupPeriod <= 0 && <SecondaryButton
<SecondaryButton
onClick={() => callDefaultFunction()}
disabled={isPending || warmupPeriod > 0}
loading={isPending}
fullWidth
>
{`${isPending ? "Claiming..." : "Claim"} ${isStakingOpened ? "(3, 3) Stake" : "(1, 1) Bond"}`}
</SecondaryButton>}
{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%" }} />
@ -322,6 +332,7 @@ const ConfirmStep = ({
chainId,
address,
receiver,
convertedReceiver,
executableFunction,
ghstSymbol,
bridgeNumbers,
@ -360,7 +371,7 @@ const ConfirmStep = ({
const execute = useCallback(async () => {
setIsPending(true);
try {
const txHash = await executableFunction()(receiver);
const txHash = await executableFunction()(convertedReceiver);
if (txHash) {
const expectedSessionIndex = (currentSession ?? 0) + (evmNetwork
? Number((evmNetwork.avg_block_speed * evmNetwork.finality_delay) / (1000n * 14400n))
@ -369,10 +380,7 @@ const ConfirmStep = ({
const transaction = {
sessionIndex: expectedSessionIndex,
transactionHash: txHash,
receiverAddress: receiver,
amount: estimatedAmount._value.toString(),
chainId: chainId,
blockNumber: Number(blockNumber),
bridgeStability: 69, // TODO: avoid stability
timestamp: Date.now()
}
@ -388,7 +396,17 @@ const ConfirmStep = ({
setIsPending(false);
closeModal();
}
}, [executableFunction, receiver, networkName, chainId, address, blockNumber, evmNetwork, currentSession]);
}, [
executableFunction,
convertedReceiver,
receiver,
networkName,
chainId,
address,
blockNumber,
evmNetwork,
currentSession,
]);
return (
<>

View File

@ -1,5 +1,6 @@
import { useEffect, useState, useMemo, useCallback } from "react";
import ReactGA from "react-ga4";
import useSWR from "swr";
import {
Box,
@ -11,9 +12,9 @@ import {
} from "@mui/material";
import { decodeAddress } from "@polkadot/util-crypto";
import { fromHex } from "@polkadot-api/utils";
import { getBlockNumber } from "@wagmi/core";
import { getBlockNumber, getTransaction } 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 +24,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";
@ -100,12 +102,25 @@ const Bridge = ({ chainId, address, config, connect }) => {
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(() => {
if (!watchTransaction) return undefined
if (!watchTransaction || !watchTransactionArgs.receiver) {
return undefined;
}
const networkIdEncoded = u64.enc(BigInt(chainId));
const amountEncoded = u128.enc(BigInt(watchTransaction.amount));
const addressEncoded = decodeAddress(watchTransaction.receiverAddress, false, 1996);
const amountEncoded = u128.enc(BigInt(watchTransactionArgs.amount));
const addressEncoded = decodeAddress(watchTransactionArgs.receiver, false, 1996);
const transactionHashEncoded = fromHex(watchTransaction.transactionHash);
const blockNumber = u64.enc(watchTransactionInfo?.blockNumber ?? 0n);
@ -117,7 +132,7 @@ const Bridge = ({ chainId, address, config, connect }) => {
...networkIdEncoded
]);
return keccak256(clapArgsStr)
}, [watchTransaction, watchTransactionInfo])
}, [watchTransaction, watchTransactionInfo, watchTransactionArgs])
const latestBlockNumber = useLatestBlockNumber();
const eraIndex = useEraIndex();
@ -183,7 +198,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);
@ -191,6 +207,8 @@ const Bridge = ({ chainId, address, config, connect }) => {
return {
...watchTransaction,
receiverAddress: watchTransactionArgs.receiver,
amount: watchTransactionArgs.amount,
finalization,
applaused,
numberOfClaps,
@ -199,18 +217,49 @@ const Bridge = ({ chainId, address, config, connect }) => {
clapsPercentage,
}
}, [
watchTransaction,
watchTransactionArgs,
watchTransactionInfo,
transactionApplaused,
finalityDelay,
watchTransaction,
blockNumber,
totalStakedAmount,
authorities
]);
const filteredStoredTransactions = useMemo(() => {
const filteredStoredTransactionInfos = useMemo(() => {
return storedTransactions.filter(obj => obj.chainId === 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(() => {
return validators?.map((validator, index) => {
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">
<Typography variant="caption">
{formatCurrency(
new DecimalBigNumber(BigInt(obj.amount), 18).toString(),
new DecimalBigNumber(obj.amount, 18).toString(),
isSemiSmallScreen ? 3 : 8,
ghstSymbol
)}

View File

@ -76,9 +76,16 @@ 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) || claim.expiry > epoch.number) {
setConfirmationModalOpenInner(value);
if (isNetworkLegacy(chainId) || value) {
setConfirmationModalOpenInner(true);
} else {
const defaultFunction = async () => {
await claim(chainId, address, false, stnkSymbol, ghstSymbol);
@ -86,22 +93,22 @@ export const ClaimsArea = ({ chainId, address, epoch }) => {
}
const warmupLeft = claim.expiry - epoch.number;
const toExecute = async (receiver) => {
await breakout(chainId, address, receiver, claimableBalance);
await claimRefetch();
const txHash = await breakout(chainId, address, receiver, breakoutBalance);
return txHash;
}
breakoutFromStaking({ defaultFunction, toExecute, amount: claimableBalance, warmupLeft })
}
}, [claim, epoch, address, chainId, ghstSymbol]);
}, [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 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 (
<>
@ -141,6 +148,7 @@ const warmupTooltip = `Your claim earns rebases during warm-up. You can emergenc
claim={claim}
epoch={epoch}
isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
@ -162,6 +170,7 @@ const warmupTooltip = `Your claim earns rebases during warm-up. You can emergenc
claim={claim}
epoch={epoch}
isClaimable={claim.expiry > epoch.number}
isLegacy={isNetworkLegacy(chainId)}
stnkSymbol={stnkSymbol}
ghstSymbol={ghstSymbol}
tokenPrice={isPayoutGhst ? ghstPrice : stnkPrice}
@ -183,7 +192,8 @@ const ClaimInfo = ({
isPayoutGhst,
stnkSymbol,
ghstSymbol,
tokenPrice
tokenPrice,
isLegacy,
}) => {
return (
<TableBody>
@ -211,6 +221,7 @@ const ClaimInfo = ({
<ActionButtons
setConfirmationModalOpen={setConfirmationModalOpen}
isClaimable={isClaimable}
isLegacy={isLegacy}
/>
</TableCell>
</TableRow>
@ -228,6 +239,7 @@ const MobileClaimInfo = ({
ghstSymbol,
stnkSymbol,
tokenPrice,
isLegacy,
}) => {
return (
<Box mt="10px">
@ -257,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"
@ -283,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

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

View File

@ -226,7 +226,7 @@ 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 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.",
};
const txHash = await executeOnChainTransaction({