migrate to PAPI and integrate weaver protocol

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-11-05 21:59:19 +03:00
parent 314abe1aba
commit 132bd0986b
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
30 changed files with 2877 additions and 337 deletions

3
.papi/descriptors/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!.gitignore
!package.json

View File

@ -0,0 +1,25 @@
{
"version": "0.1.0-autogenerated.3461409763779961730",
"name": "@polkadot-api/descriptors",
"files": [
"dist"
],
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module": "./dist/index.js",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"browser": "./dist/index.js",
"types": "./dist/index.d.ts",
"sideEffects": false,
"peerDependencies": {
"polkadot-api": ">=2.0.0"
}
}

9
.papi/polkadot-api.json Normal file
View File

@ -0,0 +1,9 @@
{
"version": 0,
"descriptorPath": ".papi\\descriptors",
"entries": {
"casper": {
"metadata": "metadatas/casper_runtime.scale"
}
}
}

Binary file not shown.

View File

@ -30,6 +30,8 @@
"@tanstack/react-query": "^5.67.2",
"@tanstack/react-query-devtools": "^5.67.2",
"@wagmi/core": "^2.17.3",
"polkadot-api": "^3.3.0",
"@polkadot-api/descriptors": "file:./.papi/descriptors",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-ga4": "^2.1.0",
@ -56,5 +58,10 @@
"vite": "^6.2.0",
"vite-plugin-svgr": "^4.3.0"
},
"pnpm": {
"overrides": {
"@polkadot-api/json-rpc-provider": "0.2.0"
}
},
"packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
}

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,7 @@ const BondModalContainer = lazy(() => import("./containers/Bond/BondModal"));
const StakeContainer = lazy(() => import("./containers/Stake/StakeContainer"));
const TreasuryDashboard = lazy(() => import("./containers/TreasuryDashboard/TreasuryDashboard"));
const Dex = lazy(() => import("./containers/Dex/Dex"));
const Bridge = lazy(() => import("./containers/Bridge/Bridge"));
const BridgeIn = lazy(() => import("./containers/Bridge/BridgeIn"));
const NotFound = lazy(() => import("./containers/NotFound/NotFound"));
const Governance = lazy(() => import("./containers/Governance/Governance"));
const ProposalDetails = lazy(() => import("./containers/Governance/ProposalDetails"));
@ -183,9 +183,9 @@ function App() {
/>
<nav className={classes.drawer}>
{isSmallerScreen ? (
<NavDrawer chainId={chainId} addressChainId={addressChainId} mobileOpen={mobileOpen} handleDrawerToggle={handleDrawerToggle} />
<NavDrawer address={address} chainId={chainId} addressChainId={addressChainId} mobileOpen={mobileOpen} handleDrawerToggle={handleDrawerToggle} />
) : (
<Sidebar chainId={chainId} addressChainId={addressChainId} />
<Sidebar address={address} chainId={chainId} addressChainId={addressChainId} />
)}
</nav>
@ -200,7 +200,7 @@ function App() {
<Route path="bonds" element={<Bonds connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bonds/:id" element={<BondModalContainer config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="stake" element={<StakeContainer config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bridge" element={<Bridge config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="bridge-in" element={<BridgeIn config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="dex/:name" element={<Dex config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="governance" element={<Governance config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
<Route path="governance/:id" element={<ProposalDetails config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />

File diff suppressed because one or more lines are too long

View File

@ -150,7 +150,8 @@ const NavItem = ({
component={ArrowUpIcon}
/>}
</Box>
{chip && <Chip size="small" label={chip} template={chipColor} />}
{chip && <Box pr="6px"><Chip p="18px" size="small" label={chip} template={chipColor} /></Box>}
</Box>
</Link>
);

View File

@ -56,6 +56,7 @@ import WifiProtectedSetupIcon from '@mui/icons-material/WifiProtectedSetup';
import { useTokenSymbol } from "../../hooks/tokens";
import { useLiveBonds } from "../../hooks/bonds/index";
import { useLocalStorage } from "../../hooks/localstorage";
import pckg from "../../../package.json"
const PREFIX = "NavContent";
@ -70,16 +71,26 @@ const StyledBox = styled(Box)(({ theme }) => ({
},
}));
const NavContent = ({ chainId, addressChainId }) => {
const NavContent = ({ chainId, addressChainId, address }) => {
const { chains } = useSwitchChain();
const chainName = useMemo(() => {
return chains.find(chain => chain.id === chainId).name.toLowerCase();
}, [chains, chainId, addressChainId])
}, [chains, chainId, addressChainId]);
const { useStorageValue, CONSTANTS } = useLocalStorage();
const { liveBonds: ghostBonds } = useLiveBonds(chainId);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { data: pendingHistory } = useStorageValue(chainId, address, CONSTANTS.pendingHistoryPrefix, {})
const pendingTransactions = useMemo(() => {
if (!pendingHistory) return null;
const count = Object.values(pendingHistory).reduce((sum, count) => sum + count, 0);
return count > 0 ? count: null;
}, [pendingHistory]);
const sortedGhostBonds = useMemo(() => sortBondsByDiscount(ghostBonds), [ghostBonds]);
const bridgeNumbers = useMemo(() => {
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
@ -179,8 +190,10 @@ const NavContent = ({ chainId, addressChainId }) => {
</AccordionDetails>
}
/>
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
<NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Bridge-In`} to={`/${chainName}/bridge-in`} />
{/* <NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} /> */}
<NavItem icon={HistoryIcon} chipColor="warning" chip={pendingTransactions} label="Transaction History" to={`/${chainName}/history`} />
<Box className="menu-divider">
<Divider />
</Box>
@ -192,7 +205,6 @@ const NavContent = ({ chainId, addressChainId }) => {
<Box>
<NavItem href="https://ghostchain.io/game-theory-3-3" icon={CasinoIcon} label={`${bridgeNumbers} Game Theory`} />
<NavItem icon={HistoryIcon} label={`History`} to={`/${chainName}/history`} />
<NavItem href="https://docs.ghostchain.io/" icon={BookIcon} label={`Documentation`} />
<StyledBox display="flex" justifyContent="space-around" paddingY="24px">
<Link href="https://git.ghostchain.io/ghostchain/ghost-dao-contracts" target="_blank" rel="noopener noreferrer">

View File

@ -27,7 +27,7 @@ const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
const drawerWidth = 280;
const NavDrawer = ({ chainId, addressChainId, mobileOpen, handleDrawerToggle }) => {
const NavDrawer = ({ chainId, address, addressChainId, mobileOpen, handleDrawerToggle }) => {
const location = useLocation();
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
@ -53,7 +53,7 @@ const NavDrawer = ({ chainId, addressChainId, mobileOpen, handleDrawerToggle })
disableBackdropTransition={!isIOS}
disableDiscovery={isIOS}
>
<NavContent addressChainId={addressChainId} chainId={chainId} />
<NavContent address={address} addressChainId={addressChainId} chainId={chainId} />
</StyledSwipeableDrawer>
);
};

View File

@ -4,10 +4,10 @@ import { Drawer } from "@mui/material";
import React from "react";
import NavContent from "./NavContent";
const Sidebar = ({ chainId, addressChainId }) => (
const Sidebar = ({ chainId, addressChainId, address }) => (
<div className="sidebar" id="sidebarContent">
<Drawer variant="permanent" anchor="left">
<NavContent addressChainId={addressChainId} chainId={chainId} />
<NavContent address={address} addressChainId={addressChainId} chainId={chainId} />
</Drawer>
</div>
);

View File

@ -1,3 +1,4 @@
import useSWR from 'swr';
import { useMemo } from "react";
import {
Box,
@ -25,7 +26,7 @@ import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
import { formatCurrency, shorten } from "../../../helpers";
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import { RESERVE_ADDRESSES, FTSO_ADDRESSES } from "../../../constants/addresses";
import { useUnstableProvider, useSystemAccount, useChainSpec } from "../../../hooks/ghost"
import { useUnstableProvider } from "../../../hooks/ghost"
import { useAccount, useDisconnect, useSwitchChain } from "wagmi";
@ -113,28 +114,44 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
const location = useLocation();
const tokens = useWallet(chainId, address);
const [currency, setCurrency] = useState("USD");
const { account, isExtensionMissing } = useUnstableProvider();
const { client, casperApi, account, isExtensionMissing } = useUnstableProvider();
const { chains } = useSwitchChain();
const [currency, setCurrency] = useState("USD");
const [accountIndex, setAccountIndex] = useState(0);
const chainName = useMemo(() => {
const chain = chains.find(chain => chain.id === chainId)
if (!chain) return null
return chain.name.toLowerCase()
}, [chains, chainId])
}, [chains, chainId]);
const chainSpec = useChainSpec();
const accountBalanceRaw = useSystemAccount({ account });
const { data: chainProperties } = useSWR(
client ? ['chainProperties', client] : null,
() => client.getChainSpecData(),
{
revalidateOnFocus: false
}
);
const { data: accountBalanceRaw } = useSWR(
client ? ['accountBalance', client] : null,
() => casperApi.query.System.Account.getValue(account),
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
const accountBalance = useMemo(() => {
const decimals = chainSpec ? chainSpec.properties.tokenDecimals : 18;
const decimals = chainProperties ? chainProperties.properties.tokenDecimals : 18;
if (!accountBalanceRaw) return new DecimalBigNumber(0n, decimals);
const balance = accountBalanceRaw.data.free
+ accountBalanceRaw.data.frozen
+ accountBalanceRaw.data.reserved;
return new DecimalBigNumber(balance, decimals);
}, [accountBalanceRaw, chainSpec])
}, [accountBalanceRaw, chainProperties]);
const nativeGhst = useMemo(() => {
return {
@ -201,7 +218,7 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
<WalletTotalValue
currency={currency}
setCurrency={setCurrency}
chainName={chainSpec?.chainName ?? ""}
chainName={chainProperties?.name ?? ""}
address={account ?? "...wait"}
tokens={nativeGhst}
/>

View File

@ -75,6 +75,7 @@ const BreakoutModal = ({ chainId, address }) => {
if (isClaimBondOpened && warmupPeriod > 0) return "Bond in Warm-up"
}, [isStakingOpened, isClaimBondOpened, warmupPeriod]);
// TODO: make it global helpers
const bridgeNumbers = useMemo(() => {
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
const number = 1 + connectedNetworks * 3;

View File

@ -0,0 +1,50 @@
import { useState, useEffect } from "react";
import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
import CheckIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import SwapCard from "../../components/Swap/SwapCard";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { shorten } from "../../helpers";
export const BridgeGhostAddressCard = ({ receiver, setReceiver }) => {
const [inputFocused, setInputFocused] = useState(false);
const [innerReceiver, setInnerReceiver] = useState(receiver);
useEffect(() => {
try {
const [publicKey, prefix] = ss58Decode(innerReceiver);
if (prefix !== 1995 && prefix !== 1996) {
throw new Error("bad prefix");
}
setReceiver(innerReceiver);
} catch (err) {
setReceiver(undefined)
}
}, [innerReceiver]);
return (
<SwapCard
id={`bridge-token-receiver-card`}
inputWidth={"100%"}
onBlur={() => setInputFocused(true)}
onFocus={() => setInputFocused(false)}
onMouseLeave={() => setInputFocused(true)}
onMouseEnter={() => setInputFocused(false)}
onChange={event => setInnerReceiver(event.currentTarget.value)}
placeholder={receiver ? shorten(receiver, 15, -10) : "GHOST address (sf prefixed)"}
value={receiver ? "" : innerReceiver}
inputProps={{ "data-testid": "fromInput" }}
endString={receiver
? <GhostStyledIcon color="success" viewBox="0 0 25 25" component={CheckIcon} />
: inputFocused && !receiver
? <GhostStyledIcon color="error" viewBox="0 0 25 25" component={CancelIcon} />
: undefined
}
type="text"
maxWidth="100%"
/>
)
}

View File

@ -0,0 +1,461 @@
import { useEffect, useState, useMemo, useCallback } from "react";
import { useLocation } from "react-router-dom";
import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
import { toHex } from "@polkadot-api/utils";
import ReactGA from "react-ga4";
import useSWR from 'swr';
import {
Box,
Container,
Typography,
Skeleton,
useMediaQuery,
useTheme,
} from "@mui/material";
import SettingsIcon from '@mui/icons-material/Settings';
import Paper from "../../components/Paper/Paper";
import Metric from "../../components/Metric/Metric";
import PageTitle from "../../components/PageTitle/PageTitle";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import MetricCollection from "../../components/Metric/MetricCollection";
import LinearProgressBar from "../../components/Progress/LinearProgressBar";
import SwapCard from "../../components/Swap/SwapCard";
import SwapCollection from "../../components/Swap/SwapCollection";
import TokenStack from "../../components/TokenStack/TokenStack";
import DataRow from "../../components/DataRow/DataRow";
import { PrimaryButton } from "../../components/Button";
import { BridgeInSettingsModal } from "./BridgeSettingsModal";
import {
ghost,
useGatekeeperApy,
useGatekeeperAddress,
useGatekeeperMinimum,
} from "../../hooks/staking";
import { useTokenSymbol, useBalance } from "../../hooks/tokens";
import { useUnstableProvider } from "../../hooks/ghost";
import { useLocalStorage } from "../../hooks/localstorage";
import { prettifySeconds } from "../../helpers/timeUtil";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { formatNumber, formatCurrency } from "../../helpers";
import { GATEKEEPER_ADDRESSES } from "../../constants/addresses";
import { networkAvgBlockSpeed } from "../../constants";
const DECIMALS_PREFIX = "bridge-in-decimals";
const BridgeStatusCard = ({ totalValidators, pendingValidators, tapestryDraft }) => {
const theme = useTheme();
const progressPercent = totalValidators > 0
? (pendingValidators / totalValidators) * 100
: 0;
return (
<Box mt="30px" sx={{ mb: { xs: "0px", md: "-30px" }}}>
<Box display="flex" justifyContent="space-between" alignItems="center" mt="2px">
<Typography variant="caption" fontWeight="bold">
{tapestryDraft ? "Round 2: Merkle Root Verification" : "Round 1: Block Consensus"}
</Typography>
<Typography variant="caption">
{tapestryDraft
? `Block: ${formatNumber(tapestryDraft.median)} (${pendingValidators} out of ${totalValidators} validators)`
: `Blocks Pending... (${pendingValidators} out of ${totalValidators} validators)`
}
</Typography>
</Box>
<LinearProgressBar
variant="determinate"
value={progressPercent}
height={6}
barColor={tapestryDraft
? theme.colors.feedback.success
: theme.colors.feedback.error
}
/>
</Box>
);
};
const BridgeIn = ({ chainId, address, config, connect }) => {
const location = useLocation();
const { useStorageValue, setStorageValue, tryStoreInHistory, CONSTANTS } = useLocalStorage();
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const { casperApi, client, account } = useUnstableProvider();
const { gatekeepedApy } = useGatekeeperApy(chainId);
const [nextEraIn, setNextEraIn] = useState(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const [isPending, setIsPending] = useState(false);
const [amount, setAmount] = useState("");
const [receiver, setReceiver] = useState(account);
const [customReceiver, setCustomReceiver] = useState(account);
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { balance: ghstBalance, refetch: ghstBalanceRefetch } = useBalance(chainId, "GHST", address);
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
const { existentialDeposit } = useGatekeeperMinimum(chainId);
const { data: formatDecimals } = useStorageValue(chainId, address, DECIMALS_PREFIX, "5");
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location]);
const bridgeNumbers = useMemo(() => {
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
const number = 1 + connectedNetworks * 3;
return `(${number}, ${number})`;
}, [chainId]);
const { data: chainProperties } = useSWR(
client ? ['chainProperties', client] : null,
() => client.getChainSpecData(),
{
revalidateOnFocus: true
}
);
const { data: activeEraStart } = useSWR(
casperApi ? ['activeEra', casperApi] : null,
() => casperApi.query.Staking.ActiveEra.getValue(),
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
const { data: weaverAuthorities } = useSWR(
casperApi ? ['weaverAuthorities', casperApi] : null,
() => casperApi.query.GhostWeaver.Authorities.getValue(),
{
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 { data: totalStaked } = useSWR(
casperApi && activeEraStart && chainProperties ? ['erasTotalStake', casperApi, activeEraStart, chainProperties] : null,
async () => {
const decimals = chainProperties.properties.tokenDecimals;
const total = await casperApi.query.Staking.ErasTotalStake.getValue(activeEraStart.index);
return new DecimalBigNumber(total, decimals);
},
{
revalidateOnFocus: true
}
);
const { data: networkAttestations } = useSWR(
casperApi ? ['networkAttestations', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.NetworkAttestations.getValue(key);
return res;
},
{
refreshInterval: 12000,
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 tapestryParticipants = useMemo(() => {
if (!tapestryStrand) return { element: "", onesCount: 0 };
let maxOnesCount = 0;
let bestElement = null;
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;
bestElement = item[0];
}
});
return {
element: bestElement,
onesCount: maxOnesCount
};
}, [tapestryStrand]);
useEffect(() => {
if (!activeEraStart || !activeEraStart.start) return;
const calculateTime = () => {
const nowMs = Date.now();
const diffMs = nowMs - Number(activeEraStart.start);
return prettifySeconds(Math.max(0, diffMs / 1000));
};
setNextEraIn(calculateTime());
const intervalId = setInterval(() => {
setNextEraIn(calculateTime());
}, 1000);
return () => clearInterval(intervalId);
}, [activeEraStart]);
const blockDelayMetric = useMemo(() => {
if (!networkData) return null;
const blockSpeed = networkAvgBlockSpeed(chainId);
const finalityDelay = networkData.finality_delay;
const estimatedMins = (blockSpeed * finalityDelay) / 60n;
return { finalityDelay, text: `~${estimatedMins} mins` }
}, [chainId, networkData]);
const nativeSymbol = useMemo(() => {
const client = config?.getClient();
return client?.chain?.nativeCurrency?.symbol;
}, [config]);
const estimatedOutAmount = useMemo(() => {
const hundredPercent = 1000000000;
const incomingShare = networkData ? networkData.incoming_share : 0;
const result = hundredPercent - incomingShare;
return amount * result / hundredPercent
}, [amount, networkData]);
const gatekeepersDifferent = useMemo(() => {
if (!networkData) return false;
return gatekeeperAddress.toUpperCase() == toHex(networkData.gatekeeper).toUpperCase();
}, [gatekeeperAddress, networkData]);
const preparedAmount = useMemo(() => {
return new DecimalBigNumber(amount, ghstBalance._decimals);
}, [amount, ghstBalance]);
const receiverHex = useMemo(() => {
const finalReceiver = customReceiver ? customReceiver : receiver;
try {
const [publicKey, prefix] = ss58Decode(finalReceiver);
if (prefix !== 1995 && prefix !== 1996) return undefined;
return toHex(publicKey);
} catch {
return undefined;
}
}, [receiver, customReceiver]);
const actionDisabled = useMemo(() => {
if (!networkData) return true;
const differentGatekeepers = gatekeeperAddress.toUpperCase() !== toHex(networkData.gatekeeper).toUpperCase();
const smallAmount = existentialDeposit.gt(preparedAmount);
const insufficientAmount = preparedAmount.gt(ghstBalance);
return isPending || differentGatekeepers || smallAmount || insufficientAmount || !receiverHex
}, [isPending, networkData, gatekeeperAddress, existentialDeposit, preparedAmount, ghstBalance, receiverHex]);
const setFormatDecimals = useCallback((value) => {
if (Number(value) <= 17) {
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
}
}, [chainId, address]);
const startGhostBridge = async () => {
setIsPending(true);
try {
const txHash = await ghost(chainId, address, receiverHex, preparedAmount._value);
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.bridgeIn.key, txHash, CONSTANTS.bridgeIn.name);
} finally {
await ghstBalanceRefetch();
setAmount("");
setIsPending(false);
}
}
return (
<Box minHeight="calc(100vh - 43px)">
<PageTitle name="GHOST Bridge-In" subtitle={`${bridgeNumbers} Bridge-In ${ghstSymbol ? ghstSymbol : "CSPR"} into ${bridgeNumbers} Stake\u00B2`} />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
}}
>
<BridgeInSettingsModal
isOpen={settingsOpen && !isPending}
onClose={() => setSettingsOpen(false)}
decimals={formatDecimals}
setDecimals={setFormatDecimals}
receiver={customReceiver}
setReceiver={setCustomReceiver}
/>
<Box mt="15px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" gap="5px">
<Typography>Next Era in</Typography>
{nextEraIn
? <Typography>{nextEraIn}</Typography>
: <Skeleton width="120px" />
}
</Box>
}
>
<MetricCollection>
<Metric
metric={`${formatNumber(gatekeepedApy, 2)}%`}
label={`Stake\u00B2 APY`}
tooltip="some description to be added"
/>
<Metric
metric={`${formatNumber(totalStaked ? totalStaked.toString() : "0", 2)} ${chainProperties ? chainProperties.properties.tokenSymbol : "CSPR"}`}
isLoading={!totalStaked}
label="Bridge Security Stake"
tooltip="some description to be added"
/>
<Metric
label="Block Delay"
tooltip={`some description to be added ${blockDelayMetric ? blockDelayMetric.finalityDelay : "estimating..."}`}
isLoading={!blockDelayMetric}
metric={blockDelayMetric?.text}
/>
</MetricCollection>
<BridgeStatusCard
tapestryDraft={tapestryDraft}
totalValidators={weaverAuthorities ? weaverAuthorities.length : 0}
pendingValidators={
tapestryDraft
? tapestryParticipants.onesCount
: networkAttestations ? networkAttestations.filter(v => v !== undefined).length : 0
}
/>
</Paper>
<Box width="100%" maxWidth="420px" mt="15px">
<Paper
fullWidth
enableBackground
topRight={
<GhostStyledIcon
component={SettingsIcon}
viewBox="0 0 23 23"
style={{ display: "flex", alignItems: "center", cursor: "pointer" }}
onClick={() => setSettingsOpen(true)}
/>
}
headerContent={
<Typography variant="h6">Bridge-In</Typography>
}
>
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="center" gap="3px">
<SwapCollection
iconNotNeeded
UpperSwapCard={
<SwapCard
maxWidth="390px"
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
id="from"
token={
<TokenStack
chainTokenNames={[nativeSymbol]}
tokens={[ghstSymbol]}
sx={{ fontSize: "25px", height: "30px" }}
/>
}
tokenName={ghstSymbol}
info={formatCurrency(ghstBalance.toString(), formatDecimals, ghstSymbol)}
endString="Max"
value={amount}
endStringOnClick={() => setAmount(ghstBalance.toString())}
onChange={event => setAmount(event.currentTarget.value)}
inputProps={{ "data-testid": "fromInput" }}
/>}
LowerSwapCard={
<SwapCard
maxWidth="390px"
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
id="to"
token={
<TokenStack
chainTokenNames={[nativeSymbol]}
tokens={["GHST"]}
sx={{ fontSize: "25px", height: "30px" }}
/>
}
tokenName={"GHST"}
value={estimatedOutAmount}
inputProps={{ "data-testid": "toInput" }}
/>
}
/>
<DataRow
title={`Min Bridge-In`}
tooltip="Minimum amount that could be handled by the bridge to avoid dust balances."
balance={<span>{formatCurrency(existentialDeposit.toString(), formatDecimals, ghstSymbol)}</span>}
/>
<PrimaryButton
fullWidth
disabled={actionDisabled}
onClick={startGhostBridge}
>
Bond
</PrimaryButton>
</Box>
</Paper>
</Box>
</Box>
</Container>
</Box>
)
}
export default BridgeIn;

View File

@ -0,0 +1,48 @@
import { useState, useEffect } from "react";
import { Box, Typography, FormControl, OutlinedInput, InputLabel } from "@mui/material";
import { BridgeGhostAddressCard } from "./BridgeGhostAddress";
import Modal from "../../components/Modal/Modal";
export const BridgeInSettingsModal = ({ isOpen, onClose, decimals, setDecimals, receiver, setReceiver }) => {
return (
<Modal
data-testid="bridge-in-settings-modal"
maxWidth="476px"
headerContent={<Typography variant="h6">Bridge-In Settings</Typography>}
open={isOpen}
onClose={onClose}
minHeight={"100px"}
>
<Box>
<InputLabel htmlFor="recipient">Decimal representation</InputLabel>
<Box mt="8px">
<FormControl variant="outlined" color="primary" fullWidth>
<OutlinedInput
inputProps={{ "data-testid": "decimals-to-wait" }}
type="number"
id="decimals-to-wait"
value={decimals}
onChange={event => setDecimals(event.currentTarget.value)}
endAdornment="decimals"
/>
</FormControl>
</Box>
<Box mt="8px">
<Typography variant="body2" color="textSecondary">
Number of decimals to be shown in token balances
</Typography>
</Box>
</Box>
<Box mt="32px">
<InputLabel htmlFor="recipient">Destination address</InputLabel>
<BridgeGhostAddressCard receiver={receiver} setReceiver={setReceiver} />
<Box mt="8px">
<Typography variant="body2" color="textSecondary">
Recipient address of swapped assets and liquidity tokens
</Typography>
</Box>
</Box>
</Modal>
)
}

View File

@ -41,12 +41,17 @@ import PoolContainer from "./PoolContainer";
import SwapContainer from "./SwapContainer";
import TokenModal from "./TokenModal";
const DEADLINE_PREFIX = "dex-deadline";
const SLIPPAGE_PREFIX = "dex-slippage";
const DECIMALS_PREFIX = "dex-decimals";
const DESTINATION_PREFIX = "dex-destination";
const Dex = ({ chainId, address, connect, config }) => {
const location = useLocation();
const pathname = useParams();
const theme = useTheme();
const { getStorageValue, setStorageValue } = useLocalStorage();
const { useStorageValue, setStorageValue, CONSTANTS } = useLocalStorage();
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
@ -59,12 +64,12 @@ const Dex = ({ chainId, address, connect, config }) => {
const [topTokenListOpen, setTopTokenListOpen] = useState(false);
const [bottomTokenListOpen, setBottomTokenListOpen] = useState(false);
const [secondsToWait, setSecondsToWait] = useState(() => getStorageValue(chainId, address, "dex-deadline", "60"));
const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "dex-slippage", "5"));
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "dex-decimals", "5"));
const [actualDestinationAddress, setActualDestinationAddress] = useState(() => getStorageValue(chainId, address, "dex-destination", address));
const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress);
const { data: secondsToWait } = useStorageValue(chainId, address, DEADLINE_PREFIX, "60");
const { data: slippage } = useStorageValue(chainId, address, SLIPPAGE_PREFIX, "5");
const { data: formatDecimals } = useStorageValue(chainId, address, DECIMALS_PREFIX, "5");
const { data: actualDestinationAddress } = useStorageValue(chainId, address, DECIMALS_PREFIX, address);
const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress);
const [tokenAddressTop, setTokenAddressTop] = useState(EMPTY_ADDRESS);
const [tokenAddressBottom, setTokenAddressBottom] = useState(FTSO_ADDRESSES[chainId]);
@ -192,23 +197,20 @@ const Dex = ({ chainId, address, connect, config }) => {
return dexInfo ? dexInfo.isFork : false;
}, [chainId, pathname])
const setSlippageInner = useCallback((value) => {
const setSlippage = useCallback((value) => {
const maybeValue = parseFloat(value);
if (!maybeValue || parseFloat(value) <= 100) {
setSlippage(value);
setStorageValue(chainId, address, "dex-slippage", value);
setStorageValue(chainId, address, SLIPPAGE_PREFIX, value);
}
}, [chainId, address]);
const setSecondsToWaitInner = useCallback((value) => {
setSecondsToWait(value);
setStorageValue(chainId, address, "dex-deadline", value);
const setSecondsToWait = useCallback((value) => {
setStorageValue(chainId, address, DEADLINE_PREFIX, value);
}, [chainId, address]);
const setFormatDecimalsInner = useCallback((value) => {
const setFormatDecimals = useCallback((value) => {
if (Number(value) <= 17) {
setFormatDecimals(value);
setStorageValue(chainId, address, "dex-decimals", value);
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
}
}, [chainId, address]);
@ -216,8 +218,7 @@ const Dex = ({ chainId, address, connect, config }) => {
const cleanedValue = value.trim();
setDestinationAddress(value);
if (isAddress(cleanedValue)) {
setActualDestinationAddress(value);
setStorageValue(chainId, address, "dex-destination", value);
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
}
}, [chainId, address]);
@ -278,7 +279,7 @@ const Dex = ({ chainId, address, connect, config }) => {
type="text"
id="slippage-dex"
value={slippage}
onChange={event => setSlippageInner(event.currentTarget.value)}
onChange={event => setSlippage(event.currentTarget.value)}
endAdornment="%"
/>
</FormControl>
@ -299,7 +300,7 @@ const Dex = ({ chainId, address, connect, config }) => {
type="number"
id="seconds-to-wait"
value={secondsToWait}
onChange={event => setSecondsToWaitInner(event.currentTarget.value)}
onChange={event => setSecondsToWait(event.currentTarget.value)}
endAdornment="seconds"
/>
</FormControl>
@ -320,7 +321,7 @@ const Dex = ({ chainId, address, connect, config }) => {
type="number"
id="decimals-to-wait"
value={formatDecimals}
onChange={event => setFormatDecimalsInner(event.currentTarget.value)}
onChange={event => setFormatDecimals(event.currentTarget.value)}
endAdornment="decimals"
/>
</FormControl>

View File

@ -38,7 +38,7 @@ const PoolContainer = ({
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 456px)");
const { tryStoreDexInHistory } = useLocalStorage();
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
const [isPending, setIsPending] = useState(false);
const [amountTop, setAmountTop] = useState("");
@ -167,7 +167,12 @@ const PoolContainer = ({
const addLiquidityInner = async () => {
setIsPending(true);
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
const numericSecondsToWait = parseInt(secondsToWait, 10);
const safeSecondsToWait = (!isNaN(numericSecondsToWait) && numericSecondsToWait > 0)
? numericSecondsToWait
: 0;
const deadline = Math.floor(Date.now() / 1000) + safeSecondsToWait;
const shares = 100000;
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
@ -201,7 +206,7 @@ const PoolContainer = ({
txHash = await addLiquidity(params);
}
if (txHash) tryStoreDexInHistory(chainId, address, txHash);
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.dex.key, txHash, CONSTANTS.dex.add);
await balanceRefetchTop();
await balanceRefetchBottom();

View File

@ -46,7 +46,7 @@ const SwapContainer = ({
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 456px)");
const { tryStoreDexInHistory } = useLocalStorage();
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
const [isPending, setIsPending] = useState(false);
const [amountBottom, setAmountBottom] = useState("");
@ -163,7 +163,12 @@ const SwapContainer = ({
const swapTokens = async () => {
setIsPending(true);
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
const numericSecondsToWait = parseInt(secondsToWait, 10);
const safeSecondsToWait = (!isNaN(numericSecondsToWait) && numericSecondsToWait > 0)
? numericSecondsToWait
: 0;
const deadline = Math.floor(Date.now() / 1000) + safeSecondsToWait;
const shares = 100000;
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
@ -202,7 +207,7 @@ const SwapContainer = ({
}
}
if (txHash) tryStoreDexInHistory(chainId, address, txHash);
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.dex.key, txHash, CONSTANTS.dex.swap);
await balanceRefetchTop();
await balanceRefetchBottom();

View File

@ -0,0 +1,531 @@
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;

View File

@ -127,11 +127,12 @@ const DexViewModal = ({ chainId, isOpen, close, config, historyRecord }) => {
headerText={
<>
{swapInfo && "DEX Swap"}
{!swapInfo && "Parsing Transaction Hash"}
</>
}
onClose={() => close()}
>
<Box display="flex" flexDirection="column" gap="15px">
{swapInfo && <Box display="flex" flexDirection="column" gap="15px">
<Box width="100%" display="flex" flexDirection="column" justifyContent="space-between" alignItems="flex-start">
<Box display="flex" justifyContent="space-between" width="100%">
<Typography>{tokenIn ? tokenIn.symbol : "Unknown"} Sender</Typography>
@ -183,7 +184,7 @@ const DexViewModal = ({ chainId, isOpen, close, config, historyRecord }) => {
>
View Transaction Details
</PrimaryButton>
</Box>
</Box>}
</Modal>
)
}

View File

@ -39,6 +39,7 @@ import EraseModal from "./EraseModal";
import SettingsModal from "./SettingsModal";
import DexViewModal from "./DexViewModal";
import StakingViewModal from "./StakingViewModal";
import BridgeInModal from "./BridgeInModal";
const HistoryItem = ({ item, chainId, config, currentTime, eraseRecord, setViewModalOpen, isSmallScreen }) => {
const theme = useTheme();
@ -57,7 +58,7 @@ const HistoryItem = ({ item, chainId, config, currentTime, eraseRecord, setViewM
const chipStatus = useMemo(() => {
const delay = 3600000; // useEvmNetwork({ evmChainId: chainId })
const width = isVerySmallScreen ? "21px" : "120px"
const width = isVerySmallScreen ? "21px" : "100px"
if (item.submitted + delay > currentTime) {
return <Chip sx={{ width: width }} label={isVerySmallScreen ? "" : "Pending"} template={"gray"} />
@ -94,7 +95,10 @@ const HistoryItem = ({ item, chainId, config, currentTime, eraseRecord, setViewM
</Box>
</Box>
{chipStatus}
<Box display="flex" flexDirection="column" alignItems="flex-end">
<Typography>{item?.name ?? "Unknown"}</Typography>
{chipStatus}
</Box>
</Box>
<Box display="flex" flexDirection="row" gap="20px">
@ -111,14 +115,16 @@ const HistoryItem = ({ item, chainId, config, currentTime, eraseRecord, setViewM
<TableCell sx={{ padding: "10px 0" }}>
<Box display="flex" alignItems="center">
<TokenStack chainTokenNames={["ETH"]} tokens={["FTSO"]} />
<Link
ml="10px"
href={`${chainInfo.url}/tx/${item.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{shorten(item.txHash, 9, -7)}</Typography>
</Link>
<Box ml="10px" display="flex" flexDirection="column" alignItems="flex-start">
<Typography>{item?.name ?? "Unknown"}</Typography>
<Link
href={`${chainInfo.url}/tx/${item.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{shorten(item.txHash, 7, -5)}</Typography>
</Link>
</Box>
</Box>
</TableCell>
@ -146,13 +152,7 @@ const HistoryList = ({ chainId, storedTransactions, historyType, config, eraseRe
const [currentTime, setCurrentTime] = useState(Date.now());
const [viewModalOpen, setViewModalOpen] = useState(null);
const {
BRIDGE_IN,
BRIDGE_OUT,
DEX_ACTION,
STAKE_ACTION,
BOND_ACTION
} = useLocalStorage();
const { CONSTANTS } = useLocalStorage();
useEffect(() => {
const intervalId = setInterval(() => {
@ -216,7 +216,7 @@ const HistoryList = ({ chainId, storedTransactions, historyType, config, eraseRe
return (
<>
{viewModalOpen !== null && historyType === DEX_ACTION && (
{viewModalOpen !== null && historyType === CONSTANTS.dex.key && (
<DexViewModal
chainId={chainId}
config={config}
@ -226,15 +226,25 @@ const HistoryList = ({ chainId, storedTransactions, historyType, config, eraseRe
/>
)}
{viewModalOpen !== null && historyType === STAKE_ACTION && (
<StakingViewModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{viewModalOpen !== null && historyType === CONSTANTS.stake.key && (
<StakingViewModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{viewModalOpen !== null && historyType === CONSTANTS.bridgeIn.key && (
<BridgeInModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{historyContainer}
</>
@ -258,41 +268,35 @@ const History = ({ chainId, config, tryConnectInjected, address }) => {
}, [location]);
const {
POSSIBLE_HISTORY_TYPES,
getHistoryTypes,
setHistoryTypes,
getStorageValue,
setStorageValue
CONSTANTS,
useStorageValue,
setStorageValue,
clearPendingHistory
} = useLocalStorage();
const [storedHistoryTypes, setStoredHistoryTypes] = useState(() =>
getHistoryTypes(chainId, address)
)
const [storedTransactions, setStoredTransactions] = useState(() =>
getStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], [])
);
const { data: storedHistoryTypes } = useStorageValue(chainId, address, CONSTANTS.historyPrefix, CONSTANTS.defaultHistoryTypes);
const { data: storedTransactions } = useStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []);
useEffect(() => {
setStoredTransactions(getStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []));
}, [chainId, address, historyTypeIndex]);
const { data: pendingHistory } = useStorageValue(chainId, address, CONSTANTS.pendingHistoryPrefix, {});
const handleHistoryTypesChange = useCallback((values) => {
setHistoryTypes(chainId, address, values);
setStoredHistoryTypes(getHistoryTypes(chainId, address));
setStorageValue(chainId, address, CONSTANTS.historyPrefix, values);
}, [chainId, address]);
const handleConfirmErase = useCallback(() => {
if (erasingIndex !== null) {
const updated = storedTransactions.filter((_, index) => index !== erasingIndex);
setStoredTransactions(updated);
setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], updated);
} else {
setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []);
setStoredTransactions([])
}
setErasingOpen(false);
setErasingIndex(null);
}, [storedTransactions, erasingIndex, chainId, address, historyTypeIndex])
}, [storedTransactions, erasingIndex, chainId, address, historyTypeIndex]);
useEffect(() => {
clearPendingHistory(chainId, address, storedHistoryTypes.at(historyTypeIndex));
}, [storedHistoryTypes, historyTypeIndex])
return (
<Box minHeight="calc(100vh - 43px)">
@ -320,7 +324,7 @@ const History = ({ chainId, config, tryConnectInjected, address }) => {
close={() => setSettingsOpen(false)}
onChange={handleHistoryTypesChange}
selectedValues={storedHistoryTypes}
possibleValues={POSSIBLE_HISTORY_TYPES}
possibleValues={CONSTANTS.possibleHistoryTypes}
/>
<Box mt="15px">
@ -347,7 +351,8 @@ const History = ({ chainId, config, tryConnectInjected, address }) => {
gap="15px"
>
<Tabs
centered
centered={!isSemiSmallScreen}
orientation={isSemiSmallScreen ? "vertical" : "horizontal"}
textColor="primary"
indicatorColor="primary"
value={storedHistoryTypes[historyTypeIndex]}
@ -364,8 +369,36 @@ const History = ({ chainId, config, tryConnectInjected, address }) => {
key={i}
value={type}
aria-label={`history-${i}-button`}
label={type}
style={{ fontSize: "1.2rem" }}
label={
<Box display="flex" alignItems="center" gap="5px">
<Typography variant="h6">{type}</Typography>
{pendingHistory && pendingHistory[type] > 0 && (
<Chip
template="warning"
strong={true}
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
height: "16px",
minWidth: "16px",
width: "auto",
fontSize: "8px",
borderRadius: "100%"
}}
sx={{
"& .MuiChip-label": {
padding: 0,
lineHeight: "16px",
display: "block"
}
}}
label={pendingHistory[type]}
/>
)}
</Box>
}
/>
)
})}

View File

@ -101,11 +101,12 @@ const StakingViewModal = ({ chainId, isOpen, close, config, historyRecord }) =>
headerText={
<>
{stakeInfo && "Staked"}
{!stakeInfo && "Parsing Transaction Hash"}
</>
}
onClose={() => close()}
>
<Box display="flex" flexDirection="column" gap="15px">
{stakeInfo && <Box display="flex" flexDirection="column" gap="15px">
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Token
parentSx={{
@ -151,7 +152,7 @@ const StakingViewModal = ({ chainId, isOpen, close, config, historyRecord }) =>
>
View Transaction Details
</PrimaryButton>
</Box>
</Box>}
</Modal>
)
}

View File

@ -23,7 +23,7 @@ const StakeConfirmationModal = (props) => {
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
const [isPending, setIsPending] = useState(false);
const { tryStoreStakeInHistory } = useLocalStorage();
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup])
@ -77,6 +77,7 @@ const StakeConfirmationModal = (props) => {
const actionAmount = new DecimalBigNumber(props.amount, props.spendDecimals);
let isRebase = false;
let txHash = null;
let txName = null;
switch (props.action.toUpperCase()) {
case "STAKE":
@ -91,6 +92,7 @@ const StakeConfirmationModal = (props) => {
props.stnkSymbol,
props.ghstSymbol
);
txName = CONSTANTS.stake.stake;
break;
case "UNSTAKE":
isRebase = props.upperToken === props.stnkSymbol;
@ -104,6 +106,7 @@ const StakeConfirmationModal = (props) => {
props.stnkSymbol,
props.ghstSymbol
);
txName = CONSTANTS.stake.unstake;
break;
case "WRAP":
txHash = await wrap(
@ -112,6 +115,7 @@ const StakeConfirmationModal = (props) => {
actionAmount._value.toBigInt(),
props.ghstSymbol
);
txName = CONSTANTS.stake.wrap;
break;
case "UNWRAP":
txHash = await unwrap(
@ -120,12 +124,13 @@ const StakeConfirmationModal = (props) => {
actionAmount._value.toBigInt(),
props.stnkSymbol
);
txName = CONSTANTS.stake.unwrap;
break;
default:
console.log("Wrong action to be executed!");
}
if (txHash) tryStoreStakeInHistory(props.chainId, props.address, txHash)
if (txHash && txName) tryStoreInHistory(props.chainId, props.address, CONSTANTS.stake.key, txHash, txName);
setIsPending(false);
props.onClose();

View File

@ -67,6 +67,10 @@ const StyledBox = styled(Box)(({ theme }) => ({
},
}));
const DECIMALS_PREFIX = "stake-decimals";
const IS_CLAIM_PREFIX = "stake-isClaim";
const IS_TRIGGER_PREFIX = "stake-isTrigger";
export const StakeInputArea = ({
chainSymbol,
upperToken,
@ -84,11 +88,11 @@ export const StakeInputArea = ({
const [bottomTokenModalOpen, setBottomTokenModalOpen] = useState(false);
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
const { getStorageValue, setStorageValue } = useLocalStorage();
const { useStorageValue, setStorageValue } = useLocalStorage();
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "stake-decimals", "5"));
const [isClaim, setIsClaim] = useState(() => getStorageValue(chainId, address, "stake-isClaim", true));
const [isTrigger, setIsTrigger] = useState(() => getStorageValue(chainId, address, "stake-isTrigger", true));
const { data: formatDecimals } = useStorageValue(chainId, address, DECIMALS_PREFIX, "5");
const { data: isClaim } = useStorageValue(chainId, address, IS_CLAIM_PREFIX, true);
const { data: isTrigger } = useStorageValue(chainId, address, IS_TRIGGER_PREFIX, true);
const [amount, setAmount] = useState("");
const [receiveAmount, setReceiveAmount] = useState("");
@ -103,20 +107,17 @@ export const StakeInputArea = ({
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const setIsClaimInner = useCallback((value) => {
setIsClaim(value);
setStorageValue(chainId, address, "stake-isClaim", value);
const setIsClaim = useCallback((value) => {
setStorageValue(chainId, address, IS_CLAIM_PREFIX, value);
}, [chainId, address]);
const setIsTriggerInner = useCallback((value) => {
setIsTrigger(value);
setStorageValue(chainId, address, "stake-isTrigger", value);
const setIsTrigger = useCallback((value) => {
setStorageValue(chainId, address, IS_TRIGGER_PREFIX, value);
}, [chainId, address]);
const setFormatDecimalsInner = useCallback((value) => {
const setFormatDecimals = useCallback((value) => {
if (Number(value) <= 17) {
setFormatDecimals(value);
setStorageValue(chainId, address, "stake-decimals", value);
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
}
}, [chainId, address]);
@ -251,11 +252,11 @@ export const StakeInputArea = ({
open={settingsModalOpen}
onClose={closeSettingModal}
formatDecimals={formatDecimals}
onFormatDecimalsChange={event => setFormatDecimalsInner(event.currentTarget.value)}
onFormatDecimalsChange={event => setFormatDecimals(event.currentTarget.value)}
isClaim={isClaim}
onIsClaimChanged={setIsClaimInner}
onIsClaimChanged={setIsClaim}
isTrigger={isTrigger}
onTriggerRebaseChange={setIsTriggerInner}
onTriggerRebaseChange={setIsTrigger}
/>
<StakeConfirmationModal
chainSymbol={chainSymbol}

View File

@ -2,76 +2,136 @@ import { createContext, useEffect, useContext, useState, useMemo, useCallback, u
import { Unstable } from "@substrate/connect-discovery"
import { createClient } from "@polkadot-api/substrate-client"
import { getObservableClient } from "@polkadot-api/observable-client"
import { getInjectedExtensions, connectInjectedExtension } from "polkadot-api/pjs-signer";
import useSWR from "swr"
import { createClient as createClientOther } from "polkadot-api";
import { getSmProvider } from "polkadot-api/sm-provider"
import { startFromWorker } from "polkadot-api/smoldot/from-worker"
import SmWorker from "polkadot-api/smoldot/worker?worker"
import { casper as casperMetadata } from "@polkadot-api/descriptors";
// TODO: move me to file
const chainSpec = `{
"badBlocks": null,
"bootNodes": [
"/dns/bootnode007.ghostchain.io/tcp/30335/p2p/12D3KooWF9SWxz9dmy6vfndQhoxqCa7PESaoFWEiF8Jkqh4xKDRf",
"/dns/bootnode007.ghostchain.io/tcp/30336/ws/p2p/12D3KooWF9SWxz9dmy6vfndQhoxqCa7PESaoFWEiF8Jkqh4xKDRf",
"/dns/bootnode007.ghostchain.io/tcp/443/wss/p2p/12D3KooWF9SWxz9dmy6vfndQhoxqCa7PESaoFWEiF8Jkqh4xKDRf",
"/dns/bootnode70.chain.ghostchain.io/tcp/30334/p2p/12D3KooWP3h5dSdqcpvsCr7fp1jyfqDj291QDZ68a4gY6VQ8T1nW",
"/dns/bootnode70.chain.ghostchain.io/tcp/30335/ws/p2p/12D3KooWP3h5dSdqcpvsCr7fp1jyfqDj291QDZ68a4gY6VQ8T1nW",
"/dns/bootnode70.chain.ghostchain.io/tcp/30336/wss/p2p/12D3KooWP3h5dSdqcpvsCr7fp1jyfqDj291QDZ68a4gY6VQ8T1nW"
],
"chainType": "Live",
"forkBlocks": null,
"genesis": {
"stateRootHash": "0xb4784a3c8f2f29ba3f191d563f1166f08f4e0337e114679c0f24ef77d06e10e4"
},
"id": "casper_staging_testnet",
"lightSyncState": {
"babeEpochChanges": "0x042c315b655c7144df4bfdc41837eeacb84d80aee06eaba51f6941a0c7a26cb1bf5d090000015e9de408000000000ea2e40800000000049b2e88b8ce43a677c1ca90f62134800b7e510c0c353dad719cad82faf6136cfb0d0e0000010ea2e40800000000bea6e408000000000000082c315b655c7144df4bfdc41837eeacb84d80aee06eaba51f6941a0c7a26cb1bf5d0900000103000000000000005e9de40800000000b00400000000000014daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e0100000000000000e0f0a776ecc9fa5e1f22e2fa001fe3fba5aea52b9444bc894b45589d4213247501000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f01000000000000002ff7c765405e9c11a59525a08173bb1fa59e5e281090f8e4d1824469f513d0cc01000000000000000400000000000000029b2e88b8ce43a677c1ca90f62134800b7e510c0c353dad719cad82faf6136cfb0d0e00000104000000000000000ea2e40800000000b00400000000000014daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e0100000000000000e0f0a776ecc9fa5e1f22e2fa001fe3fba5aea52b9444bc894b45589d4213247501000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f0100000000000000dea465ff8bf16938cc9ead89bf9fdb5d9c495fad61fd952b2067ebdc51c4e1c10100000000000000040000000000000002",
"babeFinalizedBlockWeight": 1039,
"finalizedBlockHeader": "0x6ed6c2debc2e5fd4e7db0d14426d1b904e6c101f2c62a71b8e891c217ba79079f9433280c0257a860f793833bd6bed6b9f86094400aa8e5ea24a81752f0f046ff5ed560f7231760c622ae35d3968fbab033ed34d950b57a7937ae45c411d1d384748080642414245b50103000000004fa0e40800000000a2c7d71b4e1f94c0208c9730ab93955b08c8d7ac86d2fc6ec5f62cc3272a5f1ec0e28c798461372109ee3e12f03ff66ea5ad9cac5b1c37d7280d2333d0226f0865ea77a801bad79785a8ca83c87f0dbf1e36936a6f65cd7b125a41c52f38f80105424142450101caaa6fe5f9df57930e2ac08aef43b75d5477d410c73c3a8e6dea9d5503b9fd25eae1d12b6604194389e22f3f4265d581eb162d1dc5a9034247fe33765dddbe8c",
"grandpaAuthoritySet": "0x1455446f9a7aa99ced06b317c80ce90d56b84e56526775683af2525969e8da0b640100000000000000236d2fa03f4ed8cb65de7e514d7540159b328f1c170dd402b094ad7fbf5472180100000000000000b63c5a0cf342b9b04931bc8ed74d7d0165ab99ab5f8a4514797d4b299a4501fe01000000000000008f9ea20bf4a807a8e710f7559dece86e94672b5b361de157bdaa5c1f37849f8d01000000000000000aa3a88f6b777c95c3dfe7e997b76798413f16aa325f34824cae0c9102b281d5010000000000000000000000000000000001fe1000000000"
},
"name": "Casper Staging Testnet",
"properties": {
"ss58Format": 1996,
"tokenDecimals": 18,
"tokenSymbol": "CSPR"
},
"protocolId": "cspr",
"telemetryEndpoints": [
[
"/dns/telemetry.ghostchain.io/tcp/443/x-parity-wss/%2Fsubmit%2F",
0
]
]
}`;
const GHOST_CONNECT_EXTENSION = "GHOST Connect Browser Extension"
const MAX_BLOCK_TIMEOUT = 15000
const DEFAULT_CHAIN_ID = "0xcaf9c5ddccc7ba6f96149a6b8a81c26a3641742795cd5e7e0aee79bf3f4e77b7"
const UnstableProvider = createContext(null)
export const useUnstableProvider = () => useContext(UnstableProvider)
export const UnstableProviderProvider = ({ children }) => {
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
const [providerIndex, setProviderIndex] = useState(0);
const [client, setClient] = useState(undefined);
const [status, setStatus] = useState("loading");
const { data: providerDetails } = useSWR("getGhostProviders", () =>
Unstable.getSubstrateConnectExtensionProviders()
);
const [selectedExtension, setSelectedExtension] = useState(null);
const [selectedAccountIndex, setSelectedAccountIndex] = useState(0);
const [accounts, setAccounts] = useState([]);
const providerDetail = useMemo(() => providerDetails?.at(providerIndex), [providerDetails, providerIndex]);
useEffect(() => {
let isMounted = true;
let provider = null;
const { data: provider } = useSWR(
() => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.provider` : null,
() => providerDetail ? providerDetail.provider : null
);
try {
const smoldot = startFromWorker(new SmWorker());
provider = createClientOther(
getSmProvider(() => smoldot.addChain({ chainSpec })),
)
const { data: accounts } = useSWR(
() => providerDetail ? `providerDetail.${providerDetail.info.uuid}.provider.getAccounts(${chainId})` : null,
async () => {
if (!providerDetail) return []
const activeProvider = await providerDetail.provider;
return activeProvider.getAccounts(chainId);
},
)
if (isMounted) {
setClient(provider);
setStatus("connected");
}
const account = useMemo(() => {
if (Array.isArray(accounts)) return accounts?.at(0)?.address;
if (typeof accounts === 'string') return accounts?.address;
return undefined
}, [accounts])
} catch (error) {
console.error("Smoldot initialization failed:", error);
if (isMounted) setStatus("error");
}
const connectionState = useMemo(async () => {
if (!providerDetail) return 'no-extension';
if (!provider) return 'loading';
return () => {
isMounted = false;
if (provider) provider.destroy();
};
}, []);
const chains = await provider.getChains();
if (chains[chainId]) return 'connected';
useEffect(() => {
if (status !== "connected") return;
return 'wrong-network';
}, [providerDetail, provider, chainId]);
let isMounted = true;
(async () => {
const extensions = getInjectedExtensions();
const targetExt = extensions.find(ext => ext === GHOST_CONNECT_EXTENSION);
const client = useMemo(() => {
if (!provider || !chainId) return undefined;
if (!targetExt) {
console.warn("Ghost Connect extension not found in window.injectedWeb3");
return;
}
const chain = provider.getChains()[chainId];
if (!chain) return undefined;
const ext = await connectInjectedExtension(targetExt);
if (isMounted) setSelectedExtension(ext);
})();
return createClient(chain.connect)
}, [provider, chainId]);
return () => { isMounted = false; };
}, [status]);
const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]);
useEffect(() => {
if (!selectedExtension) return;
const unsubscribe = selectedExtension.subscribe((updatedAccounts) => {
setAccounts(updatedAccounts);
});
return unsubscribe;
}, [selectedExtension]);
const account = useMemo(() => accounts?.at(selectedAccountIndex)?.address, [accounts, selectedAccountIndex]);
const casperApi = useMemo(() => client?.getTypedApi(casperMetadata), [client]);
const finalizedBlock$ = useMemo(() => client?.finalizedBlock$, [client]);
const value = useMemo(() => ({
isExtensionMissing: connectionState === "no-extension",
providerDetails,
providerDetail,
connectProviderByIndex: setProviderIndex,
chainId,
account,
status,
client,
setChainId,
chainHead$
}), [providerDetails, providerDetail, chainId, client, account, chainHead$]);
account,
accounts,
casperApi,
finalizedBlock$,
}), [accounts, status, client, finalizedBlock$]);
return (
<UnstableProvider.Provider value={value}>

View File

@ -1,7 +1,5 @@
import { createContext, useContext, useState, useEffect } from "react";
const LocalStorageContext = createContext();
export const useLocalStorage = () => useContext(LocalStorageContext);
import useSWR, { mutate } from "swr";
const BRIDGE_IN = "Bridge-In";
const BRIDGE_OUT = "Bridge-Out";
@ -9,77 +7,109 @@ const DEX_ACTION = "DEX";
const STAKE_ACTION = "Stake";
const BOND_ACTION = "Bond";
const POSSIBLE_HISTORY_TYPES = [BRIDGE_IN, BRIDGE_OUT, DEX_ACTION, STAKE_ACTION, BOND_ACTION];
const DEX_ACTION_SWAP = "Swap";
const DEX_ACTION_ADD = "Add Liquidity";
const STAKE_ACTION_STAKE = "Stake";
const UNSTAKE_ACTION_STAKE = "Unstake";
const WRAP_ACTION_STAKE = "Wrap";
const UNWRAP_ACTION_STAKE = "Unwrap";
// TODO: add bond later
const POSSIBLE_HISTORY_TYPES = [BRIDGE_IN, BRIDGE_OUT, DEX_ACTION, STAKE_ACTION];
const DEFAULT_HISTORY_TYPES = [BRIDGE_IN, BRIDGE_OUT];
const HISTORY_PREFIX = "history"
const HISTORY_PREFIX = "history";
const PENDING_HISTORY_PREIFX = "pending";
const LocalStorageContext = createContext();
export const useLocalStorage = () => useContext(LocalStorageContext);
const CONSTANTS = {
possibleHistoryTypes: POSSIBLE_HISTORY_TYPES,
defaultHistoryTypes: DEFAULT_HISTORY_TYPES,
historyPrefix: HISTORY_PREFIX,
pendingHistoryPrefix: PENDING_HISTORY_PREIFX,
bridgeIn: { key: BRIDGE_IN, name: BRIDGE_IN },
bridgeOut: { key: BRIDGE_OUT, name: BRIDGE_OUT },
dex: { key: DEX_ACTION, swap: DEX_ACTION_SWAP, add: DEX_ACTION_ADD },
stake: {
key: STAKE_ACTION,
stake: STAKE_ACTION_STAKE,
unstake: UNSTAKE_ACTION_STAKE,
wrap: WRAP_ACTION_STAKE,
unwrap: UNWRAP_ACTION_STAKE,
},
};
export const LocalStorageProvider = ({ children }) => {
const getStorageKey = (chainId, address, target) => {
return `${chainId}:${address}:${target}`;
}
const getStorageValue = (chainId, address, target, defaultValue) => {
const key = getStorageKey(chainId, address, target);
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
}
const useStorageValue = (chainId, address, target, defaultValue) => {
return useSWR(
chainId && address ? ["local", chainId, address, target] : null,
([_, chainId, address, target]) => {
const key = getStorageKey(chainId, address, target);
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
},
{ revalidateOnFocus: false, fallbackData: defaultValue }
);
};
const setStorageValue = (chainId, address, target, value) => {
const setStorageValue = async (chainId, address, target, value) => {
const key = getStorageKey(chainId, address, target);
localStorage.setItem(key, JSON.stringify(value));
await mutate(["local", chainId, address, target]);
}
const getHistoryTypes = (chainId, address) => {
const key = getStorageKey(chainId, address, HISTORY_PREFIX);
const clearPendingHistory = async (chainId, address, target) => {
const key = getStorageKey(chainId, address, PENDING_HISTORY_PREIFX);
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : DEFAULT_HISTORY_TYPES;
if (!stored) return
const pendingValues = JSON.parse(stored);
if (target in pendingValues) {
delete pendingValues[target];
localStorage.setItem(key, JSON.stringify(pendingValues));
await mutate(["local", chainId, address, PENDING_HISTORY_PREIFX]);
}
}
const setHistoryTypes = (chainId, address, value) => {
const key = getStorageKey(chainId, address, HISTORY_PREFIX);
localStorage.setItem(key, JSON.stringify(value));
}
const tryStoreInHistory = (chainId, address, target, txHash) => {
const historyTypes = getHistoryTypes(chainId, address);
const tryStoreInHistory = async (chainId, address, target, txHash, name) => {
const historyKey = getStorageKey(chainId, address, HISTORY_PREFIX);
const storedHistoryTypes = localStorage.getItem(historyKey);
const historyTypes = storedHistoryTypes ? JSON.parse(storedHistoryTypes) : [];
if (!historyTypes.includes(target)) return;
const key = getStorageKey(chainId, address, target);
const stored = localStorage.getItem(key);
const values = stored ? JSON.parse(stored) : [];
const value = { txHash, submitted: Date.now() };
const value = { name, txHash, submitted: Date.now() };
const newValues = [value, ...values];
localStorage.setItem(key, JSON.stringify(newValues));
}
const tryStoreBondInHistory = (chainId, address, txHash) => {
tryStoreInHistory(chainId, address, BOND_ACTION, txHash);
}
const pendingKey = getStorageKey(chainId, address, PENDING_HISTORY_PREIFX);
const pendingItems = localStorage.getItem(pendingKey);
const pendingValues = pendingItems ? JSON.parse(pendingItems) : {};
const tryStoreStakeInHistory = (chainId, address, txHash) => {
tryStoreInHistory(chainId, address, STAKE_ACTION, txHash);
}
const currentCount = pendingValues[target] || 0;
pendingValues[target] = currentCount + 1;
localStorage.setItem(pendingKey, JSON.stringify(pendingValues));
const tryStoreDexInHistory = (chainId, address, txHash) => {
tryStoreInHistory(chainId, address, DEX_ACTION, txHash);
await mutate(["local", chainId, address, PENDING_HISTORY_PREIFX]);
}
return (
<LocalStorageContext.Provider value={{
BRIDGE_IN,
BRIDGE_OUT,
DEX_ACTION,
STAKE_ACTION,
BOND_ACTION,
POSSIBLE_HISTORY_TYPES,
tryStoreBondInHistory,
tryStoreStakeInHistory,
tryStoreDexInHistory,
getHistoryTypes,
setHistoryTypes,
getStorageValue,
CONSTANTS,
tryStoreInHistory,
useStorageValue,
clearPendingHistory,
setStorageValue
}}>
{children}

View File

@ -14,9 +14,62 @@ import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
import { shorten } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { executeOnChainTransaction } from "../helpers";
import { executeOnChainTransaction, getTokenDecimals } from "../helpers";
import { useCirculatingSupply } from "../tokens";
export const useWeaverState = (chainId, blockNumber, context) => {
const { session, index } = context;
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
const { data: merkleRoot } = useReadContract({
abi: GatekeeperAbi,
address: gatekeeperAddress,
functionName: "getRoot",
args: [index, blockNumber],
scopeKey: `getRoot-${chainId}-${session}-${gatekeeperAddress}`,
chainId: chainId,
});
const { data: merkleProof } = useReadContract({
abi: GatekeeperAbi,
address: gatekeeperAddress,
functionName: "getProof",
args: [index, session, blockNumber],
scopeKey: `getProof-${chainId}-${session}-${gatekeeperAddress}`,
chainId: chainId,
});
const { data: slotValues } = useReadContract({
abi: GatekeeperAbi,
address: gatekeeperAddress,
functionName: "getSlotValues",
args: [index, session, blockNumber],
scopeKey: `getSlotValues-${chainId}-${session}-${gatekeeperAddress}`,
chainId: chainId,
});
return null
}
export const useGatekeeperMinimum = (chainId) => {
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
const { data: minimum, error } = useReadContract({
abi: GatekeeperAbi,
address: gatekeeperAddress,
functionName: "EXISTENTIAL_DEPOSIT",
scopeKey: `existentialDeposit-${chainId}-${gatekeeperAddress}`,
chainId: chainId,
});
const existentialDeposit = new DecimalBigNumber(
minimum ? minimum : 0n,
getTokenDecimals("GHST"),
);
return { existentialDeposit};
}
export const useGatekeeperApy = (chainId) => {
const circulatingSupply = useCirculatingSupply(chainId, "STNK");
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
@ -32,11 +85,11 @@ export const useGatekeeperApy = (chainId) => {
const amountIn = new DecimalBigNumber(
(metadata?.amountIn ?? 0n) + (amountInHistory[chainId] ?? 0n),
18,
getTokenDecimals("GHST"),
);
const amountOut = new DecimalBigNumber(
(metadata?.amountOut ?? 0n) + (amountOutHistory[chainId] ?? 0n),
18,
getTokenDecimals("GHST"),
);
const deployedAt = metadata?.deployedAt ?? 0;
@ -310,7 +363,7 @@ export const ghost = async (chainId, account, receiver, amount) => {
const args = [receiver, amount];
const messages = {
replacedMsg: "Bridge transaction was replaced. Wait for inclusion please.",
successMsg: `Amount successfully bridged! Check tx hash status or wait for slow clap finalization.`,
successMsg: `Amount successfully bridged! Check Transaction History to trace the progress.`,
errorMsg: "Bridge transaction failed. Check logs for error detalization.",
};
const txHash = await executeOnChainTransaction({

View File

@ -18,12 +18,12 @@ export const darkPalette = {
success: "#60C45B", // idk where this is - done
userFeedback: "#49A1F2", // idk where this is
error: "#F06F73", // red negative % - done
warning: "#ed6c02", // idk where this is - done
warning: "#F2E370", // idk where this is - done
pnlGain: "#60C45B", // green positive % - done
},
bridgeProgress: {
error: "#F06F73",
warning: "#ed6c02",
warning: "#F2E370",
success: "#60C45B",
},
validatorsColor: {