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 ( {bridgeInInfo && "Bridge-In"} {!bridgeInInfo && "Parsing Transaction Hash"} } onClose={() => close()} > {localString}} tooltip="some description to be added." /> copyToClipboard(receiver, 0)} > {bridgeInInfo ? shorten(receiver, 11, -8) : } } /> {formatNumber(receipt.blockNumber)} : } /> {bridgeInInfo ? shorten(historyRecord.txHash, 11, -8) : } : } />
{formatCurrency(bridgedAmount, 5, chainProperties?.properties.tokenSymbol ?? "")}} /> {formatNumber(estimatedFee, 2)}%} /> {formatCurrency(estimatedReceived.toString(), 5, chainProperties?.properties.tokenSymbol ?? "")}} /> Claim Bridge-In
) } 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 ( {finalization === null || finalization > 0 ? : } ) } const ProofSubmission = ({ theme, claimed, rootIncluded }) => { return ( Bridge Status {rootIncluded ? claimed ? "Success" : "Claim Ready" : "Pending..." } ) } 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 ( Root Verification {rootVerificationText} ) } 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 ( Sync Target {syncTargetText} ) } const BlockFinalization = ({ finalization, theme }) => { return ( 0 && "scale(1.2)", color: finalization === 0 && theme.colors.primary[300] }} maxWidth="120px" display="flex" flexDirection="column" justifyContent="start" alignItems="center" > 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} /> Finalization {(finalization ?? 0).toString()} blocks left ) } export default BridgeInModal;