migrate to PAPI and integrate weaver protocol
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
parent
314abe1aba
commit
132bd0986b
3
.papi/descriptors/.gitignore
vendored
Normal file
3
.papi/descriptors/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!package.json
|
||||
25
.papi/descriptors/package.json
Normal file
25
.papi/descriptors/package.json
Normal 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
9
.papi/polkadot-api.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": 0,
|
||||
"descriptorPath": ".papi\\descriptors",
|
||||
"entries": {
|
||||
"casper": {
|
||||
"metadata": "metadatas/casper_runtime.scale"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
metadatas/casper_runtime.scale
Normal file
BIN
metadatas/casper_runtime.scale
Normal file
Binary file not shown.
@ -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"
|
||||
}
|
||||
|
||||
1420
pnpm-lock.yaml
1420
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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
@ -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>
|
||||
);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
|
||||
50
src/containers/Bridge/BridgeGhostAddress.jsx
Normal file
50
src/containers/Bridge/BridgeGhostAddress.jsx
Normal 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%"
|
||||
/>
|
||||
)
|
||||
}
|
||||
461
src/containers/Bridge/BridgeIn.jsx
Normal file
461
src/containers/Bridge/BridgeIn.jsx
Normal 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;
|
||||
48
src/containers/Bridge/BridgeSettingsModal.jsx
Normal file
48
src/containers/Bridge/BridgeSettingsModal.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
531
src/containers/History/BridgeInModal.jsx
Normal file
531
src/containers/History/BridgeInModal.jsx
Normal 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;
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user