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;