532 lines
20 KiB
JavaScript
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;
|