ghost-dao-interface/src/containers/History/BridgeInModal.jsx
Uncle Fatso 132bd0986b
migrate to PAPI and integrate weaver protocol
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-11-05 21:59:19 +03:00

532 lines
20 KiB
JavaScript

import useSWR from 'swr';
import { useMemo, useState } from "react";
import { useTransactionReceipt, useBlockNumber, useTransaction } from 'wagmi';
import { decodeEventLog, decodeFunctionData } from 'viem';
import { Box, Typography, Link, Skeleton, useTheme } from "@mui/material";
import CheckIcon from '@mui/icons-material/Check';
import SyncIcon from '@mui/icons-material/Sync';
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import HourglassBottomIcon from '@mui/icons-material/HourglassBottom';
import PendingIcon from '@mui/icons-material/Pending';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import Modal from "../../components/Modal/Modal";
import DataRow from "../../components/DataRow/DataRow";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { PrimaryButton } from "../../components/Button";
import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
import { useWeaverState } from "../../hooks/staking"
import { useUnstableProvider } from "../../hooks/ghost";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { shorten, formatNumber, formatCurrency } from "../../helpers";
const BridgeInModal = ({ chainId, isOpen, close, config, historyRecord }) => {
if (historyRecord === null) return null;
const [copiedIndex, setCopiedIndex] = useState(null);
const { casperApi, client, account } = useUnstableProvider();
const { data: blockNumber } = useBlockNumber({ watch: true });
const { data: txData } = useTransaction({
chainId,
hash: historyRecord.txHash,
});
const functionData = useMemo(() => {
if (!txData) return null;
return decodeFunctionData({ abi: GatekeeperAbi, data: txData.input })
}, [txData]);
const copyToClipboard = (text, index) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null) , 800);
});
};
const { data: chainProperties } = useSWR(
client ? ['chainProperties', client] : null,
() => client.getChainSpecData(),
{
revalidateOnFocus: true
}
);
const { data: networkData } = useSWR(
casperApi ? ['networkData', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostNetworks.Networks.getValue(key);
return res;
},
{
revalidateOnFocus: true
}
);
const chainInfo = useMemo(() => {
const client = config?.getClient();
return {
url: client?.chain?.blockExplorers?.default?.url,
symbol: client?.chain?.nativeCurrency?.symbol,
};
}, [config]);
const { data: receipt, isLoading, error } = useTransactionReceipt({
hash: historyRecord?.txHash,
query: {
enabled: !!historyRecord?.txHash && isOpen,
}
});
const allDecodedLogs = useMemo(() => {
if (!receipt?.logs) return [];
return receipt.logs.map((log) => {
try {
const decoded = decodeEventLog({
abi: GatekeeperAbi,
data: log.data,
topics: log.topics,
});
return {
eventName: decoded.eventName,
args: decoded.args,
address: log.address
};
} catch (e) {
return null;
}
}).filter(Boolean);
}, [receipt]);
const bridgeInInfo = useMemo(() => {
return allDecodedLogs.find(l => l.eventName === 'ThreadInserted');
}, [allDecodedLogs]);
// TODO: revisit, we need other block to be used here
// TODO: how to verify the fact that merkle root is correct???
// const xxxx = useWeaverState(chainId, receipt.blockNumber, bridgeInInfo?.args);
// TODO: functionData could be here
const localString = new Date(historyRecord.submitted).toLocaleString();
const receiver = "sfKCC7CEfJLTgGopghSPB5EtNfLVUu34nMtiW3Taf1GuwVz3Z";
const rawAmount = 1000000000000000000n;
const bridgedAmount = new DecimalBigNumber(
rawAmount,
chainProperties?.properties.tokenDecimals ?? 18
);
const estimatedFee = useMemo(() => {
if (!networkData) return null
return networkData.incoming_share / 10000000;
}, [networkData]);
const estimatedReceived = useMemo(() => {
const hundredPercent = 1000000000;
const incomingShare = networkData ? networkData.incoming_share : 0;
const result = hundredPercent - incomingShare;
const value = Number(rawAmount) * result / hundredPercent;
return new DecimalBigNumber(value, chainProperties?.properties.tokenDecimals ?? 18)
}, [networkData, chainProperties]);
const finalization = useMemo(() => {
if (!receipt || !blockNumber || !networkData) return null;
const finalityDelay = networkData.finality_delay;
return Math.max(0, Number(finalityDelay + receipt.blockNumber - blockNumber));
}, [receipt, blockNumber, networkData]);
// TODO: networkId, session,
// TODO: EvmThreadProof: slotIndex global % SLOTS, proofIndex % SLOTS, receiver sf, amount raw
// TODO: compute locally the arguments
// https://git.ghostchain.io/ghostchain/ghost-node/src/commit/cc7721e3899f9740e35f65f3feb4cdc621926166/pallets/weaver/src/types.rs#L400
return (
<Modal
maxWidth="420px"
minHeight="200px"
open={isOpen}
headerText={
<>
{bridgeInInfo && "Bridge-In"}
{!bridgeInInfo && "Parsing Transaction Hash"}
</>
}
onClose={() => close()}
>
<Box display="flex" flexDirection="column">
<Box width="100%" gap="5px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
<BridgeInProgress
blockNumber={blockNumber}
chainId={chainId}
casperApi={casperApi}
finalization={finalization}
/>
<Box
mt={2}
width="100%"
display="flex"
flexDirection="column"
sx={{
'& .data-row': {
margin: '0px !important',
},
'& .data-row .MuiTypography-root': {
fontSize: '13px !important',
}
}}
>
<DataRow
title="Submitted"
balance={<Typography variant="body1">{localString}</Typography>}
tooltip="some description to be added."
/>
<DataRow
title="Receiver [-]"
tooltip="some description to be added."
balance={
<Link
style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center" }}
onClick={() => copyToClipboard(receiver, 0)}
>
<Typography variant="body1">
{bridgeInInfo ? shorten(receiver, 11, -8) : <Skeleton width="120px" />}
</Typography>
<GhostStyledIcon
sx={{ marginLeft: "5px", width: "12px", height: "12px" }}
viewBox="0 0 25 25"
component={copiedIndex === 0 ? CheckIcon : ContentPasteIcon}
/>
</Link>
}
/>
<DataRow
title="Block Height"
tooltip="some description to be added."
balance={receipt
? <Typography variant="body1">{formatNumber(receipt.blockNumber)}</Typography>
: <Skeleton width="120px" />
}
/>
<DataRow
title="Transaction Hash [+]"
tooltip="some description to be added."
balance={historyRecord
? <Link href={`${chainInfo.url}/tx/${historyRecord.txHash}`} target="_blank" rel="noopener noreferrer">
<Typography variant="body1">
{bridgeInInfo ? shorten(historyRecord.txHash, 11, -8) : <Skeleton width="120px" />}
</Typography>
</Link>
: <Skeleton width="120px" />
}
/>
</Box>
<hr style={{ width: "100%" }} />
<Box
mb="10px"
width="100%"
display="flex"
flexDirection="column"
sx={{
'& .data-row': {
margin: '0px !important',
},
'& .data-row .MuiTypography-root': {
fontSize: '13px !important',
}
}}
>
<DataRow
title="Gross Bridged [-]"
tooltip="some description to be added."
balance={<Typography variant="body1">{formatCurrency(bridgedAmount, 5, chainProperties?.properties.tokenSymbol ?? "")}</Typography>}
/>
<DataRow
title="GHOST Fee (Est.)"
tooltip="some description to be added."
balance={<Typography variant="body1">{formatNumber(estimatedFee, 2)}%</Typography>}
/>
<DataRow
title="Net Received"
tooltip="some description to be added."
balance={<Typography variant="body1">{formatCurrency(estimatedReceived.toString(), 5, chainProperties?.properties.tokenSymbol ?? "")}</Typography>}
/>
</Box>
<PrimaryButton fullWidth>Claim Bridge-In</PrimaryButton>
</Box>
</Box>
</Modal>
)
}
const BridgeInProgress = ({ chainId, casperApi, finalization, blockNumber }) => {
const theme = useTheme();
const { data: weaverAuthorities } = useSWR(
casperApi ? ['weaverAuthorities', casperApi] : null,
() => casperApi.query.GhostWeaver.Authorities.getValue(),
{
revalidateOnFocus: true
}
);
const { data: tapestryDraft } = useSWR(
casperApi ? ['tapestryDraft', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.TapestryDrafts.getValue(key);
return res;
},
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
const { data: tapestryStrand } = useSWR(
casperApi ? ['tapestryStrand', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.TapestryStrands.getValue(key);
return res;
},
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
const { data: loomState } = useSWR(
casperApi ? ['loomState', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.LoomStates.getValue(key, 0n);
return res;
},
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
const rootIncluded = useMemo(() => {
if (!loomState || !blockNumber) return false;
if (blockNumber >= loomState.at_block) return true;
return false;
});
return (
<Box p="10px 0" width="100%" display="flex" justifyContent="space-between" alignItems="center">
{finalization === null || finalization > 0
? <BlockFinalization theme={theme} finalization={finalization} />
: <SyncTarget
theme={theme}
rootIncluded={rootIncluded}
blockNumber={blockNumber}
loomState={loomState}
tapestryDraft={tapestryDraft}
/>
}
<RootVerification
theme={theme}
rootIncluded={rootIncluded}
tapestryDraft={tapestryDraft}
tapestryStrand={tapestryStrand}
totalAuthorities={weaverAuthorities?.length ?? 0}
/>
<ProofSubmission theme={theme} rootIncluded={rootIncluded} />
</Box>
)
}
const ProofSubmission = ({ theme, claimed, rootIncluded }) => {
return (
<Box
sx={{
transition: "all 0.8s ease",
transform: (rootIncluded && !claimed) && "scale(1.2)",
color: claimed && theme.colors.primary[300]
}}
maxWidth="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
transformOrigin: "center",
animation: rootIncluded && 'waitingForClaim 4s ease-in-out infinite',
'@keyframes waitingForClaim': {
'0%, 100%': { opacity: 0.4 },
'50%': { opacity: 1 },
},
}}
viewBox="0 0 25 25"
component={claimed ? CheckCircleIcon : PendingIcon}
/>
<Typography variant="caption">Bridge Status</Typography>
<Typography variant="caption">
{rootIncluded
? claimed ? "Success" : "Claim Ready"
: "Pending..."
}
</Typography>
</Box>
)
}
const RootVerification = ({ theme, tapestryDraft, rootIncluded, totalAuthorities, tapestryStrand }) => {
const rootVerificationText = useMemo(() => {
if (rootIncluded) return "Synced";
if (!tapestryStrand) return "Pending...";
let maxOnesCount = 0;
tapestryStrand.forEach(item => {
const dataArray = item[1]?.data || [];
const totalOnes = dataArray.reduce((sum, num) => {
const onesInNumber = num.toString(2).split('1').length - 1;
return sum + onesInNumber;
}, 0);
if (totalOnes > maxOnesCount) {
maxOnesCount = totalOnes;
}
});
return `${maxOnesCount} / ${totalAuthorities}`;
}, [rootIncluded, tapestryStrand, totalAuthorities]);
return (
<Box
sx={{
transition: "all 0.8s ease",
transform: !rootIncluded && tapestryDraft && "scale(1.2)",
color: (rootIncluded || tapestryDraft) && theme.colors.primary[300]
}}
maxWidth="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
transformOrigin: "center",
animation: !rootIncluded && tapestryDraft && 'scanFingerPrint 4s ease-in-out infinite',
'@keyframes scanFingerPrint': {
'0%, 100%': { opacity: 0.4 },
'50%': { opacity: 1 },
},
}}
viewBox="0 0 25 25"
component={FingerprintIcon}
/>
<Typography variant="caption">Root Verification</Typography>
<Typography variant="caption">{rootVerificationText}</Typography>
</Box>
)
}
const SyncTarget = ({ tapestryDraft, loomState, rootIncluded, blockNumber, theme }) => {
const syncTargetText = useMemo(() => {
if (rootIncluded) return "Synced";
if (!tapestryDraft || !loomState) return "Pending...";
if (blockNumber >= loomState.at_block) return "Synced"
return `${formatNumber(Number(blockNumber - loomState.at_block))} block behind`;
}, [rootIncluded, tapestryDraft, loomState, blockNumber]);
return (
<Box
sx={{
transition: "all 0.8s ease",
transform: !rootIncluded && !tapestryDraft && "scale(1.2)",
color: (rootIncluded || !tapestryDraft) && theme.colors.primary[300]
}}
maxWidth="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
transformOrigin: "center",
animation: !rootIncluded && !tapestryDraft && 'rotateSyncTarget 2s ease-in-out infinite',
'@keyframes rotateSyncTarget': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(-360deg)' },
},
}}
viewBox="0 0 25 25"
component={SyncIcon}
/>
<Typography variant="caption">Sync Target</Typography>
<Typography variant="caption">{syncTargetText}</Typography>
</Box>
)
}
const BlockFinalization = ({ finalization, theme }) => {
return (
<Box
sx={{
transition: "all 0.8s ease",
transform: finalization && finalization > 0 && "scale(1.2)",
color: finalization === 0 && theme.colors.primary[300]
}}
maxWidth="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
animation: finalization && finalization > 0 && 'rotateHourGlass 2s ease-in-out infinite',
'@keyframes rotateHourGlass': {
'0%': { transform: 'rotate(0deg)' },
'15%': { transform: 'rotate(0deg)' },
'85%': { transform: 'rotate(180deg)' },
'100%': { transform: 'rotate(180deg)' },
},
}}
viewBox="0 0 25 25"
component={HourglassBottomIcon}
/>
<Typography variant="caption">Finalization</Typography>
<Typography variant="caption">
{(finalization ?? 0).toString()} blocks left
</Typography>
</Box>
)
}
export default BridgeInModal;