Compare commits
6 Commits
main
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| ce6e316feb | |||
| e42f5db893 | |||
| b09c6b82f7 | |||
| 42d5f82040 | |||
| 132bd0986b | |||
| 314abe1aba |
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="https://ipfs.io/ipfs/QmRgJ2UbzpM58te1R91bC2neWsS28F2QatBtzerM1JekyC" type="image/png" />
|
<link rel="icon" href="https://ghostchain.io/wp-content/uploads/2026/06/ghost-favicon.ico" type="image/ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Ubuntu:regular,bold&subset=Latin">
|
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Ubuntu:regular,bold&subset=Latin">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
|||||||
BIN
metadatas/casper_runtime.scale
Normal file
BIN
metadatas/casper_runtime.scale
Normal file
Binary file not shown.
10
package.json
10
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ghost-dao-interface",
|
"name": "ghost-dao-interface",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.58",
|
"version": "0.8.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@ -30,6 +30,8 @@
|
|||||||
"@tanstack/react-query": "^5.67.2",
|
"@tanstack/react-query": "^5.67.2",
|
||||||
"@tanstack/react-query-devtools": "^5.67.2",
|
"@tanstack/react-query-devtools": "^5.67.2",
|
||||||
"@wagmi/core": "^2.17.3",
|
"@wagmi/core": "^2.17.3",
|
||||||
|
"polkadot-api": "^3.3.0",
|
||||||
|
"@polkadot-api/descriptors": "file:.papi/descriptors",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
@ -55,5 +57,11 @@
|
|||||||
"sass-embedded": "^1.85.1",
|
"sass-embedded": "^1.85.1",
|
||||||
"vite": "^6.2.0",
|
"vite": "^6.2.0",
|
||||||
"vite-plugin-svgr": "^4.3.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
10
src/App.jsx
10
src/App.jsx
@ -36,11 +36,12 @@ const BondModalContainer = lazy(() => import("./containers/Bond/BondModal"));
|
|||||||
const StakeContainer = lazy(() => import("./containers/Stake/StakeContainer"));
|
const StakeContainer = lazy(() => import("./containers/Stake/StakeContainer"));
|
||||||
const TreasuryDashboard = lazy(() => import("./containers/TreasuryDashboard/TreasuryDashboard"));
|
const TreasuryDashboard = lazy(() => import("./containers/TreasuryDashboard/TreasuryDashboard"));
|
||||||
const Dex = lazy(() => import("./containers/Dex/Dex"));
|
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 NotFound = lazy(() => import("./containers/NotFound/NotFound"));
|
||||||
const Governance = lazy(() => import("./containers/Governance/Governance"));
|
const Governance = lazy(() => import("./containers/Governance/Governance"));
|
||||||
const ProposalDetails = lazy(() => import("./containers/Governance/ProposalDetails"));
|
const ProposalDetails = lazy(() => import("./containers/Governance/ProposalDetails"));
|
||||||
const NewProposal = lazy(() => import("./containers/Governance/NewProposal"));
|
const NewProposal = lazy(() => import("./containers/Governance/NewProposal"));
|
||||||
|
const History = lazy(() => import("./containers/History/History"));
|
||||||
|
|
||||||
const PREFIX = "App";
|
const PREFIX = "App";
|
||||||
|
|
||||||
@ -182,9 +183,9 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
<nav className={classes.drawer}>
|
<nav className={classes.drawer}>
|
||||||
{isSmallerScreen ? (
|
{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>
|
</nav>
|
||||||
|
|
||||||
@ -199,11 +200,12 @@ function App() {
|
|||||||
<Route path="bonds" element={<Bonds connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
<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="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="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="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" 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} />} />
|
<Route path="governance/:id" element={<ProposalDetails config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||||
<Route path="governance/create" element={<NewProposal config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
<Route path="governance/create" element={<NewProposal config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||||
|
<Route path="history" element={<History config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||||
</Route>
|
</Route>
|
||||||
}
|
}
|
||||||
<Route path="/empty" element={<NotFound
|
<Route path="/empty" element={<NotFound
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -150,7 +150,8 @@ const NavItem = ({
|
|||||||
component={ArrowUpIcon}
|
component={ArrowUpIcon}
|
||||||
/>}
|
/>}
|
||||||
</Box>
|
</Box>
|
||||||
{chip && <Chip size="small" label={chip} template={chipColor} />}
|
|
||||||
|
{chip && <Box pr="6px"><Chip p="18px" size="small" label={chip} template={chipColor} /></Box>}
|
||||||
</Box>
|
</Box>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import HubIcon from '@mui/icons-material/Hub';
|
|||||||
import PublicIcon from '@mui/icons-material/Public';
|
import PublicIcon from '@mui/icons-material/Public';
|
||||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||||
import GavelIcon from '@mui/icons-material/Gavel';
|
import GavelIcon from '@mui/icons-material/Gavel';
|
||||||
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
import CasinoIcon from '@mui/icons-material/Casino';
|
import CasinoIcon from '@mui/icons-material/Casino';
|
||||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||||
import BookIcon from '@mui/icons-material/Book';
|
import BookIcon from '@mui/icons-material/Book';
|
||||||
@ -55,6 +56,7 @@ import WifiProtectedSetupIcon from '@mui/icons-material/WifiProtectedSetup';
|
|||||||
|
|
||||||
import { useTokenSymbol } from "../../hooks/tokens";
|
import { useTokenSymbol } from "../../hooks/tokens";
|
||||||
import { useLiveBonds } from "../../hooks/bonds/index";
|
import { useLiveBonds } from "../../hooks/bonds/index";
|
||||||
|
import { useLocalStorage } from "../../hooks/localstorage";
|
||||||
import pckg from "../../../package.json"
|
import pckg from "../../../package.json"
|
||||||
|
|
||||||
const PREFIX = "NavContent";
|
const PREFIX = "NavContent";
|
||||||
@ -69,16 +71,26 @@ const StyledBox = styled(Box)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const NavContent = ({ chainId, addressChainId }) => {
|
const NavContent = ({ chainId, addressChainId, address }) => {
|
||||||
const { chains } = useSwitchChain();
|
const { chains } = useSwitchChain();
|
||||||
const chainName = useMemo(() => {
|
const chainName = useMemo(() => {
|
||||||
return chains.find(chain => chain.id === chainId).name.toLowerCase();
|
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 { liveBonds: ghostBonds } = useLiveBonds(chainId);
|
||||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
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 sortedGhostBonds = useMemo(() => sortBondsByDiscount(ghostBonds), [ghostBonds]);
|
||||||
const bridgeNumbers = useMemo(() => {
|
const bridgeNumbers = useMemo(() => {
|
||||||
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
||||||
@ -145,9 +157,6 @@ const NavContent = ({ chainId, addressChainId }) => {
|
|||||||
to={`/${chainName}/bonds`}
|
to={`/${chainName}/bonds`}
|
||||||
children={
|
children={
|
||||||
<AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
<AccordionDetails style={{ margin: "0 0 -20px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||||
{sortedGhostBonds.length > 0 && <Box width="180px" mb="10px" ml="auto">
|
|
||||||
<Typography component="span" variant="body2">Bond Discounts</Typography>
|
|
||||||
</Box>}
|
|
||||||
{sortedGhostBonds.map((bond, index) => {
|
{sortedGhostBonds.map((bond, index) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@ -181,8 +190,10 @@ const NavContent = ({ chainId, addressChainId }) => {
|
|||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
|
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Bridge-In`} to={`/${chainName}/bridge-in`} />
|
||||||
<NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />
|
{/* <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">
|
<Box className="menu-divider">
|
||||||
<Divider />
|
<Divider />
|
||||||
</Box>
|
</Box>
|
||||||
@ -194,7 +205,6 @@ const NavContent = ({ chainId, addressChainId }) => {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<NavItem href="https://ghostchain.io/game-theory-3-3" icon={CasinoIcon} label={`${bridgeNumbers} Game Theory`} />
|
<NavItem href="https://ghostchain.io/game-theory-3-3" icon={CasinoIcon} label={`${bridgeNumbers} Game Theory`} />
|
||||||
<NavItem href="http://ecosystem.ghostchain.io" icon={PublicIcon} label={`Ecosystem`} />
|
|
||||||
<NavItem href="https://docs.ghostchain.io/" icon={BookIcon} label={`Documentation`} />
|
<NavItem href="https://docs.ghostchain.io/" icon={BookIcon} label={`Documentation`} />
|
||||||
<StyledBox display="flex" justifyContent="space-around" paddingY="24px">
|
<StyledBox display="flex" justifyContent="space-around" paddingY="24px">
|
||||||
<Link href="https://git.ghostchain.io/ghostchain/ghost-dao-contracts" target="_blank" rel="noopener noreferrer">
|
<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 drawerWidth = 280;
|
||||||
|
|
||||||
const NavDrawer = ({ chainId, addressChainId, mobileOpen, handleDrawerToggle }) => {
|
const NavDrawer = ({ chainId, address, addressChainId, mobileOpen, handleDrawerToggle }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ const NavDrawer = ({ chainId, addressChainId, mobileOpen, handleDrawerToggle })
|
|||||||
disableBackdropTransition={!isIOS}
|
disableBackdropTransition={!isIOS}
|
||||||
disableDiscovery={isIOS}
|
disableDiscovery={isIOS}
|
||||||
>
|
>
|
||||||
<NavContent addressChainId={addressChainId} chainId={chainId} />
|
<NavContent address={address} addressChainId={addressChainId} chainId={chainId} />
|
||||||
</StyledSwipeableDrawer>
|
</StyledSwipeableDrawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import { Drawer } from "@mui/material";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import NavContent from "./NavContent";
|
import NavContent from "./NavContent";
|
||||||
|
|
||||||
const Sidebar = ({ chainId, addressChainId }) => (
|
const Sidebar = ({ chainId, addressChainId, address }) => (
|
||||||
<div className="sidebar" id="sidebarContent">
|
<div className="sidebar" id="sidebarContent">
|
||||||
<Drawer variant="permanent" anchor="left">
|
<Drawer variant="permanent" anchor="left">
|
||||||
<NavContent addressChainId={addressChainId} chainId={chainId} />
|
<NavContent address={address} addressChainId={addressChainId} chainId={chainId} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@ -25,7 +26,7 @@ import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
|
|||||||
import { formatCurrency, shorten } from "../../../helpers";
|
import { formatCurrency, shorten } from "../../../helpers";
|
||||||
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||||
import { RESERVE_ADDRESSES, FTSO_ADDRESSES } from "../../../constants/addresses";
|
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";
|
import { useAccount, useDisconnect, useSwitchChain } from "wagmi";
|
||||||
|
|
||||||
@ -113,28 +114,44 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const tokens = useWallet(chainId, address);
|
const tokens = useWallet(chainId, address);
|
||||||
|
|
||||||
const [currency, setCurrency] = useState("USD");
|
const { client, casperApi, account, isExtensionMissing } = useUnstableProvider();
|
||||||
const { account, isExtensionMissing } = useUnstableProvider();
|
|
||||||
const { chains } = useSwitchChain();
|
const { chains } = useSwitchChain();
|
||||||
|
|
||||||
|
const [currency, setCurrency] = useState("USD");
|
||||||
|
const [accountIndex, setAccountIndex] = useState(0);
|
||||||
|
|
||||||
const chainName = useMemo(() => {
|
const chainName = useMemo(() => {
|
||||||
const chain = chains.find(chain => chain.id === chainId)
|
const chain = chains.find(chain => chain.id === chainId)
|
||||||
if (!chain) return null
|
if (!chain) return null
|
||||||
return chain.name.toLowerCase()
|
return chain.name.toLowerCase()
|
||||||
}, [chains, chainId])
|
}, [chains, chainId]);
|
||||||
|
|
||||||
const chainSpec = useChainSpec();
|
const { data: chainProperties } = useSWR(
|
||||||
const accountBalanceRaw = useSystemAccount({ account });
|
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 accountBalance = useMemo(() => {
|
||||||
const decimals = chainSpec ? chainSpec.properties.tokenDecimals : 18;
|
const decimals = chainProperties ? chainProperties.properties.tokenDecimals : 18;
|
||||||
if (!accountBalanceRaw) return new DecimalBigNumber(0n, decimals);
|
if (!accountBalanceRaw) return new DecimalBigNumber(0n, decimals);
|
||||||
const balance = accountBalanceRaw.data.free
|
const balance = accountBalanceRaw.data.free
|
||||||
+ accountBalanceRaw.data.frozen
|
+ accountBalanceRaw.data.frozen
|
||||||
+ accountBalanceRaw.data.reserved;
|
+ accountBalanceRaw.data.reserved;
|
||||||
|
|
||||||
return new DecimalBigNumber(balance, decimals);
|
return new DecimalBigNumber(balance, decimals);
|
||||||
}, [accountBalanceRaw, chainSpec])
|
}, [accountBalanceRaw, chainProperties]);
|
||||||
|
|
||||||
const nativeGhst = useMemo(() => {
|
const nativeGhst = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@ -201,7 +218,7 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
|
|||||||
<WalletTotalValue
|
<WalletTotalValue
|
||||||
currency={currency}
|
currency={currency}
|
||||||
setCurrency={setCurrency}
|
setCurrency={setCurrency}
|
||||||
chainName={chainSpec?.chainName ?? ""}
|
chainName={chainProperties?.name ?? ""}
|
||||||
address={account ?? "...wait"}
|
address={account ?? "...wait"}
|
||||||
tokens={nativeGhst}
|
tokens={nativeGhst}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,45 +1,46 @@
|
|||||||
import { defineChain } from 'viem'
|
import { defineChain } from 'viem'
|
||||||
import { http, fallback, createConfig } from 'wagmi'
|
import { http, fallback, createConfig } from 'wagmi'
|
||||||
import { sepolia, hoodi } from 'wagmi/chains'
|
import { hoodi } from 'wagmi/chains'
|
||||||
|
|
||||||
const mordor = defineChain({
|
// const mordor = defineChain({
|
||||||
id: 63,
|
// id: 63,
|
||||||
name: 'Mordor',
|
// name: 'Mordor',
|
||||||
nativeCurrency: {
|
// nativeCurrency: {
|
||||||
decimals: 18,
|
// decimals: 18,
|
||||||
name: 'METC',
|
// name: 'METC',
|
||||||
symbol: 'mETC',
|
// symbol: 'mETC',
|
||||||
},
|
// },
|
||||||
rpcUrls: {
|
// rpcUrls: {
|
||||||
default: { http: ['https://rpc.mordor.etccooperative.org'] },
|
// default: { http: ['https://rpc.mordor.etccooperative.org'] },
|
||||||
},
|
// },
|
||||||
blockExplorers: {
|
// blockExplorers: {
|
||||||
default: {
|
// default: {
|
||||||
name: 'Blockscout',
|
// name: 'Blockscout',
|
||||||
url: 'https://etc-mordor.blockscout.com/'
|
// url: 'https://etc-mordor.blockscout.com/'
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
|
|
||||||
export const config = createConfig({
|
export const config = createConfig({
|
||||||
chains: [sepolia, hoodi, mordor],
|
chains: [hoodi],
|
||||||
transports: {
|
transports: {
|
||||||
[sepolia.id]: fallback([
|
// [sepolia.id]: fallback([
|
||||||
http('https://ethereum-sepolia-rpc.publicnode.com'),
|
// http('https://ethereum-sepolia-rpc.publicnode.com'),
|
||||||
http('https://1rpc.io/sepolia'),
|
// http('https://1rpc.io/sepolia'),
|
||||||
http('https://eth-sepolia.public.blastapi.io'),
|
// http('https://eth-sepolia.public.blastapi.io'),
|
||||||
http('https://0xrpc.io/sep'),
|
// http('https://0xrpc.io/sep'),
|
||||||
http('https://api.zan.top/eth-sepolia'),
|
// http('https://api.zan.top/eth-sepolia'),
|
||||||
http('https://eth-sepolia.api.onfinality.io/public'),
|
// http('https://eth-sepolia.api.onfinality.io/public'),
|
||||||
http('https://sepolia.drpc.org')
|
// http('https://sepolia.drpc.org')
|
||||||
]),
|
// ]),
|
||||||
[hoodi.id]: fallback([
|
[hoodi.id]: fallback([
|
||||||
http('https://rpc.hoodi.ethpandaops.io'),
|
http('https://rpc.hoodi.ethpandaops.io'),
|
||||||
|
http('https://rpc.sentio.xyz/hoodi'),
|
||||||
http('https://0xrpc.io/hoodi'),
|
http('https://0xrpc.io/hoodi'),
|
||||||
]),
|
]),
|
||||||
[mordor.id]: fallback([
|
// [mordor.id]: fallback([
|
||||||
http('https://rpc.mordor.etccooperative.org'),
|
// http('https://rpc.mordor.etccooperative.org'),
|
||||||
http('https://geth-mordor.etc-network.info'),
|
// http('https://geth-mordor.etc-network.info'),
|
||||||
])
|
// ])
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,25 +4,25 @@ export const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|||||||
|
|
||||||
export const STAKING_ADDRESSES = {
|
export const STAKING_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
[NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||||
[NetworkId.TESTNET_HOODI]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
[NetworkId.TESTNET_HOODI]: "0x5d659FDEFc9c1552A3a53f661Cf4F52b0d7dDb40",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
[NetworkId.TESTNET_MORDOR]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BOND_DEPOSITORY_ADDRESSES = {
|
export const BOND_DEPOSITORY_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
[NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||||
[NetworkId.TESTNET_HOODI]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
[NetworkId.TESTNET_HOODI]: "0x4105D6b48f0fEfA8033b8B01d89972c2CBd787bD",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
[NetworkId.TESTNET_MORDOR]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DAO_TREASURY_ADDRESSES = {
|
export const DAO_TREASURY_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
[NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||||
[NetworkId.TESTNET_HOODI]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
[NetworkId.TESTNET_HOODI]: "0xeA5113bF7e0af48B6D0Cf1a4B1e4dD0184708a09",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
[NetworkId.TESTNET_MORDOR]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FTSO_DAI_LP_ADDRESSES = {
|
export const FTSO_RESERVE_LP_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D",
|
[NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D",
|
||||||
[NetworkId.TESTNET_HOODI]: "0x32388605b5E83Ea79CDdC479AA9939DBCF98f59D",
|
[NetworkId.TESTNET_HOODI]: "0xf3F2cc4DaB20F09eee0d07d8456924927ceA1847",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0x53B13C4722081c405ce25c7A7629fC326A49a469",
|
[NetworkId.TESTNET_MORDOR]: "0x53B13C4722081c405ce25c7A7629fC326A49a469",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,43 +46,43 @@ export const WETH_ADDRESSES = {
|
|||||||
|
|
||||||
export const GHST_ADDRESSES = {
|
export const GHST_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
[NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||||
[NetworkId.TESTNET_HOODI]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
[NetworkId.TESTNET_HOODI]: "0x594a5CD093859B3FD48aA901d86F71a7D537834a",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
[NetworkId.TESTNET_MORDOR]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const STNK_ADDRESSES = {
|
export const STNK_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
[NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||||
[NetworkId.TESTNET_HOODI]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
[NetworkId.TESTNET_HOODI]: "0x7a7d0F214b8C03bD962B88AC0a7F543085aB817B",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
[NetworkId.TESTNET_MORDOR]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FTSO_ADDRESSES = {
|
export const FTSO_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
[NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||||
[NetworkId.TESTNET_HOODI]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
[NetworkId.TESTNET_HOODI]: "0x07427B220934B3421223969eA61a247bFA37cF03",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
[NetworkId.TESTNET_MORDOR]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DISTRIBUTOR_ADDRESSES = {
|
export const DISTRIBUTOR_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
[NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||||
[NetworkId.TESTNET_HOODI]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
[NetworkId.TESTNET_HOODI]: "0xe503d65C9574bD3C18c8d725cD346eCBFAE24b4d",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
[NetworkId.TESTNET_MORDOR]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GHOST_GOVERNANCE_ADDRESSES = {
|
export const GHOST_GOVERNANCE_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0xaf7Ad1b83C47405BB9aa96868bCFbb6D65e4C2a1",
|
[NetworkId.TESTNET_SEPOLIA]: "0xaf7Ad1b83C47405BB9aa96868bCFbb6D65e4C2a1",
|
||||||
[NetworkId.TESTNET_HOODI]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
[NetworkId.TESTNET_HOODI]: "0x0000000000000000000000000000000000000000",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
[NetworkId.TESTNET_MORDOR]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BONDING_CALCULATOR_ADDRESSES = {
|
export const BONDING_CALCULATOR_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
[NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||||
[NetworkId.TESTNET_HOODI]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
[NetworkId.TESTNET_HOODI]: "0xf13A5319aD8071e4dcE7776728Ef9Cc221A5c098",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
[NetworkId.TESTNET_MORDOR]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GATEKEEPER_ADDRESSES = {
|
export const GATEKEEPER_ADDRESSES = {
|
||||||
[NetworkId.TESTNET_SEPOLIA]: "0xd735cA07984a16911222c08411A80e24EB38869B",
|
[NetworkId.TESTNET_SEPOLIA]: "0xd735cA07984a16911222c08411A80e24EB38869B",
|
||||||
[NetworkId.TESTNET_HOODI]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
[NetworkId.TESTNET_HOODI]: "0xc9D5f50e17eeEAF39E0D0cDbbcF4B04FA4A4Bbd6",
|
||||||
[NetworkId.TESTNET_MORDOR]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
[NetworkId.TESTNET_MORDOR]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { ArrowBack } from "@mui/icons-material";
|
import { Box, Container, Skeleton, Typography, useMediaQuery } from "@mui/material";
|
||||||
import { Box, Link, Skeleton, Typography } from "@mui/material";
|
|
||||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import { Link as RouterLink, useLocation, useParams, useNavigate } from "react-router-dom";
|
import { useLocation, useParams, useNavigate } from "react-router-dom";
|
||||||
import { useAccount, useSwitchChain } from "wagmi";
|
import { useAccount, useSwitchChain } from "wagmi";
|
||||||
import { isAddress } from "viem";
|
import { isAddress } from "viem";
|
||||||
import ReactGA from "react-ga4";
|
import ReactGA from "react-ga4";
|
||||||
|
|
||||||
|
import Countdown from "../../components/Countdown/Countdown";
|
||||||
|
import Paper from "../../components/Paper/Paper";
|
||||||
import PageTitle from "../../components/PageTitle/PageTitle";
|
import PageTitle from "../../components/PageTitle/PageTitle";
|
||||||
import Metric from "../../components/Metric/Metric";
|
import Metric from "../../components/Metric/Metric";
|
||||||
|
import MetricCollection from "../../components/Metric/MetricCollection";
|
||||||
import TokenStack from "../../components/TokenStack/TokenStack";
|
import TokenStack from "../../components/TokenStack/TokenStack";
|
||||||
|
|
||||||
import BondPrice from "./components/BondPrice";
|
import BondPrice from "./components/BondPrice";
|
||||||
@ -40,6 +42,9 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
|||||||
const { network } = useParams();
|
const { network } = useParams();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||||
|
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||||
|
|
||||||
const [isSettingsOpen, setSettingsOpen] = useState(false);
|
const [isSettingsOpen, setSettingsOpen] = useState(false);
|
||||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||||
|
|
||||||
@ -102,17 +107,9 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
|
subtitle="We need this one just for alignment"
|
||||||
name={
|
name={
|
||||||
<Box display="flex" flexDirection="row" alignItems="center">
|
<Box display="flex" flexDirection="row" alignItems="center">
|
||||||
<Link component={RouterLink} to={`/${network}/bonds`}>
|
|
||||||
<Box display="flex" flexDirection="row">
|
|
||||||
<ArrowBack />
|
|
||||||
<Typography fontWeight="500" marginLeft="9.5px" marginRight="18px">
|
|
||||||
Back
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<TokenStack
|
<TokenStack
|
||||||
chainTokenNames={preparedQuoteToken.chainNames}
|
chainTokenNames={preparedQuoteToken.chainNames}
|
||||||
tokens={preparedQuoteToken.icons}
|
tokens={preparedQuoteToken.icons}
|
||||||
@ -120,7 +117,7 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
|||||||
/>
|
/>
|
||||||
<Box display="flex" flexDirection="column" ml={1} justifyContent="center" alignItems="center">
|
<Box display="flex" flexDirection="column" ml={1} justifyContent="center" alignItems="center">
|
||||||
<Typography variant="h4" fontWeight={500}>
|
<Typography variant="h4" fontWeight={500}>
|
||||||
{preparedQuoteToken.name}
|
{preparedQuoteToken.name} Bond
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@ -139,18 +136,30 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
|||||||
onDecimalsChange={event => setFormatDecimalsInner(event.currentTarget.value)}
|
onDecimalsChange={event => setFormatDecimalsInner(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box display="flex" flexDirection="row" justifyContent="space-between" width={["100%", "70%"]} mt="24px">
|
<Container
|
||||||
|
style={{
|
||||||
|
marginTop: "15px",
|
||||||
|
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||||
|
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
fullWidth
|
||||||
|
enableBackground
|
||||||
|
headerContent={
|
||||||
|
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
|
||||||
|
<Typography variant="h6">
|
||||||
|
Bond Expires
|
||||||
|
<Countdown otherwise="[EXPIRED]" targetTime={bond.duration * 1000} />
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MetricCollection>
|
||||||
<Metric
|
<Metric
|
||||||
label={`Bond Price`}
|
label={`Bond Price`}
|
||||||
metric={
|
tooltip="Some description about what the bond price is"
|
||||||
bond.isSoldOut ? (
|
metric={bond.isSoldOut ? ( "--" ) : ( <BondPrice price={bond.price.inUsd} /> )}
|
||||||
"--"
|
|
||||||
) : (
|
|
||||||
<BondPrice
|
|
||||||
price={bond.price.inUsd}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Metric
|
<Metric
|
||||||
label="Market Price"
|
label="Market Price"
|
||||||
@ -158,11 +167,14 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
|||||||
/>
|
/>
|
||||||
<Metric
|
<Metric
|
||||||
label="Discount"
|
label="Discount"
|
||||||
|
tooltip="Some description about what the discount is"
|
||||||
metric={<BondDiscount discount={bond.discount} textOnly />}
|
metric={<BondDiscount discount={bond.discount} textOnly />}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</MetricCollection>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<Box width="100%" mt="24px">
|
<Box width="100%" display="flex" justifyContent="center" alignItems="center">
|
||||||
<BondInputArea
|
<BondInputArea
|
||||||
chainId={chainId}
|
chainId={chainId}
|
||||||
bond={bond}
|
bond={bond}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import BondDiscount from "./BondDiscount";
|
|||||||
import BondVesting from "./BondVesting";
|
import BondVesting from "./BondVesting";
|
||||||
import BondSlippage from "./BondSlippage";
|
import BondSlippage from "./BondSlippage";
|
||||||
|
|
||||||
|
import { useLocalStorage } from "../../../hooks/localstorage";
|
||||||
import { purchaseBond } from "../../../hooks/bonds";
|
import { purchaseBond } from "../../../hooks/bonds";
|
||||||
import { useWarmupLength } from "../../../hooks/staking";
|
import { useWarmupLength } from "../../../hooks/staking";
|
||||||
|
|
||||||
@ -28,7 +29,32 @@ const StyledBox = styled(Box, {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const BondConfirmModal = ({
|
export const DiscountConfirmModal = ({ isOpen, isPending, onSubmit, handleConfirmClose, discount }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
maxWidth="380px"
|
||||||
|
minHeight="200px"
|
||||||
|
open={isOpen}
|
||||||
|
headerContent={<Typography variant="h4">Confirm Bond Purchase</Typography>}
|
||||||
|
onClose={!isPending && handleConfirmClose}
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection="column" justifyContent="space-between" alignItems="center" gap="10px">
|
||||||
|
<Typography>I confirm the bond purchase with a negative discount</Typography>
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="row" alignItems="center" justifyContent="center">
|
||||||
|
<Typography color={theme.colors.feedback.error} variant="h6">{discount}%</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<PrimaryButton fullWidth onClick={onSubmit} disabled={isPending} loading={isPending}>
|
||||||
|
Confirm
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BondConfirmModal = ({
|
||||||
chainId,
|
chainId,
|
||||||
bond,
|
bond,
|
||||||
slippage,
|
slippage,
|
||||||
@ -51,6 +77,8 @@ const BondConfirmModal = ({
|
|||||||
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
|
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
|
const { tryStoreBondInHistory } = useLocalStorage();
|
||||||
|
|
||||||
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup]);
|
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup]);
|
||||||
|
|
||||||
const AcknowledgeWarmupCheckbox = () => {
|
const AcknowledgeWarmupCheckbox = () => {
|
||||||
@ -104,21 +132,19 @@ const BondConfirmModal = ({
|
|||||||
const one = BigInt(shares * 100);
|
const one = BigInt(shares * 100);
|
||||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||||
const bigIntSlippage = one + BigInt(Math.round(floatSlippage * shares));
|
const bigIntSlippage = one + BigInt(Math.round(floatSlippage * shares));
|
||||||
|
|
||||||
const maxPrice = bond.price.inBaseToken._value * bigIntSlippage / one;
|
const maxPrice = bond.price.inBaseToken._value * bigIntSlippage / one;
|
||||||
const referral = import.meta.env.VITE_APP_REFERRAL_ADDRESS;
|
|
||||||
|
|
||||||
await purchaseBond({
|
const txHash = await purchaseBond({
|
||||||
chainId,
|
chainId,
|
||||||
bondId: bond.id,
|
bondId: bond.id,
|
||||||
amount: spendAmountValue._value.toBigInt(),
|
amount: spendAmountValue._value.toBigInt(),
|
||||||
maxPrice,
|
maxPrice,
|
||||||
user: recipientAddress,
|
user: recipientAddress,
|
||||||
sender,
|
sender,
|
||||||
referral,
|
|
||||||
isNative
|
isNative
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tryStoreBondInHistory(chainId, sender, txHash);
|
||||||
setIsPending(false);
|
setIsPending(false);
|
||||||
handleConfirmCloseMaster();
|
handleConfirmCloseMaster();
|
||||||
}
|
}
|
||||||
@ -212,5 +238,3 @@ const BondConfirmModal = ({
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BondConfirmModal;
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
|
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
|
||||||
import { Box, Checkbox, FormControlLabel, useMediaQuery } from "@mui/material";
|
import { Box, Container, Checkbox, FormControlLabel, Typography, useMediaQuery } from "@mui/material";
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
|
||||||
import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses";
|
import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses";
|
||||||
import { shorten, formatNumber, formatCurrency } from "../../../helpers";
|
import { shorten, formatNumber, formatCurrency } from "../../../helpers";
|
||||||
@ -9,10 +10,11 @@ import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
|||||||
|
|
||||||
import BondDiscount from "./BondDiscount";
|
import BondDiscount from "./BondDiscount";
|
||||||
import BondVesting from "./BondVesting";
|
import BondVesting from "./BondVesting";
|
||||||
import BondConfirmModal from "./BondConfirmModal";
|
import { BondConfirmModal, DiscountConfirmModal } from "./BondConfirmModal";
|
||||||
|
|
||||||
import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard";
|
import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard";
|
||||||
import { PrimaryButton } from "../../../components/Button";
|
import { PrimaryButton } from "../../../components/Button";
|
||||||
|
import GhostStyledIcon from "../../../components/Icon/GhostIcon";
|
||||||
import SwapCard from "../../../components/Swap/SwapCard";
|
import SwapCard from "../../../components/Swap/SwapCard";
|
||||||
import SwapCollection from "../../../components/Swap/SwapCollection";
|
import SwapCollection from "../../../components/Swap/SwapCollection";
|
||||||
import TokenStack from "../../../components/TokenStack/TokenStack";
|
import TokenStack from "../../../components/TokenStack/TokenStack";
|
||||||
@ -33,6 +35,7 @@ const BondInputArea = ({
|
|||||||
preparedQuoteToken,
|
preparedQuoteToken,
|
||||||
connect
|
connect
|
||||||
}) => {
|
}) => {
|
||||||
|
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||||
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
|
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
|
||||||
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
@ -45,7 +48,9 @@ const BondInputArea = ({
|
|||||||
|
|
||||||
const [amount, setAmount] = useState("");
|
const [amount, setAmount] = useState("");
|
||||||
const [checked, setChecked] = useState(false);
|
const [checked, setChecked] = useState(false);
|
||||||
|
const [discountConfirmed, setDiscountConfirmed] = useState(false);
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
const [discountConfirmOpen, setDiscountConfirmOpen] = useState(false);
|
||||||
|
|
||||||
const parsedAmount = useMemo(() => {
|
const parsedAmount = useMemo(() => {
|
||||||
return new DecimalBigNumber(amount, bond.quoteToken.decimals);
|
return new DecimalBigNumber(amount, bond.quoteToken.decimals);
|
||||||
@ -60,12 +65,24 @@ const BondInputArea = ({
|
|||||||
return new DecimalBigNumber("0").gt(bond.discount);
|
return new DecimalBigNumber("0").gt(bond.discount);
|
||||||
}, [bond]);
|
}, [bond]);
|
||||||
|
|
||||||
|
const setModalOpen = () => {
|
||||||
|
if (showDisclaimer && !discountConfirmOpen && !discountConfirmed) setDiscountConfirmOpen(true);
|
||||||
|
else setConfirmOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
const handleConfirmClose = () => {
|
const handleConfirmClose = () => {
|
||||||
setAmount("");
|
setAmount("");
|
||||||
setConfirmOpen(false);
|
setConfirmOpen(false);
|
||||||
|
setDiscountConfirmOpen(false);
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmDiscount = () => {
|
||||||
|
setConfirmOpen(false);
|
||||||
|
setDiscountConfirmOpen(false);
|
||||||
|
setDiscountConfirmed(true);
|
||||||
|
}
|
||||||
|
|
||||||
const setMax = useCallback(() => {
|
const setMax = useCallback(() => {
|
||||||
if (!balance) return;
|
if (!balance) return;
|
||||||
|
|
||||||
@ -95,15 +112,56 @@ const BondInputArea = ({
|
|||||||
}, [amountInBaseToken, baseTokenString, balance, parsedAmount]);
|
}, [amountInBaseToken, baseTokenString, balance, parsedAmount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box minHeight="calc(100vh - 210px)" display="flex" flexDirection="column" justifyContent="center">
|
<Box sx={{ width: "100%", maxWidth: "420px", mt: "15px" }}>
|
||||||
<Box display="flex" flexDirection="row" width="100%" justifyContent="center" mt="10px">
|
<Paper
|
||||||
<Box display="flex" flexDirection="column" width="100%" maxWidth="476px">
|
fullWidth
|
||||||
<Box mb="21px">
|
enableBackground
|
||||||
|
topRight={
|
||||||
|
<GhostStyledIcon
|
||||||
|
component={SettingsIcon}
|
||||||
|
viewBox="0 0 23 23"
|
||||||
|
style={{ display: "flex", alignItems: "center", cursor: "pointer" }}
|
||||||
|
onClick={handleSettingsOpen}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
headerContent={
|
||||||
|
<Typography variant="h6">Bond</Typography>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DiscountConfirmModal
|
||||||
|
isOpen={discountConfirmOpen}
|
||||||
|
onSubmit={() => confirmDiscount()}
|
||||||
|
handleConfirmClose={() => handleConfirmClose()}
|
||||||
|
discount={bond
|
||||||
|
? formatNumber(Number(bond.discount.mul(new DecimalBigNumber("100"))).toString(), 2)
|
||||||
|
: "Unknown"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BondConfirmModal
|
||||||
|
chainId={chainId}
|
||||||
|
bond={bond}
|
||||||
|
slippage={slippage}
|
||||||
|
recipientAddress={recipientAddress}
|
||||||
|
spendAmountValue={parsedAmount}
|
||||||
|
spendAmount={formatNumber(parsedAmount, formatDecimals)}
|
||||||
|
receiveAmount={formatNumber(amountInBaseToken, formatDecimals)}
|
||||||
|
bondQuoteTokenName={preparedQuoteToken.name}
|
||||||
|
bondQuoteTokenIcons={preparedQuoteToken.icons}
|
||||||
|
bondQuoteTokenChainNames={preparedQuoteToken.chainNames}
|
||||||
|
isNative={preparedQuoteToken.address === undefined}
|
||||||
|
handleSettingsOpen={handleSettingsOpen}
|
||||||
|
isOpen={confirmOpen}
|
||||||
|
sender={address}
|
||||||
|
handleConfirmClose={() => handleConfirmClose()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="center" gap="15px">
|
||||||
<SwapCollection
|
<SwapCollection
|
||||||
iconNotNeeded
|
iconNotNeeded
|
||||||
UpperSwapCard={
|
UpperSwapCard={
|
||||||
<SwapCard
|
<SwapCard
|
||||||
maxWidth="476px"
|
maxWidth="390px"
|
||||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||||
id="from"
|
id="from"
|
||||||
token={
|
token={
|
||||||
@ -123,7 +181,7 @@ const BondInputArea = ({
|
|||||||
/>}
|
/>}
|
||||||
LowerSwapCard={
|
LowerSwapCard={
|
||||||
<SwapCard
|
<SwapCard
|
||||||
maxWidth="476px"
|
maxWidth="390px"
|
||||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||||
id="to"
|
id="to"
|
||||||
token={
|
token={
|
||||||
@ -139,6 +197,63 @@ const BondInputArea = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
sx={{
|
||||||
|
"& .DataRow-root": {
|
||||||
|
margin: "0px !important",
|
||||||
|
},
|
||||||
|
"& .MuiTypography-root": {
|
||||||
|
fontSize: "13px !important",
|
||||||
|
},
|
||||||
|
"& span": {
|
||||||
|
fontSize: "13px !important",
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DataRow
|
||||||
|
title={"Bond Payout"}
|
||||||
|
balance={
|
||||||
|
<span>
|
||||||
|
{formatCurrency(amountInBaseToken, formatDecimals, ftsoSymbol)}
|
||||||
|
{" "}
|
||||||
|
{!!currentIndex && (
|
||||||
|
<span>
|
||||||
|
(≈{formatCurrency(amountInBaseToken.div(currentIndex), formatDecimals, ghstSymbol)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
tooltip={`Total amount of ${ftsoSymbol} you will receive from this bond purchase`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataRow
|
||||||
|
title="Max Payout"
|
||||||
|
tooltip={`Maximum ${ftsoSymbol} that can be purchased in a single transaction. Prevents whales from buying entire bond supply at once.`}
|
||||||
|
balance={
|
||||||
|
<span>
|
||||||
|
{bond.baseToken.tokenAddress.toUpperCase() === bond.quoteToken.quoteTokenAddress.toUpperCase()
|
||||||
|
? `${formatCurrency(baseTokenString, formatDecimals, ftsoSymbol)}`
|
||||||
|
: `${formatCurrency(baseTokenString, formatDecimals, ftsoSymbol)} (≈${formatCurrency(baseTokenString.div(currentIndex), formatDecimals, ghstSymbol)})`}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataRow
|
||||||
|
title={`Vesting Term`}
|
||||||
|
balance={<span><BondVesting vesting={bond.vesting} /></span>}
|
||||||
|
tooltip={"Time until bond fully vests and becomes claimable. Vesting period aligns bond buyer with the protocol by encouraging longer-term holding."}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{recipientAddress !== address && (
|
||||||
|
<DataRow
|
||||||
|
title={`Recipient`}
|
||||||
|
balance={<span>{shorten(recipientAddress)}</span>}
|
||||||
|
tooltip="The address that will receive the bond. To change it from your connected account, update the settings via the gear icon."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TokenAllowanceGuard
|
<TokenAllowanceGuard
|
||||||
@ -161,96 +276,18 @@ const BondInputArea = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{showDisclaimer && (
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={checked}
|
|
||||||
onChange={event => setChecked(event.target.checked)}
|
|
||||||
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
|
|
||||||
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="I understand that I'm buying a negative discount bond"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={incorrectInputAmount || bond.isSoldOut || (showDisclaimer && !checked)}
|
disabled={incorrectInputAmount || bond.isSoldOut || (!showDisclaimer && !checked)}
|
||||||
onClick={() => setConfirmOpen(true)}
|
onClick={() => setModalOpen(true)}
|
||||||
>
|
>
|
||||||
Bond
|
Bond
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</TokenAllowanceGuard>
|
</TokenAllowanceGuard>
|
||||||
|
|
||||||
<Paper style={{ marginBottom: "7px", marginTop: "21px" }} fullWidth enableBackground>
|
|
||||||
<Box mt="24px">
|
|
||||||
<DataRow
|
|
||||||
title={"You Will Get"}
|
|
||||||
balance={
|
|
||||||
<span>
|
|
||||||
{formatCurrency(amountInBaseToken, formatDecimals, ftsoSymbol)}
|
|
||||||
{" "}
|
|
||||||
{!!currentIndex && (
|
|
||||||
<span>
|
|
||||||
(≈{formatCurrency(amountInBaseToken.div(currentIndex), formatDecimals, ghstSymbol)})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
tooltip={`Total amount of ${ftsoSymbol} you will receive from this bond purchase`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataRow
|
|
||||||
title="Max You Can Buy"
|
|
||||||
tooltip={`Maximum ${ftsoSymbol} that can be purchased in a single transaction. Prevents whales from buying entire bond supply at once.`}
|
|
||||||
balance={
|
|
||||||
<span>
|
|
||||||
{bond.baseToken.tokenAddress.toUpperCase() === bond.quoteToken.quoteTokenAddress.toUpperCase()
|
|
||||||
? `${formatCurrency(baseTokenString, formatDecimals, ftsoSymbol)}`
|
|
||||||
: `${formatCurrency(baseTokenString, formatDecimals, ftsoSymbol)} (≈${formatCurrency(baseTokenString.div(currentIndex), formatDecimals, ghstSymbol)})`}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataRow
|
|
||||||
title="Discount"
|
|
||||||
balance={<BondDiscount discount={bond.discount} textOnly />}
|
|
||||||
tooltip={`The discount (or premium) between ${ftsoSymbol} market price and bond price. Higher discount = better deal for bond buyers.`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataRow
|
|
||||||
title={`Vesting Term`}
|
|
||||||
balance={<BondVesting vesting={bond.vesting} />}
|
|
||||||
tooltip={"Time until bond fully vests and becomes claimable. Vesting period aligns bond buyer with the protocol by encouraging longer-term holding."}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{recipientAddress !== address && (
|
|
||||||
<DataRow title={`Recipient`} balance={shorten(recipientAddress)} />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)
|
||||||
<BondConfirmModal
|
};
|
||||||
chainId={chainId}
|
|
||||||
bond={bond}
|
|
||||||
slippage={slippage}
|
|
||||||
recipientAddress={recipientAddress}
|
|
||||||
spendAmountValue={parsedAmount}
|
|
||||||
spendAmount={formatNumber(parsedAmount, formatDecimals)}
|
|
||||||
receiveAmount={formatNumber(amountInBaseToken, formatDecimals)}
|
|
||||||
bondQuoteTokenName={preparedQuoteToken.name}
|
|
||||||
bondQuoteTokenIcons={preparedQuoteToken.icons}
|
|
||||||
bondQuoteTokenChainNames={preparedQuoteToken.chainNames}
|
|
||||||
isNative={preparedQuoteToken.address === undefined}
|
|
||||||
handleSettingsOpen={handleSettingsOpen}
|
|
||||||
isOpen={confirmOpen}
|
|
||||||
sender={address}
|
|
||||||
handleConfirmClose={() => handleConfirmClose()}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BondInputArea;
|
export default BondInputArea;
|
||||||
|
|||||||
@ -75,6 +75,7 @@ const BreakoutModal = ({ chainId, address }) => {
|
|||||||
if (isClaimBondOpened && warmupPeriod > 0) return "Bond in Warm-up"
|
if (isClaimBondOpened && warmupPeriod > 0) return "Bond in Warm-up"
|
||||||
}, [isStakingOpened, isClaimBondOpened, warmupPeriod]);
|
}, [isStakingOpened, isClaimBondOpened, warmupPeriod]);
|
||||||
|
|
||||||
|
// TODO: make it global helpers
|
||||||
const bridgeNumbers = useMemo(() => {
|
const bridgeNumbers = useMemo(() => {
|
||||||
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
||||||
const number = 1 + connectedNetworks * 3;
|
const number = 1 + connectedNetworks * 3;
|
||||||
|
|||||||
75
src/containers/Bridge/BridgeGhostAddress.jsx
Normal file
75
src/containers/Bridge/BridgeGhostAddress.jsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
|
||||||
|
import { FormControl, OutlinedInput } from "@mui/material";
|
||||||
|
|
||||||
|
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, isCard }) => {
|
||||||
|
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]);
|
||||||
|
|
||||||
|
if (isCard) {
|
||||||
|
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%"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl variant="outlined" color="primary" fullWidth>
|
||||||
|
<OutlinedInput
|
||||||
|
inputProps={{ "data-testid": "custom-destination-input" }}
|
||||||
|
id="custom-destination-input"
|
||||||
|
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}
|
||||||
|
endAdornment={receiver
|
||||||
|
? <GhostStyledIcon color="success" viewBox="0 0 25 25" component={CheckIcon} />
|
||||||
|
: inputFocused && !receiver
|
||||||
|
? <GhostStyledIcon color="error" viewBox="0 0 25 25" component={CancelIcon} />
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
505
src/containers/Bridge/BridgeIn.jsx
Normal file
505
src/containers/Bridge/BridgeIn.jsx
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
import { useEffect, useState, useMemo, useCallback, useRef } 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 latestRef = useRef({
|
||||||
|
chainId,
|
||||||
|
address,
|
||||||
|
receiverHex,
|
||||||
|
preparedAmount,
|
||||||
|
tryStoreInHistory,
|
||||||
|
ghstBalanceRefetch
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestRef.current = {
|
||||||
|
chainId,
|
||||||
|
address,
|
||||||
|
receiverHex,
|
||||||
|
preparedAmount,
|
||||||
|
tryStoreInHistory,
|
||||||
|
ghstBalanceRefetch
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const setFormatDecimals = useCallback((value) => {
|
||||||
|
if (Number(value) <= 17) {
|
||||||
|
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
|
||||||
|
}
|
||||||
|
}, [chainId, address]);
|
||||||
|
|
||||||
|
const startGhostBridge = async () => {
|
||||||
|
setIsPending(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
chainId: currentChainId,
|
||||||
|
address: currentAddress,
|
||||||
|
receiverHex: currentReceiverHex,
|
||||||
|
preparedAmount: currentPreparedAmount,
|
||||||
|
tryStoreInHistory: currentTryStoreInHistory,
|
||||||
|
ghstBalanceRefetch: currentGhstBalanceRefetch
|
||||||
|
} = latestRef.current;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const txHash = await ghost(
|
||||||
|
currentChainId,
|
||||||
|
currentAddress,
|
||||||
|
currentReceiverHex,
|
||||||
|
currentPreparedAmount._value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (txHash) {
|
||||||
|
currentTryStoreInHistory(
|
||||||
|
currentChainId,
|
||||||
|
currentAddress,
|
||||||
|
CONSTANTS.bridgeIn.key,
|
||||||
|
txHash,
|
||||||
|
CONSTANTS.bridgeIn.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await currentGhstBalanceRefetch();
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
{isPending ? "Pending..." : "Bridge-In"}
|
||||||
|
</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">
|
||||||
|
Custom recipient address of bridged assets
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -41,12 +41,17 @@ import PoolContainer from "./PoolContainer";
|
|||||||
import SwapContainer from "./SwapContainer";
|
import SwapContainer from "./SwapContainer";
|
||||||
import TokenModal from "./TokenModal";
|
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 Dex = ({ chainId, address, connect, config }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const pathname = useParams();
|
const pathname = useParams();
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
const { useStorageValue, setStorageValue, CONSTANTS } = useLocalStorage();
|
||||||
|
|
||||||
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||||
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||||
@ -59,12 +64,12 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
const [topTokenListOpen, setTopTokenListOpen] = useState(false);
|
const [topTokenListOpen, setTopTokenListOpen] = useState(false);
|
||||||
const [bottomTokenListOpen, setBottomTokenListOpen] = useState(false);
|
const [bottomTokenListOpen, setBottomTokenListOpen] = useState(false);
|
||||||
|
|
||||||
const [secondsToWait, setSecondsToWait] = useState(() => getStorageValue(chainId, address, "dex-deadline", "60"));
|
const { data: secondsToWait } = useStorageValue(chainId, address, DEADLINE_PREFIX, "60");
|
||||||
const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "dex-slippage", "5"));
|
const { data: slippage } = useStorageValue(chainId, address, SLIPPAGE_PREFIX, "5");
|
||||||
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "dex-decimals", "5"));
|
const { data: formatDecimals } = useStorageValue(chainId, address, DECIMALS_PREFIX, "5");
|
||||||
const [actualDestinationAddress, setActualDestinationAddress] = useState(() => getStorageValue(chainId, address, "dex-destination", address));
|
const { data: actualDestinationAddress } = useStorageValue(chainId, address, DESTINATION_PREFIX, address);
|
||||||
const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress);
|
|
||||||
|
|
||||||
|
const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress);
|
||||||
const [tokenAddressTop, setTokenAddressTop] = useState(EMPTY_ADDRESS);
|
const [tokenAddressTop, setTokenAddressTop] = useState(EMPTY_ADDRESS);
|
||||||
const [tokenAddressBottom, setTokenAddressBottom] = useState(FTSO_ADDRESSES[chainId]);
|
const [tokenAddressBottom, setTokenAddressBottom] = useState(FTSO_ADDRESSES[chainId]);
|
||||||
|
|
||||||
@ -192,23 +197,20 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
return dexInfo ? dexInfo.isFork : false;
|
return dexInfo ? dexInfo.isFork : false;
|
||||||
}, [chainId, pathname])
|
}, [chainId, pathname])
|
||||||
|
|
||||||
const setSlippageInner = useCallback((value) => {
|
const setSlippage = useCallback((value) => {
|
||||||
const maybeValue = parseFloat(value);
|
const maybeValue = parseFloat(value);
|
||||||
if (!maybeValue || parseFloat(value) <= 100) {
|
if (!maybeValue || parseFloat(value) <= 100) {
|
||||||
setSlippage(value);
|
setStorageValue(chainId, address, SLIPPAGE_PREFIX, value);
|
||||||
setStorageValue(chainId, address, "dex-slippage", value);
|
|
||||||
}
|
}
|
||||||
}, [chainId, address]);
|
}, [chainId, address]);
|
||||||
|
|
||||||
const setSecondsToWaitInner = useCallback((value) => {
|
const setSecondsToWait = useCallback((value) => {
|
||||||
setSecondsToWait(value);
|
setStorageValue(chainId, address, DEADLINE_PREFIX, value);
|
||||||
setStorageValue(chainId, address, "dex-deadline", value);
|
|
||||||
}, [chainId, address]);
|
}, [chainId, address]);
|
||||||
|
|
||||||
const setFormatDecimalsInner = useCallback((value) => {
|
const setFormatDecimals = useCallback((value) => {
|
||||||
if (Number(value) <= 17) {
|
if (Number(value) <= 17) {
|
||||||
setFormatDecimals(value);
|
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
|
||||||
setStorageValue(chainId, address, "dex-decimals", value);
|
|
||||||
}
|
}
|
||||||
}, [chainId, address]);
|
}, [chainId, address]);
|
||||||
|
|
||||||
@ -216,8 +218,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
const cleanedValue = value.trim();
|
const cleanedValue = value.trim();
|
||||||
setDestinationAddress(value);
|
setDestinationAddress(value);
|
||||||
if (isAddress(cleanedValue)) {
|
if (isAddress(cleanedValue)) {
|
||||||
setActualDestinationAddress(value);
|
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
|
||||||
setStorageValue(chainId, address, "dex-destination", value);
|
|
||||||
}
|
}
|
||||||
}, [chainId, address]);
|
}, [chainId, address]);
|
||||||
|
|
||||||
@ -278,7 +279,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
id="slippage-dex"
|
id="slippage-dex"
|
||||||
value={slippage}
|
value={slippage}
|
||||||
onChange={event => setSlippageInner(event.currentTarget.value)}
|
onChange={event => setSlippage(event.currentTarget.value)}
|
||||||
endAdornment="%"
|
endAdornment="%"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -291,7 +292,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box mt="32px">
|
<Box mt="32px">
|
||||||
<InputLabel htmlFor="recipient">Transaction deadline</InputLabel>
|
<InputLabel htmlFor="dex-deadline">Transaction deadline</InputLabel>
|
||||||
<Box mt="8px">
|
<Box mt="8px">
|
||||||
<FormControl variant="outlined" color="primary" fullWidth>
|
<FormControl variant="outlined" color="primary" fullWidth>
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
@ -299,7 +300,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
type="number"
|
type="number"
|
||||||
id="seconds-to-wait"
|
id="seconds-to-wait"
|
||||||
value={secondsToWait}
|
value={secondsToWait}
|
||||||
onChange={event => setSecondsToWaitInner(event.currentTarget.value)}
|
onChange={event => setSecondsToWait(event.currentTarget.value)}
|
||||||
endAdornment="seconds"
|
endAdornment="seconds"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -312,7 +313,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box mt="32px">
|
<Box mt="32px">
|
||||||
<InputLabel htmlFor="recipient">Decimal representation</InputLabel>
|
<InputLabel htmlFor="dex-decimals">Decimal representation</InputLabel>
|
||||||
<Box mt="8px">
|
<Box mt="8px">
|
||||||
<FormControl variant="outlined" color="primary" fullWidth>
|
<FormControl variant="outlined" color="primary" fullWidth>
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
@ -320,7 +321,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
type="number"
|
type="number"
|
||||||
id="decimals-to-wait"
|
id="decimals-to-wait"
|
||||||
value={formatDecimals}
|
value={formatDecimals}
|
||||||
onChange={event => setFormatDecimalsInner(event.currentTarget.value)}
|
onChange={event => setFormatDecimals(event.currentTarget.value)}
|
||||||
endAdornment="decimals"
|
endAdornment="decimals"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -333,7 +334,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box mt="32px">
|
<Box mt="32px">
|
||||||
<InputLabel htmlFor="recipient">
|
<InputLabel htmlFor="dex-recipient">
|
||||||
{`${actualDestinationAddress ? "Custom" : "Default"} destination address`}
|
{`${actualDestinationAddress ? "Custom" : "Default"} destination address`}
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<Box mt="8px">
|
<Box mt="8px">
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
addLiquidity,
|
addLiquidity,
|
||||||
addLiquidityETH,
|
addLiquidityETH,
|
||||||
} from "../../hooks/uniswapv2";
|
} from "../../hooks/uniswapv2";
|
||||||
|
import { useLocalStorage } from "../../hooks/localstorage";
|
||||||
|
|
||||||
const PoolContainer = ({
|
const PoolContainer = ({
|
||||||
chainSymbol,
|
chainSymbol,
|
||||||
@ -37,6 +38,7 @@ const PoolContainer = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isSmallScreen = useMediaQuery("(max-width: 456px)");
|
const isSmallScreen = useMediaQuery("(max-width: 456px)");
|
||||||
|
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
|
||||||
|
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [amountTop, setAmountTop] = useState("");
|
const [amountTop, setAmountTop] = useState("");
|
||||||
@ -165,7 +167,12 @@ const PoolContainer = ({
|
|||||||
const addLiquidityInner = async () => {
|
const addLiquidityInner = async () => {
|
||||||
setIsPending(true);
|
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 shares = 100000;
|
||||||
const one = BigInt(shares * 100);
|
const one = BigInt(shares * 100);
|
||||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||||
@ -191,12 +198,16 @@ const PoolContainer = ({
|
|||||||
deadline,
|
deadline,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let txHash;
|
||||||
|
|
||||||
if (topIsNative || bottomIsNative) {
|
if (topIsNative || bottomIsNative) {
|
||||||
await addLiquidityETH(params)
|
txHash = await addLiquidityETH(params)
|
||||||
} else {
|
} else {
|
||||||
await addLiquidity(params);
|
txHash = await addLiquidity(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.dex.key, txHash, CONSTANTS.dex.add);
|
||||||
|
|
||||||
await balanceRefetchTop();
|
await balanceRefetchTop();
|
||||||
await balanceRefetchBottom();
|
await balanceRefetchBottom();
|
||||||
await pairAddressRefetch();
|
await pairAddressRefetch();
|
||||||
@ -215,7 +226,7 @@ const PoolContainer = ({
|
|||||||
}, [pairAddress])
|
}, [pairAddress])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box maxWidth="356px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
|
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
|
||||||
<SwapCollection
|
<SwapCollection
|
||||||
UpperSwapCard={
|
UpperSwapCard={
|
||||||
<SwapCard
|
<SwapCard
|
||||||
@ -256,7 +267,7 @@ const PoolContainer = ({
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap="0px"
|
gap="0px"
|
||||||
maxWidth="356px"
|
maxWidth="390px"
|
||||||
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
|
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
|
||||||
>
|
>
|
||||||
<Box width="100%" display="flex" justifyContent="space-between">
|
<Box width="100%" display="flex" justifyContent="space-between">
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
swapExactETHForTokens,
|
swapExactETHForTokens,
|
||||||
swapExactTokensForETH,
|
swapExactTokensForETH,
|
||||||
} from "../../hooks/uniswapv2";
|
} from "../../hooks/uniswapv2";
|
||||||
|
import { useLocalStorage } from "../../hooks/localstorage";
|
||||||
|
|
||||||
import { EMPTY_ADDRESS } from "../../constants/addresses";
|
import { EMPTY_ADDRESS } from "../../constants/addresses";
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ const SwapContainer = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isSmallScreen = useMediaQuery("(max-width: 456px)");
|
const isSmallScreen = useMediaQuery("(max-width: 456px)");
|
||||||
|
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
|
||||||
|
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [amountBottom, setAmountBottom] = useState("");
|
const [amountBottom, setAmountBottom] = useState("");
|
||||||
@ -161,7 +163,12 @@ const SwapContainer = ({
|
|||||||
const swapTokens = async () => {
|
const swapTokens = async () => {
|
||||||
setIsPending(true);
|
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 shares = 100000;
|
||||||
const one = BigInt(shares * 100);
|
const one = BigInt(shares * 100);
|
||||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||||
@ -173,10 +180,12 @@ const SwapContainer = ({
|
|||||||
const amountBDesired = BigInt(Math.round(parseFloat(amountBottom) * Math.pow(10, balanceBottom._decimals)));
|
const amountBDesired = BigInt(Math.round(parseFloat(amountBottom) * Math.pow(10, balanceBottom._decimals)));
|
||||||
const amountBMin = amountBDesired * bigIntSlippage / one;
|
const amountBMin = amountBDesired * bigIntSlippage / one;
|
||||||
|
|
||||||
|
let txHash;
|
||||||
|
|
||||||
if (isWrapping) {
|
if (isWrapping) {
|
||||||
await depositNative(chainId, address, amountADesired);
|
txHash = await depositNative(chainId, address, amountADesired);
|
||||||
} else if (isUnwrapping) {
|
} else if (isUnwrapping) {
|
||||||
await withdrawWeth(chainId, address, amountADesired);
|
txHash = await withdrawWeth(chainId, address, amountADesired);
|
||||||
} else {
|
} else {
|
||||||
const params = {
|
const params = {
|
||||||
chainId,
|
chainId,
|
||||||
@ -190,14 +199,16 @@ const SwapContainer = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (topIsNative) {
|
if (topIsNative) {
|
||||||
await swapExactETHForTokens(params)
|
txHash = await swapExactETHForTokens(params)
|
||||||
} else if (bottomIsNative) {
|
} else if (bottomIsNative) {
|
||||||
await swapExactTokensForETH(params)
|
txHash = await swapExactTokensForETH(params)
|
||||||
} else {
|
} else {
|
||||||
await swapExactTokensForTokens(params);
|
txHash = await swapExactTokensForTokens(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.dex.key, txHash, CONSTANTS.dex.swap);
|
||||||
|
|
||||||
await balanceRefetchTop();
|
await balanceRefetchTop();
|
||||||
await balanceRefetchBottom();
|
await balanceRefetchBottom();
|
||||||
await pairReservesRefetch();
|
await pairReservesRefetch();
|
||||||
@ -207,11 +218,11 @@ const SwapContainer = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box maxWidth="356px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
|
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
|
||||||
<SwapCollection
|
<SwapCollection
|
||||||
UpperSwapCard={
|
UpperSwapCard={
|
||||||
<SwapCard
|
<SwapCard
|
||||||
maxWidth="356px"
|
maxWidth="390px"
|
||||||
id="from"
|
id="from"
|
||||||
token={tokenNameTop}
|
token={tokenNameTop}
|
||||||
chainTokenName={tokenNameTop !== chainSymbol && chainSymbol}
|
chainTokenName={tokenNameTop !== chainSymbol && chainSymbol}
|
||||||
@ -230,7 +241,7 @@ const SwapContainer = ({
|
|||||||
}
|
}
|
||||||
LowerSwapCard={
|
LowerSwapCard={
|
||||||
<SwapCard
|
<SwapCard
|
||||||
maxWidth="356px"
|
maxWidth="390px"
|
||||||
id="to"
|
id="to"
|
||||||
token={tokenNameBottom}
|
token={tokenNameBottom}
|
||||||
chainTokenName={tokenNameBottom !== chainSymbol && chainSymbol}
|
chainTokenName={tokenNameBottom !== chainSymbol && chainSymbol}
|
||||||
@ -252,7 +263,7 @@ const SwapContainer = ({
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap="0px"
|
gap="0px"
|
||||||
maxWidth="356px"
|
maxWidth="390px"
|
||||||
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
|
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
|
||||||
>
|
>
|
||||||
<Box width="100%" display="flex" justifyContent="space-between">
|
<Box width="100%" display="flex" justifyContent="space-between">
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { ArgumentsWrapper, BooleanTrigger, ArgumentInput, ParsedCell } from "./i
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
RESERVE_ADDRESSES,
|
RESERVE_ADDRESSES,
|
||||||
FTSO_DAI_LP_ADDRESSES,
|
FTSO_RESERVE_LP_ADDRESSES,
|
||||||
BOND_DEPOSITORY_ADDRESSES,
|
BOND_DEPOSITORY_ADDRESSES,
|
||||||
} from "../../../../constants/addresses";
|
} from "../../../../constants/addresses";
|
||||||
import { abi as DepositoryAbi } from "../../../../abi/GhostBondDepository.json";
|
import { abi as DepositoryAbi } from "../../../../abi/GhostBondDepository.json";
|
||||||
@ -114,7 +114,7 @@ export const CreateBondSteps = ({ nativeCurrency, ftsoSymbol, chainId, toInitial
|
|||||||
}, [step, tokenAddress, capacity, initialPrice, debtBuffer, depositInterval, tuneInterval]);
|
}, [step, tokenAddress, capacity, initialPrice, debtBuffer, depositInterval, tuneInterval]);
|
||||||
|
|
||||||
const possibleTokens = [
|
const possibleTokens = [
|
||||||
{ value: FTSO_DAI_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_DAI_LP_ADDRESSES[chainId])}` },
|
{ value: FTSO_RESERVE_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_RESERVE_LP_ADDRESSES[chainId])}` },
|
||||||
{ value: RESERVE_ADDRESSES[chainId], symbol: reserveSymbol, label: `${reserveSymbol}: ${shorten(RESERVE_ADDRESSES[chainId])}` },
|
{ value: RESERVE_ADDRESSES[chainId], symbol: reserveSymbol, label: `${reserveSymbol}: ${shorten(RESERVE_ADDRESSES[chainId])}` },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
660
src/containers/History/BridgeInModal.jsx
Normal file
660
src/containers/History/BridgeInModal.jsx
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
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 { ss58Encode } from "@polkadot-labs/hdkd-helpers";
|
||||||
|
import { hexToBytes, bytesToHex } from "viem";
|
||||||
|
import { blake2AsU8a } from "@polkadot/util-crypto";
|
||||||
|
|
||||||
|
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 { executePapiBareTransaction } from "../../hooks/helpers";
|
||||||
|
import { useWeaverRoot, useWeaverProof, useWeaverSlotValues, useWeaverMaximumSlots } 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 [isPending, setIsPending] = useState(false);
|
||||||
|
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 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 functionArgs = useMemo(() => {
|
||||||
|
if (!txData || !chainProperties) return null;
|
||||||
|
const decoded = decodeFunctionData({ abi: GatekeeperAbi, data: txData.input });
|
||||||
|
const prefix = chainProperties.properties.ss58Format;
|
||||||
|
const receiverRaw = decoded?.args.at(0);
|
||||||
|
const receiver = receiverRaw ? ss58Encode(receiverRaw, prefix) : null;
|
||||||
|
return { receiver, amount: decoded?.args.at(1) };
|
||||||
|
}, [txData, chainProperties]);
|
||||||
|
|
||||||
|
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: 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 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]);
|
||||||
|
|
||||||
|
const { maximumSlots } = useWeaverMaximumSlots(chainId);
|
||||||
|
const { merkleRoot: evmMerkleRoot } = useWeaverRoot(chainId, loomState?.at_block, bridgeInInfo?.args);
|
||||||
|
const { merkleProof } = useWeaverProof(chainId, loomState?.at_block, bridgeInInfo?.args);
|
||||||
|
const { slotValues } = useWeaverSlotValues(chainId, loomState?.at_block, bridgeInInfo?.args);
|
||||||
|
|
||||||
|
const localString = new Date(historyRecord.submitted).toLocaleString();
|
||||||
|
|
||||||
|
const bridgedAmount = useMemo(() => {
|
||||||
|
if (!functionArgs) return null;
|
||||||
|
return new DecimalBigNumber(
|
||||||
|
functionArgs.amount,
|
||||||
|
chainProperties?.properties.tokenDecimals ?? 18
|
||||||
|
)
|
||||||
|
}, [functionArgs, chainProperties]);
|
||||||
|
|
||||||
|
const estimatedFee = useMemo(() => {
|
||||||
|
if (!networkData) return null;
|
||||||
|
return networkData.incoming_share / 10000000;
|
||||||
|
}, [networkData]);
|
||||||
|
|
||||||
|
const estimatedReceived = useMemo(() => {
|
||||||
|
if (!functionArgs) return null;
|
||||||
|
|
||||||
|
const hundredPercent = 1000000000;
|
||||||
|
const incomingShare = networkData ? networkData.incoming_share : 0;
|
||||||
|
const result = hundredPercent - incomingShare;
|
||||||
|
|
||||||
|
const value = Number(functionArgs.amount) * result / hundredPercent;
|
||||||
|
return new DecimalBigNumber(value, chainProperties?.properties.tokenDecimals ?? 18)
|
||||||
|
}, [functionArgs, 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]);
|
||||||
|
|
||||||
|
const rootIncluded = useMemo(() => {
|
||||||
|
if (!loomState || !receipt) return false;
|
||||||
|
return evmMerkleRoot === loomState.root_hash && receipt.blockNumber < loomState.at_block;
|
||||||
|
}, [loomState, receipt, evmMerkleRoot]);
|
||||||
|
|
||||||
|
const weaverArgs = useMemo(() => {
|
||||||
|
if (!bridgeInInfo) return null;
|
||||||
|
return {
|
||||||
|
sessionIndex: bridgeInInfo.args.session,
|
||||||
|
slotIndex: bridgeInInfo.args.index % maximumSlots,
|
||||||
|
valueIndex: bridgeInInfo.args.index / maximumSlots,
|
||||||
|
};
|
||||||
|
}, [maximumSlots, bridgeInInfo]);
|
||||||
|
|
||||||
|
const pulledThreadKey = useMemo(() => {
|
||||||
|
if (!chainId || !weaverArgs) return "0x0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
|
||||||
|
const padU64 = (val) => "0x" + BigInt(val).toString(16).padStart(64, "0");
|
||||||
|
|
||||||
|
const metadata = [
|
||||||
|
padU64(weaverArgs.sessionIndex),
|
||||||
|
padU64(weaverArgs.valueIndex),
|
||||||
|
padU64(weaverArgs.slotIndex),
|
||||||
|
padU64(chainId)
|
||||||
|
].map(hexToBytes);
|
||||||
|
|
||||||
|
let currentHash = metadata[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < metadata.length; i++) {
|
||||||
|
const combined = new Uint8Array(64);
|
||||||
|
combined.set(currentHash, 0);
|
||||||
|
combined.set(metadata[i], 32);
|
||||||
|
currentHash = blake2AsU8a(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytesToHex(currentHash);
|
||||||
|
}, [chainId, weaverArgs]);
|
||||||
|
|
||||||
|
const { data: threadPulled, mutate: threadPulledMutate } = useSWR(
|
||||||
|
casperApi ? ['threadPulled', casperApi, pulledThreadKey, chainId] : null,
|
||||||
|
async () => {
|
||||||
|
const key = BigInt(chainId);
|
||||||
|
const res = await casperApi.query.GhostWeaver.PulledThreads.getValue(pulledThreadKey);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
revalidateOnFocus: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const pullCurrentThread = async () => {
|
||||||
|
setIsPending(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const txArgs = {
|
||||||
|
network_id: BigInt(chainId),
|
||||||
|
session: weaverArgs.sessionIndex,
|
||||||
|
thread_proof: {
|
||||||
|
type: "EvmThreadProof",
|
||||||
|
value: {
|
||||||
|
slot_index: weaverArgs.valueIndex,
|
||||||
|
proof_index: weaverArgs.slotIndex,
|
||||||
|
receiver: functionArgs.receiver,
|
||||||
|
amount: functionArgs.amount,
|
||||||
|
proof: merkleProof,
|
||||||
|
slots: slotValues,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tx = casperApi.tx.GhostWeaver.pull_thread(txArgs);
|
||||||
|
const bareTx = await tx.getBareTx();
|
||||||
|
|
||||||
|
await executePapiBareTransaction({ client, bareTx });
|
||||||
|
await threadPulledMutate();
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
maxWidth="420px"
|
||||||
|
minHeight="200px"
|
||||||
|
open={isOpen}
|
||||||
|
headerContent={
|
||||||
|
<Box display="flex" flexDirection="row">
|
||||||
|
{bridgeInInfo
|
||||||
|
? <Box display="flex" gap="10px">
|
||||||
|
<Typography variant="h5">Bridge-In</Typography>
|
||||||
|
<Link
|
||||||
|
underline="always"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={`${chainInfo.url}/tx/${historyRecord.txHash}`}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">
|
||||||
|
{bridgeInInfo ? shorten(historyRecord.txHash, 7, -5) : ""}
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
: <Typography variant="h5">Parsing Transaction Hash</Typography>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
onClose={() => isPending ? {} : close()}
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection="column">
|
||||||
|
<Box width="100%" gap="5px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
|
||||||
|
<BridgeInProgress
|
||||||
|
submissionBlock={receipt ? receipt.blockNumber : 0}
|
||||||
|
blockNumber={blockNumber}
|
||||||
|
chainId={chainId}
|
||||||
|
casperApi={casperApi}
|
||||||
|
finalization={finalization}
|
||||||
|
loomState={loomState}
|
||||||
|
rootIncluded={rootIncluded}
|
||||||
|
claimed={threadPulled === null}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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={() => functionArgs?.receiver ? copyToClipboard(functionArgs.receiver, 0) : {}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{functionArgs?.receiver ? shorten(functionArgs.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" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</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={bridgedAmount
|
||||||
|
? <Typography variant="body1">
|
||||||
|
{formatCurrency(bridgedAmount.toString(), 5, chainProperties?.properties.tokenSymbol ?? "")}
|
||||||
|
</Typography>
|
||||||
|
: <Skeleton width="120px" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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={estimatedReceived
|
||||||
|
? <Typography variant="body1">
|
||||||
|
{formatCurrency(estimatedReceived.toString(), 5, chainProperties?.properties.tokenSymbol ?? "")}
|
||||||
|
</Typography>
|
||||||
|
: <Skeleton width="120px" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<PrimaryButton
|
||||||
|
fullWidth
|
||||||
|
disabled={!rootIncluded || isPending || threadPulled === null}
|
||||||
|
onClick={pullCurrentThread}
|
||||||
|
>
|
||||||
|
{isPending
|
||||||
|
? "Pending..."
|
||||||
|
: threadPulled === null ? "Transaction Claimed" : "Claim Bridge-In"
|
||||||
|
}
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BridgeInProgress = ({ chainId, submissionBlock, claimed, casperApi, finalization, loomState, rootIncluded }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { data: weaverAuthorities } = useSWR(
|
||||||
|
casperApi ? ['weaverAuthorities', casperApi] : null,
|
||||||
|
() => casperApi.query.GhostWeaver.Authorities.getValue(),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: networkAttestations } = useSWR(
|
||||||
|
casperApi ? ['networkAttestations', casperApi] : 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
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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}
|
||||||
|
weaverAuthorities={weaverAuthorities}
|
||||||
|
networkAttestations={networkAttestations}
|
||||||
|
rootIncluded={rootIncluded}
|
||||||
|
submissionBlock={submissionBlock}
|
||||||
|
loomState={loomState}
|
||||||
|
tapestryDraft={tapestryDraft}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<RootVerification
|
||||||
|
theme={theme}
|
||||||
|
finalization={finalization}
|
||||||
|
rootIncluded={rootIncluded}
|
||||||
|
tapestryDraft={tapestryDraft}
|
||||||
|
tapestryStrand={tapestryStrand}
|
||||||
|
totalAuthorities={weaverAuthorities?.length ?? 0}
|
||||||
|
/>
|
||||||
|
<ProofSubmission claimed={claimed} 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 && !claimed) && '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"
|
||||||
|
: "Claim Pending..."
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RootVerification = ({ theme, finalization, tapestryDraft, rootIncluded, totalAuthorities, tapestryStrand }) => {
|
||||||
|
const finalizationPending = useMemo(() => finalization > 0, [finalization]);
|
||||||
|
|
||||||
|
const rootVerificationText = useMemo(() => {
|
||||||
|
if (rootIncluded) return "Synced";
|
||||||
|
if (!tapestryDraft || finalizationPending) 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, tapestryDraft, tapestryStrand, totalAuthorities, finalizationPending]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
transition: "all 0.8s ease",
|
||||||
|
transform: !rootIncluded && !finalizationPending && tapestryDraft && "scale(1.2)",
|
||||||
|
color: (rootIncluded || tapestryDraft) && !finalizationPending && 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 = ({ weaverAuthorities, networkAttestations, tapestryDraft, submissionBlock, loomState, rootIncluded, theme }) => {
|
||||||
|
const syncTargetText = useMemo(() => {
|
||||||
|
if (rootIncluded) return "Synced";
|
||||||
|
|
||||||
|
if (!tapestryDraft || !loomState) {
|
||||||
|
const total = weaverAuthorities ? weaverAuthorities.length : 0;
|
||||||
|
const count = networkAttestations
|
||||||
|
? networkAttestations.filter(v => v !== undefined).length
|
||||||
|
: 0;
|
||||||
|
return `${count} / ${total}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submissionBlock < loomState.at_block) return "Synced";
|
||||||
|
|
||||||
|
return `${formatNumber(Number(submissionBlock - loomState.at_block))} block behind`;
|
||||||
|
}, [rootIncluded, tapestryDraft, loomState, weaverAuthorities, submissionBlock, networkAttestations]);
|
||||||
|
|
||||||
|
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;
|
||||||
192
src/containers/History/DexViewModal.jsx
Normal file
192
src/containers/History/DexViewModal.jsx
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { useTransactionReceipt, useToken } from 'wagmi';
|
||||||
|
import { decodeEventLog } from 'viem';
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
|
||||||
|
import { Box, Typography, Link } from "@mui/material";
|
||||||
|
|
||||||
|
import Token from "../../components/Token/Token";
|
||||||
|
import Modal from "../../components/Modal/Modal";
|
||||||
|
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||||
|
import { PrimaryButton } from "../../components/Button";
|
||||||
|
|
||||||
|
import { abi as UniswapV2PairAbi } from "../../abi/UniswapV2Pair.json";
|
||||||
|
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||||
|
import { formatNumber, shorten } from "../../helpers";
|
||||||
|
|
||||||
|
const TokenBox = ({ tokenAmount, tokenSymbol, decimals, chainTokenSymbol }) => {
|
||||||
|
return (
|
||||||
|
<Box display="flex" flexDirection="column" width="70px">
|
||||||
|
<Token
|
||||||
|
parentSx={{
|
||||||
|
height: "80px",
|
||||||
|
"& svg:nth-of-type(2)": {
|
||||||
|
transform: "translate(10%, -50%)",
|
||||||
|
zIndex: 100,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ transform: "scale(1.65)", }}
|
||||||
|
chainTokenName={chainTokenSymbol}
|
||||||
|
name={tokenSymbol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="column" alignItems="center">
|
||||||
|
<Typography>{formatNumber(
|
||||||
|
new DecimalBigNumber(tokenAmount ? tokenAmount : 0n, decimals),
|
||||||
|
5
|
||||||
|
)}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DexViewModal = ({ chainId, isOpen, close, config, historyRecord }) => {
|
||||||
|
if (historyRecord === null) return null;
|
||||||
|
|
||||||
|
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: UniswapV2PairAbi,
|
||||||
|
data: log.data,
|
||||||
|
topics: log.topics,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: decoded.eventName,
|
||||||
|
args: decoded.args,
|
||||||
|
address: log.address
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(Boolean);
|
||||||
|
}, [receipt]);
|
||||||
|
|
||||||
|
const swapInfo = useMemo(() => {
|
||||||
|
const swapEvent = allDecodedLogs.find(l => l.eventName === 'Swap');
|
||||||
|
if (!swapEvent) return null;
|
||||||
|
|
||||||
|
const isZeroForOne = swapEvent.args.amount0In > 0n;
|
||||||
|
|
||||||
|
const transferLogs = allDecodedLogs.filter(l => l.eventName === 'Transfer');
|
||||||
|
|
||||||
|
const tokenInAddress = isZeroForOne
|
||||||
|
? transferLogs.find(t => t.args.value === swapEvent.args.amount0In)?.address
|
||||||
|
: transferLogs.find(t => t.args.value === swapEvent.args.amount1In)?.address;
|
||||||
|
|
||||||
|
const tokenOutAddress = isZeroForOne
|
||||||
|
? transferLogs.find(t => t.args.value === swapEvent.args.amount1Out)?.address
|
||||||
|
: transferLogs.find(t => t.args.value === swapEvent.args.amount0Out)?.address;
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: swapEvent.args.sender,
|
||||||
|
to: swapEvent.args.to,
|
||||||
|
tokenInAddress,
|
||||||
|
tokenOutAddress,
|
||||||
|
amountIn: isZeroForOne ? swapEvent.args.amount0In : swapEvent.args.amount1In,
|
||||||
|
amountOut: isZeroForOne ? swapEvent.args.amount1Out : swapEvent.args.amount0Out,
|
||||||
|
};
|
||||||
|
}, [allDecodedLogs]);
|
||||||
|
|
||||||
|
const { data: tokenIn } = useToken({
|
||||||
|
address: swapInfo?.tokenInAddress,
|
||||||
|
scopeKey: `token-${swapInfo ? swapInfo.tokenInAddress : ""}-${chainId}`,
|
||||||
|
chainId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: tokenOut } = useToken({
|
||||||
|
address: swapInfo?.tokenOutAddress,
|
||||||
|
scopeKey: `token-${swapInfo ? swapInfo.tokenOutAddress : ""}-${chainId}`,
|
||||||
|
chainId
|
||||||
|
});
|
||||||
|
|
||||||
|
const chainInfo = useMemo(() => {
|
||||||
|
const client = config?.getClient();
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: client?.chain?.blockExplorers?.default?.url,
|
||||||
|
symbol: client?.chain?.nativeCurrency?.symbol,
|
||||||
|
};
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
maxWidth="420px"
|
||||||
|
minHeight="200px"
|
||||||
|
open={isOpen}
|
||||||
|
headerText={
|
||||||
|
<>
|
||||||
|
{swapInfo && "DEX Swap"}
|
||||||
|
{!swapInfo && "Parsing Transaction Hash"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onClose={() => close()}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
<Link
|
||||||
|
href={`${chainInfo.url}/address/${swapInfo ? swapInfo.from : ""}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Typography>{swapInfo ? shorten(swapInfo.from, 12, -10) : ""}</Typography>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" justifyContent="space-between" width="100%">
|
||||||
|
<Typography>{tokenOut ? tokenOut.symbol : "Unknown"} Receiver</Typography>
|
||||||
|
<Link
|
||||||
|
href={`${chainInfo.url}/address/${swapInfo ? swapInfo.to : ""}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Typography>{swapInfo ? shorten(swapInfo.to, 12, -10) : ""}</Typography>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-around" alignItems="center">
|
||||||
|
<TokenBox
|
||||||
|
tokenAmount={swapInfo ? swapInfo.amountIn : 0n}
|
||||||
|
tokenSymbol={tokenIn ? tokenIn.symbol : "Unknown"}
|
||||||
|
decimals={tokenIn ? tokenIn.decimals : 18}
|
||||||
|
chainTokenSymbol={tokenIn?.symbol.toUpperCase() === "WETH" ? undefined : chainInfo.symbol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GhostStyledIcon sx={{ fontSize: '24px' }} component={ArrowRightIcon} />
|
||||||
|
|
||||||
|
<TokenBox
|
||||||
|
tokenAmount={swapInfo ? swapInfo.amountOut : 0n}
|
||||||
|
tokenSymbol={tokenOut ? tokenOut.symbol : "Unknown"}
|
||||||
|
decimals={tokenOut ? tokenOut.decimals : 18}
|
||||||
|
chainTokenSymbol={tokenOut?.symbol.toUpperCase() === "WETH" ? undefined : chainInfo.symbol}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<PrimaryButton
|
||||||
|
fullWidth
|
||||||
|
component="a"
|
||||||
|
href={`${chainInfo.url}/tx/${historyRecord.txHash}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
View Transaction Details
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DexViewModal;
|
||||||
23
src/containers/History/EraseModal.jsx
Normal file
23
src/containers/History/EraseModal.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
import Modal from "../../components/Modal/Modal";
|
||||||
|
import { PrimaryButton } from "../../components/Button";
|
||||||
|
|
||||||
|
const EraseModal = ({ isOpen, close, submit, isAll }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
maxWidth="350px"
|
||||||
|
minHeight="200px"
|
||||||
|
open={isOpen}
|
||||||
|
headerText={`Erase ${isAll ? "All Records" : "Record"}`}
|
||||||
|
onClose={() => close()}
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection="column" justifyContent="space-between" gap="20px">
|
||||||
|
<Typography>{`Erasing ${isAll ? "all records" : "this record"} is permanent, and cannot be recovered.`}</Typography>
|
||||||
|
<PrimaryButton fullWidth onClick={submit}>Erase {isAll ? "All Records" : ""}</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EraseModal;
|
||||||
452
src/containers/History/History.jsx
Normal file
452
src/containers/History/History.jsx
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
Container,
|
||||||
|
FormControlLabel,
|
||||||
|
FormControl,
|
||||||
|
Checkbox,
|
||||||
|
Link,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import ReactGA from "react-ga4";
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
|
||||||
|
import Modal from "../../components/Modal/Modal";
|
||||||
|
import Paper from "../../components/Paper/Paper";
|
||||||
|
import PageTitle from "../../components/PageTitle/PageTitle";
|
||||||
|
import TokenStack from "../../components/TokenStack/TokenStack";
|
||||||
|
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||||
|
import Chip from "../../components/Chip/Chip";
|
||||||
|
|
||||||
|
import { PrimaryButton, TertiaryButton } from "../../components/Button";
|
||||||
|
import { Tab, Tabs } from "../../components/Tabs/Tabs";
|
||||||
|
|
||||||
|
import { useTokenSymbol } from "../../hooks/tokens";
|
||||||
|
import { useLocalStorage } from "../../hooks/localstorage";
|
||||||
|
import { useUnstableProvider } from "../../hooks/ghost";
|
||||||
|
import { shorten } from "../../helpers";
|
||||||
|
import { networkAvgBlockSpeed } from "../../constants";
|
||||||
|
|
||||||
|
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, finalityDelay }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isVerySmallScreen = useMediaQuery("(max-width: 450px)");
|
||||||
|
|
||||||
|
const localString = new Date(item.submitted).toLocaleString();
|
||||||
|
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO")
|
||||||
|
|
||||||
|
const chainInfo = useMemo(() => {
|
||||||
|
const client = config?.getClient();
|
||||||
|
return {
|
||||||
|
url: client?.chain?.blockExplorers?.default?.url,
|
||||||
|
symbol: client?.chain?.nativeCurrency?.symbol,
|
||||||
|
};
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const chipStatus = useMemo(() => {
|
||||||
|
const delay = Number(finalityDelay * networkAvgBlockSpeed(chainId)) * 1000;
|
||||||
|
const width = isVerySmallScreen ? "21px" : "100px";
|
||||||
|
|
||||||
|
if (item.submitted + delay > currentTime) {
|
||||||
|
return <Chip sx={{ width: width }} label={isVerySmallScreen ? "" : "Pending"} template={"gray"} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Chip sx={{ width: width }} label={isVerySmallScreen ? "" : "Ready"} template={"success"} />
|
||||||
|
}, [item, currentTime, isVerySmallScreen, finalityDelay])
|
||||||
|
|
||||||
|
if (isSmallScreen || isVerySmallScreen) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box display="flex" flexDirection="column" gap="10px">
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
flexDirection={isVerySmallScreen ? "row" : "row"}
|
||||||
|
>
|
||||||
|
<Box display="flex" gap="20px" p="10px 0">
|
||||||
|
<TokenStack chainTokenNames={[chainInfo.symbol]} tokens={["FTSO"]} />
|
||||||
|
<Box display="flex" flexDirection="column">
|
||||||
|
<Typography>{localString}</Typography>
|
||||||
|
<Link
|
||||||
|
href={`${chainInfo.url}/tx/${item.txHash}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Typography>{shorten(item.txHash)}</Typography>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="column" alignItems="flex-end">
|
||||||
|
<Typography>{item?.name ?? "Unknown"}</Typography>
|
||||||
|
{chipStatus}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="row" gap="20px">
|
||||||
|
<TertiaryButton fullWidth onClick={eraseRecord}>Erase</TertiaryButton>
|
||||||
|
<TertiaryButton fullWidth onClick={setViewModalOpen}>View</TertiaryButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell sx={{ padding: "10px 0" }}>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<TokenStack chainTokenNames={["ETH"]} tokens={["FTSO"]} />
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<TableCell align="center" sx={{ padding: "10px 0" }}>
|
||||||
|
{chipStatus}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell sx={{ padding: "10px 0" }}>
|
||||||
|
<Typography>{localString}</Typography>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell sx={{ padding: "10px 0" }}>
|
||||||
|
<Box display="flex" flexDirection="row" gap="20px">
|
||||||
|
<TertiaryButton fullWidth onClick={eraseRecord}>Erase</TertiaryButton>
|
||||||
|
<TertiaryButton fullWidth onClick={setViewModalOpen}>View</TertiaryButton>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryList = ({ chainId, storedTransactions, historyType, config, eraseRecord }) => {
|
||||||
|
const isSmallScreen = useMediaQuery("(max-width: 900px)");
|
||||||
|
|
||||||
|
const [currentTime, setCurrentTime] = useState(Date.now());
|
||||||
|
const [viewModalOpen, setViewModalOpen] = useState(null);
|
||||||
|
|
||||||
|
const { CONSTANTS } = useLocalStorage();
|
||||||
|
const { casperApi } = useUnstableProvider();
|
||||||
|
|
||||||
|
const { data: networkData, error, isLoading } = 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 finalityDelay = useMemo(() => {
|
||||||
|
return networkData ? networkData.finality_delay : 420n;
|
||||||
|
}, [networkData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
setCurrentTime(Date.now());
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const historyContainer = useMemo(() => {
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<Box display="flex" flexDirection="column" gap="50px">
|
||||||
|
{storedTransactions?.map((item, i) => {
|
||||||
|
return (
|
||||||
|
<HistoryItem
|
||||||
|
key={i}
|
||||||
|
item={item}
|
||||||
|
config={config}
|
||||||
|
chainId={chainId}
|
||||||
|
currentTime={currentTime}
|
||||||
|
finalityDelay={finalityDelay}
|
||||||
|
eraseRecord={() => eraseRecord(i)}
|
||||||
|
setViewModalOpen={() => setViewModalOpen(i)}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell style={{ padding: "8px 0" }}>Transaction</TableCell>
|
||||||
|
<TableCell style={{ padding: "8px 0" }} align="center">Status</TableCell>
|
||||||
|
<TableCell style={{ padding: "8px 0" }}>Submitted</TableCell>
|
||||||
|
<TableCell style={{ padding: "8px 0" }}></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{storedTransactions?.map((item, i) => {
|
||||||
|
return (
|
||||||
|
<HistoryItem
|
||||||
|
key={i}
|
||||||
|
item={item}
|
||||||
|
config={config}
|
||||||
|
currentTime={currentTime}
|
||||||
|
finalityDelay={finalityDelay}
|
||||||
|
eraseRecord={() => eraseRecord(i)}
|
||||||
|
setViewModalOpen={() => setViewModalOpen(i)}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [isSmallScreen, storedTransactions, config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{viewModalOpen !== null && historyType === CONSTANTS.dex.key && (
|
||||||
|
<DexViewModal
|
||||||
|
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}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const History = ({ chainId, config, tryConnectInjected, address }) => {
|
||||||
|
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||||
|
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
|
||||||
|
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||||
|
|
||||||
|
const [historyTypeIndex, setHistoryTypeIndex] = useState(0);
|
||||||
|
const [erasingIndex, setErasingIndex] = useState(null);
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
const [erasingOpen, setErasingOpen] = useState(false);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ReactGA.send({ hitType: "pageview", page: location.pathname });
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
CONSTANTS,
|
||||||
|
useStorageValue,
|
||||||
|
setStorageValue,
|
||||||
|
clearPendingHistory
|
||||||
|
} = useLocalStorage();
|
||||||
|
|
||||||
|
const { data: storedHistoryTypes } = useStorageValue(chainId, address, CONSTANTS.historyPrefix, CONSTANTS.defaultHistoryTypes);
|
||||||
|
const { data: storedTransactions } = useStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []);
|
||||||
|
|
||||||
|
const { data: pendingHistory } = useStorageValue(chainId, address, CONSTANTS.pendingHistoryPrefix, {});
|
||||||
|
|
||||||
|
const handleHistoryTypesChange = useCallback((values) => {
|
||||||
|
setStorageValue(chainId, address, CONSTANTS.historyPrefix, values);
|
||||||
|
}, [chainId, address]);
|
||||||
|
|
||||||
|
const handleConfirmErase = useCallback(() => {
|
||||||
|
if (erasingIndex !== null) {
|
||||||
|
const updated = storedTransactions.filter((_, index) => index !== erasingIndex);
|
||||||
|
setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], updated);
|
||||||
|
} else {
|
||||||
|
setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []);
|
||||||
|
}
|
||||||
|
setErasingOpen(false);
|
||||||
|
setErasingIndex(null);
|
||||||
|
}, [storedTransactions, erasingIndex, chainId, address, historyTypeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearPendingHistory(chainId, address, storedHistoryTypes.at(historyTypeIndex));
|
||||||
|
}, [storedHistoryTypes, historyTypeIndex])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box minHeight="calc(100vh - 43px)">
|
||||||
|
<PageTitle name="History" subtitle="A complete record of your activity on the ghostDAO protocol" />
|
||||||
|
<Container
|
||||||
|
style={{
|
||||||
|
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||||
|
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EraseModal
|
||||||
|
key="EraseModal"
|
||||||
|
isOpen={erasingOpen}
|
||||||
|
close={() => {
|
||||||
|
setErasingOpen(false);
|
||||||
|
setErasingIndex(null);
|
||||||
|
}}
|
||||||
|
submit={handleConfirmErase}
|
||||||
|
isAll={erasingIndex === null}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingsModal
|
||||||
|
key="SettingsModal"
|
||||||
|
isOpen={settingsOpen}
|
||||||
|
close={() => setSettingsOpen(false)}
|
||||||
|
onChange={handleHistoryTypesChange}
|
||||||
|
selectedValues={storedHistoryTypes}
|
||||||
|
possibleValues={CONSTANTS.possibleHistoryTypes}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box mt="15px">
|
||||||
|
<Paper
|
||||||
|
fullWidth
|
||||||
|
enableBackground
|
||||||
|
topRight={
|
||||||
|
<Box style={{ position: "absolute", top: "28px", right: "20px", zIndex: 10 }}>
|
||||||
|
<GhostStyledIcon
|
||||||
|
component={SettingsIcon}
|
||||||
|
viewBox="0 0 23 23"
|
||||||
|
style={{ display: "flex", cursor: "pointer" }}
|
||||||
|
onClick={() => setSettingsOpen(true)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
headerContent={
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
gap="15px"
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
centered={!isSemiSmallScreen}
|
||||||
|
orientation={isSemiSmallScreen ? "vertical" : "horizontal"}
|
||||||
|
textColor="primary"
|
||||||
|
indicatorColor="primary"
|
||||||
|
value={storedHistoryTypes[historyTypeIndex]}
|
||||||
|
aria-label="History menu"
|
||||||
|
onChange={(_, selectedValue) => {
|
||||||
|
const index = storedHistoryTypes.indexOf(selectedValue);
|
||||||
|
if (index !== -1) setHistoryTypeIndex(index);
|
||||||
|
}}
|
||||||
|
TabIndicatorProps={{ style: { display: "none" } }}
|
||||||
|
>
|
||||||
|
{storedHistoryTypes.map((type, i) => {
|
||||||
|
return (
|
||||||
|
<Tab
|
||||||
|
key={i}
|
||||||
|
value={type}
|
||||||
|
aria-label={`history-${i}-button`}
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<PrimaryButton
|
||||||
|
fullWidth
|
||||||
|
onClick={() => setErasingOpen(true)}
|
||||||
|
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
|
||||||
|
>
|
||||||
|
Erase All Records
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HistoryList
|
||||||
|
chainId={chainId}
|
||||||
|
config={config}
|
||||||
|
eraseRecord={(index) => {
|
||||||
|
setErasingIndex(index);
|
||||||
|
setErasingOpen(true);
|
||||||
|
}}
|
||||||
|
historyType={storedHistoryTypes[historyTypeIndex]}
|
||||||
|
storedTransactions={storedTransactions}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default History;
|
||||||
49
src/containers/History/SettingsModal.jsx
Normal file
49
src/containers/History/SettingsModal.jsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Box, FormControlLabel, Checkbox, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
import Modal from "../../components/Modal/Modal";
|
||||||
|
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
|
||||||
|
|
||||||
|
const SettingsModal = ({ isOpen, close, submit, selectedValues, possibleValues, onChange }) => {
|
||||||
|
const handleCheckboxChange = (type, isChecked) => {
|
||||||
|
const updatedTypes = possibleValues.filter(v => {
|
||||||
|
if (v === type) return isChecked
|
||||||
|
return selectedValues.includes(v)
|
||||||
|
})
|
||||||
|
onChange(updatedTypes);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
key="SettingsModal"
|
||||||
|
maxWidth="350px"
|
||||||
|
minHeight="200px"
|
||||||
|
open={isOpen}
|
||||||
|
headerText="History Settings"
|
||||||
|
onClose={() => close()}
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection="column" justifyContent="space-between" gap="20px">
|
||||||
|
{possibleValues.map((type, i) => {
|
||||||
|
const isChecked = selectedValues.includes(type);
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
key={i}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
inputProps={{ "data-testid": `history-type-${type}` }}
|
||||||
|
id={`history-type-${type}`}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={event => handleCheckboxChange(type, event.target.checked)}
|
||||||
|
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
|
||||||
|
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<Typography variant="body1">{isChecked ? "Disable" : "Enable"} {type} History Storage</Typography>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsModal;
|
||||||
160
src/containers/History/StakingViewModal.jsx
Normal file
160
src/containers/History/StakingViewModal.jsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { useTransactionReceipt } from 'wagmi';
|
||||||
|
import { decodeEventLog } from 'viem';
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
|
||||||
|
import { Box, Typography, Link } from "@mui/material";
|
||||||
|
|
||||||
|
import Token from "../../components/Token/Token";
|
||||||
|
import Modal from "../../components/Modal/Modal";
|
||||||
|
import GhostStyledIcon from "../../components/Icon/GhostIcon";
|
||||||
|
import { PrimaryButton } from "../../components/Button";
|
||||||
|
|
||||||
|
import { abi as StakingAbi } from "../../abi/GhostStaking.json";
|
||||||
|
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||||
|
import { formatNumber, shorten } from "../../helpers";
|
||||||
|
|
||||||
|
import { useTokenSymbol } from "../../hooks/tokens";
|
||||||
|
import { getTokenDecimals } from "../../hooks/helpers";
|
||||||
|
|
||||||
|
const TokenBox = ({ tokenAmount, tokenSymbol, decimals, chainTokenSymbol }) => {
|
||||||
|
return (
|
||||||
|
<Box display="flex" flexDirection="column" width="70px">
|
||||||
|
<Token
|
||||||
|
parentSx={{
|
||||||
|
height: "80px",
|
||||||
|
"& svg:nth-of-type(2)": {
|
||||||
|
transform: "translate(10%, -50%)",
|
||||||
|
zIndex: 100,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ transform: "scale(1.65)", }}
|
||||||
|
chainTokenName={chainTokenSymbol}
|
||||||
|
name={tokenSymbol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="column" alignItems="center">
|
||||||
|
<Typography>{tokenSymbol}</Typography>
|
||||||
|
|
||||||
|
<Typography>{formatNumber(
|
||||||
|
new DecimalBigNumber(tokenAmount ? tokenAmount : 0n, decimals),
|
||||||
|
5
|
||||||
|
)}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StakingViewModal = ({ chainId, isOpen, close, config, historyRecord }) => {
|
||||||
|
if (historyRecord === null) return null;
|
||||||
|
|
||||||
|
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: StakingAbi,
|
||||||
|
data: log.data,
|
||||||
|
topics: log.topics,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: decoded.eventName,
|
||||||
|
args: decoded.args,
|
||||||
|
address: log.address
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(Boolean);
|
||||||
|
}, [receipt]);
|
||||||
|
|
||||||
|
const stakeInfo = useMemo(() => {
|
||||||
|
return allDecodedLogs.find(l => l.eventName === 'Staked');
|
||||||
|
}, [allDecodedLogs]);
|
||||||
|
|
||||||
|
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||||
|
const ftsoDecimals = getTokenDecimals("FTSO");
|
||||||
|
|
||||||
|
const chainInfo = useMemo(() => {
|
||||||
|
const client = config?.getClient();
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: client?.chain?.blockExplorers?.default?.url,
|
||||||
|
symbol: client?.chain?.nativeCurrency?.symbol,
|
||||||
|
};
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
maxWidth="420px"
|
||||||
|
minHeight="200px"
|
||||||
|
open={isOpen}
|
||||||
|
headerText={
|
||||||
|
<>
|
||||||
|
{stakeInfo && "Staked"}
|
||||||
|
{!stakeInfo && "Parsing Transaction Hash"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onClose={() => close()}
|
||||||
|
>
|
||||||
|
{stakeInfo && <Box display="flex" flexDirection="column" gap="15px">
|
||||||
|
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
|
||||||
|
<Token
|
||||||
|
parentSx={{
|
||||||
|
height: "80px",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
"& svg:nth-of-type(2)": {
|
||||||
|
transform: "translate(50%, -50%)",
|
||||||
|
zIndex: 100,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ transform: "scale(1.65)", }}
|
||||||
|
chainTokenName={chainInfo.symbol}
|
||||||
|
name={ftsoSymbol ? ftsoSymbol : ""}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box flexBasis="75%" display="flex" flexDirection="column">
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Typography>Receiver</Typography>
|
||||||
|
<Link
|
||||||
|
href={`${chainInfo.url}/address/${stakeInfo.args.receiver}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Typography>{shorten(stakeInfo.args.receiver, 9, -7)}</Typography>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Typography>Amount</Typography>
|
||||||
|
<Typography>{formatNumber(
|
||||||
|
new DecimalBigNumber(stakeInfo ? stakeInfo.args.amount : 0n, ftsoDecimals),
|
||||||
|
5
|
||||||
|
)} {ftsoSymbol}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<PrimaryButton
|
||||||
|
fullWidth
|
||||||
|
component="a"
|
||||||
|
href={`${chainInfo.url}/tx/${historyRecord.txHash}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
View Transaction Details
|
||||||
|
</PrimaryButton>
|
||||||
|
</Box>}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StakingViewModal;
|
||||||
@ -78,7 +78,6 @@ export const StakeContainer = ({ chainId, address, config, connect }) => {
|
|||||||
Rebase
|
Rebase
|
||||||
<Countdown otherwise="Ready" targetTime={epoch.end * 1000} />
|
<Countdown otherwise="Ready" targetTime={epoch.end * 1000} />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import Token from "../../../components/Token/Token";
|
|||||||
import { PrimaryButton, SecondaryButton } from "../../../components/Button";
|
import { PrimaryButton, SecondaryButton } from "../../../components/Button";
|
||||||
import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard";
|
import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard";
|
||||||
|
|
||||||
|
import { useLocalStorage } from "../../../hooks/localstorage";
|
||||||
import { useWarmupLength, stake, unstake, wrap, unwrap } from "../../../hooks/staking";
|
import { useWarmupLength, stake, unstake, wrap, unwrap } from "../../../hooks/staking";
|
||||||
import { formatNumber } from "../../../helpers";
|
import { formatNumber } from "../../../helpers";
|
||||||
|
|
||||||
@ -22,6 +23,8 @@ const StakeConfirmationModal = (props) => {
|
|||||||
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
|
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
|
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
|
||||||
|
|
||||||
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup])
|
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup])
|
||||||
|
|
||||||
const AcknowledgeWarmupCheckbox = () => {
|
const AcknowledgeWarmupCheckbox = () => {
|
||||||
@ -73,11 +76,13 @@ const StakeConfirmationModal = (props) => {
|
|||||||
setIsPending(true);
|
setIsPending(true);
|
||||||
const actionAmount = new DecimalBigNumber(props.amount, props.spendDecimals);
|
const actionAmount = new DecimalBigNumber(props.amount, props.spendDecimals);
|
||||||
let isRebase = false;
|
let isRebase = false;
|
||||||
|
let txHash = null;
|
||||||
|
let txName = null;
|
||||||
|
|
||||||
switch (props.action.toUpperCase()) {
|
switch (props.action.toUpperCase()) {
|
||||||
case "STAKE":
|
case "STAKE":
|
||||||
isRebase = props.bottomToken === props.stnkSymbol;
|
isRebase = props.bottomToken === props.stnkSymbol;
|
||||||
await stake(
|
txHash = await stake(
|
||||||
props.chainId,
|
props.chainId,
|
||||||
props.address,
|
props.address,
|
||||||
actionAmount._value.toBigInt(),
|
actionAmount._value.toBigInt(),
|
||||||
@ -87,10 +92,11 @@ const StakeConfirmationModal = (props) => {
|
|||||||
props.stnkSymbol,
|
props.stnkSymbol,
|
||||||
props.ghstSymbol
|
props.ghstSymbol
|
||||||
);
|
);
|
||||||
|
txName = CONSTANTS.stake.stake;
|
||||||
break;
|
break;
|
||||||
case "UNSTAKE":
|
case "UNSTAKE":
|
||||||
isRebase = props.upperToken === props.stnkSymbol;
|
isRebase = props.upperToken === props.stnkSymbol;
|
||||||
await unstake(
|
txHash = await unstake(
|
||||||
props.chainId,
|
props.chainId,
|
||||||
props.address,
|
props.address,
|
||||||
actionAmount._value.toBigInt(),
|
actionAmount._value.toBigInt(),
|
||||||
@ -100,27 +106,32 @@ const StakeConfirmationModal = (props) => {
|
|||||||
props.stnkSymbol,
|
props.stnkSymbol,
|
||||||
props.ghstSymbol
|
props.ghstSymbol
|
||||||
);
|
);
|
||||||
|
txName = CONSTANTS.stake.unstake;
|
||||||
break;
|
break;
|
||||||
case "WRAP":
|
case "WRAP":
|
||||||
await wrap(
|
txHash = await wrap(
|
||||||
props.chainId,
|
props.chainId,
|
||||||
props.address,
|
props.address,
|
||||||
actionAmount._value.toBigInt(),
|
actionAmount._value.toBigInt(),
|
||||||
props.ghstSymbol
|
props.ghstSymbol
|
||||||
);
|
);
|
||||||
|
txName = CONSTANTS.stake.wrap;
|
||||||
break;
|
break;
|
||||||
case "UNWRAP":
|
case "UNWRAP":
|
||||||
await unwrap(
|
txHash = await unwrap(
|
||||||
props.chainId,
|
props.chainId,
|
||||||
props.address,
|
props.address,
|
||||||
actionAmount._value.toBigInt(),
|
actionAmount._value.toBigInt(),
|
||||||
props.stnkSymbol
|
props.stnkSymbol
|
||||||
);
|
);
|
||||||
|
txName = CONSTANTS.stake.unwrap;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Wrong action to be executed!");
|
console.log("Wrong action to be executed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (txHash && txName) tryStoreInHistory(props.chainId, props.address, CONSTANTS.stake.key, txHash, txName);
|
||||||
|
|
||||||
setIsPending(false);
|
setIsPending(false);
|
||||||
props.onClose();
|
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 = ({
|
export const StakeInputArea = ({
|
||||||
chainSymbol,
|
chainSymbol,
|
||||||
upperToken,
|
upperToken,
|
||||||
@ -84,11 +88,11 @@ export const StakeInputArea = ({
|
|||||||
const [bottomTokenModalOpen, setBottomTokenModalOpen] = useState(false);
|
const [bottomTokenModalOpen, setBottomTokenModalOpen] = useState(false);
|
||||||
const [confirmationModalOpen, setConfirmationModalOpen] = 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 { data: formatDecimals } = useStorageValue(chainId, address, DECIMALS_PREFIX, "5");
|
||||||
const [isClaim, setIsClaim] = useState(() => getStorageValue(chainId, address, "stake-isClaim", true));
|
const { data: isClaim } = useStorageValue(chainId, address, IS_CLAIM_PREFIX, true);
|
||||||
const [isTrigger, setIsTrigger] = useState(() => getStorageValue(chainId, address, "stake-isTrigger", true));
|
const { data: isTrigger } = useStorageValue(chainId, address, IS_TRIGGER_PREFIX, true);
|
||||||
|
|
||||||
const [amount, setAmount] = useState("");
|
const [amount, setAmount] = useState("");
|
||||||
const [receiveAmount, setReceiveAmount] = useState("");
|
const [receiveAmount, setReceiveAmount] = useState("");
|
||||||
@ -103,20 +107,17 @@ export const StakeInputArea = ({
|
|||||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||||
|
|
||||||
const setIsClaimInner = useCallback((value) => {
|
const setIsClaim = useCallback((value) => {
|
||||||
setIsClaim(value);
|
setStorageValue(chainId, address, IS_CLAIM_PREFIX, value);
|
||||||
setStorageValue(chainId, address, "stake-isClaim", value);
|
|
||||||
}, [chainId, address]);
|
}, [chainId, address]);
|
||||||
|
|
||||||
const setIsTriggerInner = useCallback((value) => {
|
const setIsTrigger = useCallback((value) => {
|
||||||
setIsTrigger(value);
|
setStorageValue(chainId, address, IS_TRIGGER_PREFIX, value);
|
||||||
setStorageValue(chainId, address, "stake-isTrigger", value);
|
|
||||||
}, [chainId, address]);
|
}, [chainId, address]);
|
||||||
|
|
||||||
const setFormatDecimalsInner = useCallback((value) => {
|
const setFormatDecimals = useCallback((value) => {
|
||||||
if (Number(value) <= 17) {
|
if (Number(value) <= 17) {
|
||||||
setFormatDecimals(value);
|
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
|
||||||
setStorageValue(chainId, address, "stake-decimals", value);
|
|
||||||
}
|
}
|
||||||
}, [chainId, address]);
|
}, [chainId, address]);
|
||||||
|
|
||||||
@ -251,11 +252,11 @@ export const StakeInputArea = ({
|
|||||||
open={settingsModalOpen}
|
open={settingsModalOpen}
|
||||||
onClose={closeSettingModal}
|
onClose={closeSettingModal}
|
||||||
formatDecimals={formatDecimals}
|
formatDecimals={formatDecimals}
|
||||||
onFormatDecimalsChange={event => setFormatDecimalsInner(event.currentTarget.value)}
|
onFormatDecimalsChange={event => setFormatDecimals(event.currentTarget.value)}
|
||||||
isClaim={isClaim}
|
isClaim={isClaim}
|
||||||
onIsClaimChanged={setIsClaimInner}
|
onIsClaimChanged={setIsClaim}
|
||||||
isTrigger={isTrigger}
|
isTrigger={isTrigger}
|
||||||
onTriggerRebaseChange={setIsTriggerInner}
|
onTriggerRebaseChange={setIsTrigger}
|
||||||
/>
|
/>
|
||||||
<StakeConfirmationModal
|
<StakeConfirmationModal
|
||||||
chainSymbol={chainSymbol}
|
chainSymbol={chainSymbol}
|
||||||
|
|||||||
@ -273,24 +273,26 @@ export const useNotes = (chainId, address) => {
|
|||||||
return { notes, refetch };
|
return { notes, refetch };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => {
|
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, isNative }) => {
|
||||||
const args = [bondId, amount, maxPrice, user, referral];
|
const args = [bondId, amount, maxPrice, user];
|
||||||
const messages = {
|
const messages = {
|
||||||
replacedMsg: "Bond transaction was replaced. Wait for inclusion please.",
|
replacedMsg: "Bond transaction was replaced. Wait for inclusion please.",
|
||||||
successMsg: `Bond successfully purchased for ${shorten(user)}! Wait until it'll mature before claim.`,
|
successMsg: `Bond successfully purchased for ${shorten(user)}! Wait until it'll mature before claim.`,
|
||||||
errorMsg: "Bond transaction failed. Check logs for error detalization.",
|
errorMsg: "Bond transaction failed. Check logs for error detalization.",
|
||||||
};
|
};
|
||||||
|
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
messages,
|
messages,
|
||||||
abi: BondAbi,
|
abi: BondAbi,
|
||||||
address: BOND_DEPOSITORY_ADDRESSES[chainId],
|
address: BOND_DEPOSITORY_ADDRESSES[chainId],
|
||||||
functionName: "deposit",
|
functionName: "deposit",
|
||||||
account: user,
|
account: sender,
|
||||||
value: isNative ? amount : undefined
|
value: isNative ? amount : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const redeem = async ({ chainId, user, isGhst, indexes }) => {
|
export const redeem = async ({ chainId, user, isGhst, indexes }) => {
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { createContext, useContext, useState, useMemo } from "react";
|
import { createContext, useContext, useState, useMemo } from "react";
|
||||||
import { claim } from "../../hooks/staking";
|
|
||||||
|
|
||||||
const emptyFunction = () => {};
|
const emptyFunction = () => {};
|
||||||
const BreakoutModalContext = createContext();
|
const BreakoutModalContext = createContext();
|
||||||
|
|||||||
@ -2,76 +2,102 @@ import { createContext, useEffect, useContext, useState, useMemo, useCallback, u
|
|||||||
import { Unstable } from "@substrate/connect-discovery"
|
import { Unstable } from "@substrate/connect-discovery"
|
||||||
import { createClient } from "@polkadot-api/substrate-client"
|
import { createClient } from "@polkadot-api/substrate-client"
|
||||||
import { getObservableClient } from "@polkadot-api/observable-client"
|
import { getObservableClient } from "@polkadot-api/observable-client"
|
||||||
|
import { getInjectedExtensions, connectInjectedExtension } from "polkadot-api/pjs-signer";
|
||||||
import useSWR from "swr"
|
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";
|
||||||
|
import { casperChainSpec } from "./chainSpecs";
|
||||||
|
|
||||||
|
const GHOST_CONNECT_EXTENSION = "GHOST Connect Browser Extension"
|
||||||
const MAX_BLOCK_TIMEOUT = 15000
|
const MAX_BLOCK_TIMEOUT = 15000
|
||||||
const DEFAULT_CHAIN_ID = "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3"
|
const DEFAULT_CHAIN_ID = "0xcaf9c5ddccc7ba6f96149a6b8a81c26a3641742795cd5e7e0aee79bf3f4e77b7"
|
||||||
const UnstableProvider = createContext(null)
|
const UnstableProvider = createContext(null)
|
||||||
export const useUnstableProvider = () => useContext(UnstableProvider)
|
export const useUnstableProvider = () => useContext(UnstableProvider)
|
||||||
|
|
||||||
export const UnstableProviderProvider = ({ children }) => {
|
export const UnstableProviderProvider = ({ children }) => {
|
||||||
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
|
const [client, setClient] = useState(undefined);
|
||||||
const [providerIndex, setProviderIndex] = useState(0);
|
const [status, setStatus] = useState("loading");
|
||||||
|
|
||||||
const { data: providerDetails } = useSWR("getGhostProviders", () =>
|
const [selectedExtension, setSelectedExtension] = useState(null);
|
||||||
Unstable.getSubstrateConnectExtensionProviders()
|
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(
|
try {
|
||||||
() => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.provider` : null,
|
const smoldot = startFromWorker(new SmWorker());
|
||||||
() => providerDetail ? providerDetail.provider : null
|
provider = createClientOther(
|
||||||
);
|
getSmProvider(() => smoldot.addChain({ chainSpec: casperChainSpec })),
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const account = useMemo(() => {
|
if (isMounted) {
|
||||||
if (Array.isArray(accounts)) return accounts?.at(0)?.address;
|
setClient(provider);
|
||||||
if (typeof accounts === 'string') return accounts?.address;
|
setStatus("connected");
|
||||||
return undefined
|
}
|
||||||
}, [accounts])
|
|
||||||
|
|
||||||
const connectionState = useMemo(() => {
|
} catch (error) {
|
||||||
if (!providerDetail) return 'no-extension';
|
console.error("Smoldot initialization failed:", error);
|
||||||
if (!provider) return 'loading';
|
if (isMounted) setStatus("error");
|
||||||
|
}
|
||||||
|
|
||||||
const chains = provider.getChains();
|
return () => {
|
||||||
if (chains[chainId]) return 'connected';
|
isMounted = false;
|
||||||
|
if (provider) provider.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return 'wrong-network';
|
useEffect(() => {
|
||||||
}, [providerDetail, provider, chainId]);
|
if (status !== "connected") return;
|
||||||
|
|
||||||
const client = useMemo(() => {
|
let isMounted = true;
|
||||||
if (!provider || !chainId) return undefined;
|
(async () => {
|
||||||
|
const extensions = getInjectedExtensions();
|
||||||
|
const targetExt = extensions.find(ext => ext === GHOST_CONNECT_EXTENSION);
|
||||||
|
|
||||||
const chain = provider.getChains()[chainId];
|
if (!targetExt) {
|
||||||
if (!chain) return undefined;
|
console.warn("Ghost Connect extension not found in window.injectedWeb3");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return createClient(chain.connect)
|
const ext = await connectInjectedExtension(targetExt);
|
||||||
}, [provider, chainId]);
|
if (isMounted) setSelectedExtension(ext);
|
||||||
|
})();
|
||||||
|
|
||||||
const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
|
return () => { isMounted = false; };
|
||||||
const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]);
|
}, [status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedExtension) return;
|
||||||
|
|
||||||
|
const extensionAccounts = selectedExtension.getAccounts();
|
||||||
|
setAccounts(extensionAccounts)
|
||||||
|
|
||||||
|
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(() => ({
|
const value = useMemo(() => ({
|
||||||
isExtensionMissing: connectionState === "no-extension",
|
status,
|
||||||
providerDetails,
|
|
||||||
providerDetail,
|
|
||||||
connectProviderByIndex: setProviderIndex,
|
|
||||||
chainId,
|
|
||||||
account,
|
|
||||||
client,
|
client,
|
||||||
setChainId,
|
account,
|
||||||
chainHead$
|
accounts,
|
||||||
}), [providerDetails, providerDetail, chainId, client, account, chainHead$]);
|
casperApi,
|
||||||
|
finalizedBlock$,
|
||||||
|
}), [accounts, status, client, finalizedBlock$]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnstableProvider.Provider value={value}>
|
<UnstableProvider.Provider value={value}>
|
||||||
|
|||||||
36
src/hooks/ghost/chainSpecs.js
Normal file
36
src/hooks/ghost/chainSpecs.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export const casperChainSpec = `{
|
||||||
|
"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
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}`;
|
||||||
@ -6,7 +6,7 @@ import {
|
|||||||
FTSO_ADDRESSES,
|
FTSO_ADDRESSES,
|
||||||
STNK_ADDRESSES,
|
STNK_ADDRESSES,
|
||||||
GHST_ADDRESSES,
|
GHST_ADDRESSES,
|
||||||
FTSO_DAI_LP_ADDRESSES,
|
FTSO_RESERVE_LP_ADDRESSES,
|
||||||
WETH_ADDRESSES,
|
WETH_ADDRESSES,
|
||||||
} from "../constants/addresses";
|
} from "../constants/addresses";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
@ -134,10 +134,10 @@ export const getTokenAddress = (chainId, name) => {
|
|||||||
address = GHST_ADDRESSES[chainId];
|
address = GHST_ADDRESSES[chainId];
|
||||||
break;
|
break;
|
||||||
case "GDAI_FTSO":
|
case "GDAI_FTSO":
|
||||||
address = FTSO_DAI_LP_ADDRESSES[chainId];
|
address = FTSO_RESERVE_LP_ADDRESSES[chainId];
|
||||||
break;
|
break;
|
||||||
case "RESERVE_FTSO":
|
case "RESERVE_FTSO":
|
||||||
address = FTSO_DAI_LP_ADDRESSES[chainId];
|
address = FTSO_RESERVE_LP_ADDRESSES[chainId];
|
||||||
break;
|
break;
|
||||||
case "WETH":
|
case "WETH":
|
||||||
address = WETH_ADDRESSES[chainId];
|
address = WETH_ADDRESSES[chainId];
|
||||||
@ -173,7 +173,7 @@ export const getTokenIcons = (chainId, address) => {
|
|||||||
case GHST_ADDRESSES[chainId]:
|
case GHST_ADDRESSES[chainId]:
|
||||||
icons = ["GHST"];
|
icons = ["GHST"];
|
||||||
break;
|
break;
|
||||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
case FTSO_RESERVE_LP_ADDRESSES[chainId]:
|
||||||
icons = ["WETH", "FTSO"];
|
icons = ["WETH", "FTSO"];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -185,7 +185,7 @@ export const getTokenIcons = (chainId, address) => {
|
|||||||
export const getChainTokenNames = (chainId, address) => {
|
export const getChainTokenNames = (chainId, address) => {
|
||||||
let icons = [undefined];
|
let icons = [undefined];
|
||||||
switch (address) {
|
switch (address) {
|
||||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
case FTSO_RESERVE_LP_ADDRESSES[chainId]:
|
||||||
icons = [undefined, "WETH"];
|
icons = [undefined, "WETH"];
|
||||||
break;
|
break;
|
||||||
case RESERVE_ADDRESSES[chainId]:
|
case RESERVE_ADDRESSES[chainId]:
|
||||||
@ -199,7 +199,7 @@ export const getChainTokenNames = (chainId, address) => {
|
|||||||
|
|
||||||
export const getBondNameDisplayName = (chainId, tokenAddress, baseTokenSymbol) => {
|
export const getBondNameDisplayName = (chainId, tokenAddress, baseTokenSymbol) => {
|
||||||
let stringValue = tokenNameConverter(chainId, "WETH")
|
let stringValue = tokenNameConverter(chainId, "WETH")
|
||||||
if (tokenAddress.toUpperCase() === FTSO_DAI_LP_ADDRESSES[chainId].toUpperCase()) {
|
if (tokenAddress.toUpperCase() === FTSO_RESERVE_LP_ADDRESSES[chainId].toUpperCase()) {
|
||||||
stringValue = `${baseTokenSymbol}-${stringValue} LP`;
|
stringValue = `${baseTokenSymbol}-${stringValue} LP`;
|
||||||
}
|
}
|
||||||
return stringValue;
|
return stringValue;
|
||||||
@ -214,7 +214,7 @@ export const getTokenPurchaseLink = (chainId, tokenAddress, chainName) => {
|
|||||||
purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/wrapper`;
|
purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/wrapper`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
case FTSO_RESERVE_LP_ADDRESSES[chainId]:
|
||||||
purchaseUrl += `?pool=true&from=${RESERVE_ADDRESSES[chainId]}&to=${FTSO_ADDRESSES[chainId]}`;
|
purchaseUrl += `?pool=true&from=${RESERVE_ADDRESSES[chainId]}&to=${FTSO_ADDRESSES[chainId]}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -248,6 +248,7 @@ export const executeOnChainTransaction = async ({
|
|||||||
onReplaced: () => toast(messages.replacedMsg),
|
onReplaced: () => toast(messages.replacedMsg),
|
||||||
chainId
|
chainId
|
||||||
});
|
});
|
||||||
|
// TODO: onReplaced
|
||||||
|
|
||||||
toast.success(messages.successMsg);
|
toast.success(messages.successMsg);
|
||||||
|
|
||||||
@ -258,3 +259,40 @@ export const executeOnChainTransaction = async ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const executePapiBareTransaction = async ({
|
||||||
|
client,
|
||||||
|
bareTx,
|
||||||
|
messages = {
|
||||||
|
loadingMsg: "Broadcasting transaction to available peers...",
|
||||||
|
inBlockMsg: "Transaction included in block, waiting for finality...",
|
||||||
|
successMsg: "Transaction successfully finalized!",
|
||||||
|
errorMsg: "Transaction failed.",
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const subscription = client.submitAndWatch(bareTx).subscribe({
|
||||||
|
next: (event) => {
|
||||||
|
if (event.type === "broadcasted") {
|
||||||
|
toast.success(messages.loadingMsg);
|
||||||
|
} else if (event.type === "inBestBlock") {
|
||||||
|
toast.success(messages.inBlockMsg);
|
||||||
|
} else if (event.type === "finalized") {
|
||||||
|
toast.success(messages.successMsg);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
resolve(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
toast.error(`${messages.errorMsg} ${err.message || err}`);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Bare PAPI Transaction failed:", err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -1,26 +1,117 @@
|
|||||||
import { createContext, useContext, useState, useEffect } from "react";
|
import { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
|
const BRIDGE_IN = "Bridge-In";
|
||||||
|
const BRIDGE_OUT = "Bridge-Out";
|
||||||
|
const DEX_ACTION = "DEX";
|
||||||
|
const STAKE_ACTION = "Stake";
|
||||||
|
const BOND_ACTION = "Bond";
|
||||||
|
|
||||||
|
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 PENDING_HISTORY_PREIFX = "pending";
|
||||||
|
|
||||||
const LocalStorageContext = createContext();
|
const LocalStorageContext = createContext();
|
||||||
export const useLocalStorage = () => useContext(LocalStorageContext);
|
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 }) => {
|
export const LocalStorageProvider = ({ children }) => {
|
||||||
const getStorageKey = (chainId, address, target) => {
|
const getStorageKey = (chainId, address, target) => {
|
||||||
return `${chainId}:${address}:${target}`;
|
return `${chainId}:${address}:${target}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStorageValue = (chainId, address, target, 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 key = getStorageKey(chainId, address, target);
|
||||||
const stored = localStorage.getItem(key);
|
const stored = localStorage.getItem(key);
|
||||||
return stored ? JSON.parse(stored) : defaultValue;
|
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);
|
const key = getStorageKey(chainId, address, target);
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
await mutate(["local", chainId, address, target]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearPendingHistory = async (chainId, address, target) => {
|
||||||
|
const key = getStorageKey(chainId, address, PENDING_HISTORY_PREIFX);
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
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 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) : DEFAULT_HISTORY_TYPES;
|
||||||
|
if (!historyTypes.includes(target)) return;
|
||||||
|
|
||||||
|
const key = getStorageKey(chainId, address, target);
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
const values = stored ? JSON.parse(stored) : [];
|
||||||
|
|
||||||
|
const value = { name, txHash, submitted: Date.now() };
|
||||||
|
const newValues = [value, ...values];
|
||||||
|
localStorage.setItem(key, JSON.stringify(newValues));
|
||||||
|
|
||||||
|
const pendingKey = getStorageKey(chainId, address, PENDING_HISTORY_PREIFX);
|
||||||
|
const pendingItems = localStorage.getItem(pendingKey);
|
||||||
|
const pendingValues = pendingItems ? JSON.parse(pendingItems) : {};
|
||||||
|
|
||||||
|
const currentCount = pendingValues[target] || 0;
|
||||||
|
pendingValues[target] = currentCount + 1;
|
||||||
|
localStorage.setItem(pendingKey, JSON.stringify(pendingValues));
|
||||||
|
|
||||||
|
await mutate(["local", chainId, address, PENDING_HISTORY_PREIFX]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalStorageContext.Provider value={{ getStorageValue, setStorageValue }}>
|
<LocalStorageContext.Provider value={{
|
||||||
|
CONSTANTS,
|
||||||
|
tryStoreInHistory,
|
||||||
|
useStorageValue,
|
||||||
|
clearPendingHistory,
|
||||||
|
setStorageValue
|
||||||
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</LocalStorageContext.Provider>
|
</LocalStorageContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useCurrentIndex, useGhostedSupply } from "../staking";
|
|||||||
import { useUniswapV2PairReserves } from "../uniswapv2";
|
import { useUniswapV2PairReserves } from "../uniswapv2";
|
||||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||||
import {
|
import {
|
||||||
FTSO_DAI_LP_ADDRESSES,
|
FTSO_RESERVE_LP_ADDRESSES,
|
||||||
RESERVE_ADDRESSES,
|
RESERVE_ADDRESSES,
|
||||||
FTSO_ADDRESSES,
|
FTSO_ADDRESSES,
|
||||||
CEX_TICKERS,
|
CEX_TICKERS,
|
||||||
@ -135,7 +135,7 @@ export const useReservePrice = (chainId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useFtsoPrice = (chainId) => {
|
export const useFtsoPrice = (chainId) => {
|
||||||
const { reserves, tokens, refetch } = useUniswapV2PairReserves(chainId, FTSO_DAI_LP_ADDRESSES[chainId]);
|
const { reserves, tokens, refetch } = useUniswapV2PairReserves(chainId, FTSO_RESERVE_LP_ADDRESSES[chainId]);
|
||||||
|
|
||||||
const reservePrice = useReservePrice(chainId);
|
const reservePrice = useReservePrice(chainId);
|
||||||
const reserveAddress = RESERVE_ADDRESSES[chainId];
|
const reserveAddress = RESERVE_ADDRESSES[chainId];
|
||||||
|
|||||||
@ -14,9 +14,92 @@ import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
|
|||||||
|
|
||||||
import { shorten } from "../../helpers";
|
import { shorten } from "../../helpers";
|
||||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||||
import { executeOnChainTransaction } from "../helpers";
|
import { executeOnChainTransaction, getTokenDecimals } from "../helpers";
|
||||||
import { useCirculatingSupply } from "../tokens";
|
import { useCirculatingSupply } from "../tokens";
|
||||||
|
|
||||||
|
export const useWeaverMaximumSlots = (chainId) => {
|
||||||
|
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
|
||||||
|
|
||||||
|
const { data } = useReadContract({
|
||||||
|
abi: GatekeeperAbi,
|
||||||
|
address: gatekeeperAddress,
|
||||||
|
functionName: "SLOTS",
|
||||||
|
scopeKey: `SLOTS-${chainId}-${gatekeeperAddress}`,
|
||||||
|
chainId: chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { maximumSlots: data ? data : 1n };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWeaverRoot = (chainId, blockNumber, context) => {
|
||||||
|
const session = context?.session;
|
||||||
|
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
|
||||||
|
|
||||||
|
const { data } = useReadContract({
|
||||||
|
abi: GatekeeperAbi,
|
||||||
|
address: gatekeeperAddress,
|
||||||
|
functionName: "getRoot",
|
||||||
|
args: [session, blockNumber],
|
||||||
|
scopeKey: `getRoot-${chainId}-${session}-${blockNumber}-${gatekeeperAddress}`,
|
||||||
|
chainId: chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { merkleRoot: data?.at(0), nextSession: data?.at(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWeaverProof = (chainId, blockNumber, context) => {
|
||||||
|
const index = context?.index;
|
||||||
|
const session = context?.session;
|
||||||
|
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
|
||||||
|
|
||||||
|
const { data: merkleProofInner } = useReadContract({
|
||||||
|
abi: GatekeeperAbi,
|
||||||
|
address: gatekeeperAddress,
|
||||||
|
functionName: "getProof",
|
||||||
|
args: [index, session, blockNumber],
|
||||||
|
scopeKey: `getProof-${chainId}-${session}-${index}-${blockNumber}-${gatekeeperAddress}`,
|
||||||
|
chainId: chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { merkleProof: merkleProofInner }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWeaverSlotValues = (chainId, blockNumber, context) => {
|
||||||
|
const index = context?.index;
|
||||||
|
const session = context?.session;
|
||||||
|
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
|
||||||
|
|
||||||
|
const { data: slotValuesInner } = useReadContract({
|
||||||
|
abi: GatekeeperAbi,
|
||||||
|
address: gatekeeperAddress,
|
||||||
|
functionName: "getSlotValues",
|
||||||
|
args: [index, session, blockNumber],
|
||||||
|
scopeKey: `getSlotValues-${chainId}-${session}-${index}-${blockNumber}-${gatekeeperAddress}`,
|
||||||
|
chainId: chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { slotValues: slotValuesInner }
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
export const useGatekeeperApy = (chainId) => {
|
||||||
const circulatingSupply = useCirculatingSupply(chainId, "STNK");
|
const circulatingSupply = useCirculatingSupply(chainId, "STNK");
|
||||||
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
|
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
|
||||||
@ -32,11 +115,11 @@ export const useGatekeeperApy = (chainId) => {
|
|||||||
|
|
||||||
const amountIn = new DecimalBigNumber(
|
const amountIn = new DecimalBigNumber(
|
||||||
(metadata?.amountIn ?? 0n) + (amountInHistory[chainId] ?? 0n),
|
(metadata?.amountIn ?? 0n) + (amountInHistory[chainId] ?? 0n),
|
||||||
18,
|
getTokenDecimals("GHST"),
|
||||||
);
|
);
|
||||||
const amountOut = new DecimalBigNumber(
|
const amountOut = new DecimalBigNumber(
|
||||||
(metadata?.amountOut ?? 0n) + (amountOutHistory[chainId] ?? 0n),
|
(metadata?.amountOut ?? 0n) + (amountOutHistory[chainId] ?? 0n),
|
||||||
18,
|
getTokenDecimals("GHST"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const deployedAt = metadata?.deployedAt ?? 0;
|
const deployedAt = metadata?.deployedAt ?? 0;
|
||||||
@ -181,7 +264,7 @@ export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSym
|
|||||||
successMsg: `${ftsoSymbol} tokens staked successfully! Wait for the warm-up period to claim your ${isRebase ? stnkSymbol : ghstSymbol}.`,
|
successMsg: `${ftsoSymbol} tokens staked successfully! Wait for the warm-up period to claim your ${isRebase ? stnkSymbol : ghstSymbol}.`,
|
||||||
errorMsg: "Staking transaction failed. Check logs for error detalization.",
|
errorMsg: "Staking transaction failed. Check logs for error detalization.",
|
||||||
};
|
};
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
messages,
|
messages,
|
||||||
@ -190,6 +273,7 @@ export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSym
|
|||||||
address: STAKING_ADDRESSES[chainId],
|
address: STAKING_ADDRESSES[chainId],
|
||||||
functionName: "stake",
|
functionName: "stake",
|
||||||
});
|
});
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unstake = async (chainId, account, amount, isTrigger, isRebase, ftsoSymbol, stnkSymbol, ghstSymbol) => {
|
export const unstake = async (chainId, account, amount, isTrigger, isRebase, ftsoSymbol, stnkSymbol, ghstSymbol) => {
|
||||||
@ -199,7 +283,7 @@ export const unstake = async (chainId, account, amount, isTrigger, isRebase, fts
|
|||||||
successMsg: `${isRebase ? stnkSymbol : ghstSymbol} tokens unstaked successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
|
successMsg: `${isRebase ? stnkSymbol : ghstSymbol} tokens unstaked successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
|
||||||
errorMsg: "Unstake transaction failed. Check logs for error detalization.",
|
errorMsg: "Unstake transaction failed. Check logs for error detalization.",
|
||||||
};
|
};
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
messages,
|
messages,
|
||||||
@ -208,6 +292,7 @@ export const unstake = async (chainId, account, amount, isTrigger, isRebase, fts
|
|||||||
address: STAKING_ADDRESSES[chainId],
|
address: STAKING_ADDRESSES[chainId],
|
||||||
functionName: "unstake",
|
functionName: "unstake",
|
||||||
});
|
});
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const forfeit = async (chainId, account, ftsoSymbol) => {
|
export const forfeit = async (chainId, account, ftsoSymbol) => {
|
||||||
@ -216,7 +301,7 @@ export const forfeit = async (chainId, account, ftsoSymbol) => {
|
|||||||
successMsg: `Tokens forfeited successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
|
successMsg: `Tokens forfeited successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
|
||||||
errorMsg: "Forfeit transaction failed. Check logs for error detalization.",
|
errorMsg: "Forfeit transaction failed. Check logs for error detalization.",
|
||||||
};
|
};
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
messages,
|
messages,
|
||||||
account,
|
account,
|
||||||
@ -225,6 +310,7 @@ export const forfeit = async (chainId, account, ftsoSymbol) => {
|
|||||||
address: STAKING_ADDRESSES[chainId],
|
address: STAKING_ADDRESSES[chainId],
|
||||||
functionName: "forfeit",
|
functionName: "forfeit",
|
||||||
});
|
});
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => {
|
export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => {
|
||||||
@ -234,7 +320,7 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) =>
|
|||||||
successMsg: `${isStnk ? stnkSymbol : ghstSymbol} tokens claimed successfully to ${shorten(account)}.`,
|
successMsg: `${isStnk ? stnkSymbol : ghstSymbol} tokens claimed successfully to ${shorten(account)}.`,
|
||||||
errorMsg: "Claim transaction failed. Check logs for error detalization.",
|
errorMsg: "Claim transaction failed. Check logs for error detalization.",
|
||||||
};
|
};
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
messages,
|
messages,
|
||||||
@ -243,6 +329,7 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) =>
|
|||||||
address: STAKING_ADDRESSES[chainId],
|
address: STAKING_ADDRESSES[chainId],
|
||||||
functionName: "claim",
|
functionName: "claim",
|
||||||
});
|
});
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const breakout = async (chainId, account, receiver, amount) => {
|
export const breakout = async (chainId, account, receiver, amount) => {
|
||||||
@ -261,7 +348,6 @@ export const breakout = async (chainId, account, receiver, amount) => {
|
|||||||
address: STAKING_ADDRESSES[chainId],
|
address: STAKING_ADDRESSES[chainId],
|
||||||
functionName: "breakout",
|
functionName: "breakout",
|
||||||
});
|
});
|
||||||
|
|
||||||
return txHash;
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,6 +367,7 @@ export const unwrap = async (chainId, account, amount, stnkSymbol) => {
|
|||||||
address: STAKING_ADDRESSES[chainId],
|
address: STAKING_ADDRESSES[chainId],
|
||||||
functionName: "unwrap",
|
functionName: "unwrap",
|
||||||
});
|
});
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wrap = async (chainId, account, amount, ghstSymbol) => {
|
export const wrap = async (chainId, account, amount, ghstSymbol) => {
|
||||||
@ -299,13 +386,14 @@ export const wrap = async (chainId, account, amount, ghstSymbol) => {
|
|||||||
address: STAKING_ADDRESSES[chainId],
|
address: STAKING_ADDRESSES[chainId],
|
||||||
functionName: "wrap",
|
functionName: "wrap",
|
||||||
});
|
});
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ghost = async (chainId, account, receiver, amount) => {
|
export const ghost = async (chainId, account, receiver, amount) => {
|
||||||
const args = [receiver, amount];
|
const args = [receiver, amount];
|
||||||
const messages = {
|
const messages = {
|
||||||
replacedMsg: "Bridge transaction was replaced. Wait for inclusion please.",
|
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.",
|
errorMsg: "Bridge transaction failed. Check logs for error detalization.",
|
||||||
};
|
};
|
||||||
const txHash = await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
@ -317,6 +405,5 @@ export const ghost = async (chainId, account, receiver, amount) => {
|
|||||||
address: STAKING_ADDRESSES[chainId],
|
address: STAKING_ADDRESSES[chainId],
|
||||||
functionName: "ghost",
|
functionName: "ghost",
|
||||||
});
|
});
|
||||||
|
|
||||||
return txHash;
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const swapExactETHForTokens = async ({
|
|||||||
deadline
|
deadline
|
||||||
];
|
];
|
||||||
|
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
abi: RouterAbi,
|
abi: RouterAbi,
|
||||||
@ -43,6 +43,8 @@ export const swapExactETHForTokens = async ({
|
|||||||
messages: swapMessages,
|
messages: swapMessages,
|
||||||
value: amountADesired,
|
value: amountADesired,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const swapExactTokensForETH = async ({
|
export const swapExactTokensForETH = async ({
|
||||||
@ -63,7 +65,7 @@ export const swapExactTokensForETH = async ({
|
|||||||
deadline
|
deadline
|
||||||
];
|
];
|
||||||
|
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
abi: RouterAbi,
|
abi: RouterAbi,
|
||||||
@ -72,6 +74,8 @@ export const swapExactTokensForETH = async ({
|
|||||||
account: address,
|
account: address,
|
||||||
messages: swapMessages,
|
messages: swapMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const swapExactTokensForTokens = async ({
|
export const swapExactTokensForTokens = async ({
|
||||||
@ -92,7 +96,7 @@ export const swapExactTokensForTokens = async ({
|
|||||||
deadline
|
deadline
|
||||||
];
|
];
|
||||||
|
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
abi: RouterAbi,
|
abi: RouterAbi,
|
||||||
@ -101,6 +105,8 @@ export const swapExactTokensForTokens = async ({
|
|||||||
account: address,
|
account: address,
|
||||||
messages: swapMessages,
|
messages: swapMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addLiquidity = async ({
|
export const addLiquidity = async ({
|
||||||
@ -126,7 +132,7 @@ export const addLiquidity = async ({
|
|||||||
deadline
|
deadline
|
||||||
];
|
];
|
||||||
|
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
abi: RouterAbi,
|
abi: RouterAbi,
|
||||||
@ -135,6 +141,8 @@ export const addLiquidity = async ({
|
|||||||
account: address,
|
account: address,
|
||||||
messages: addMessages,
|
messages: addMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addLiquidityETH = async ({
|
export const addLiquidityETH = async ({
|
||||||
@ -172,7 +180,7 @@ export const addLiquidityETH = async ({
|
|||||||
deadline
|
deadline
|
||||||
];
|
];
|
||||||
|
|
||||||
await executeOnChainTransaction({
|
const txHash = await executeOnChainTransaction({
|
||||||
chainId,
|
chainId,
|
||||||
args,
|
args,
|
||||||
abi: RouterAbi,
|
abi: RouterAbi,
|
||||||
@ -182,4 +190,6 @@ export const addLiquidityETH = async ({
|
|||||||
messages: addMessages,
|
messages: addMessages,
|
||||||
value: amountETHDesired,
|
value: amountETHDesired,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,12 +18,12 @@ export const darkPalette = {
|
|||||||
success: "#60C45B", // idk where this is - done
|
success: "#60C45B", // idk where this is - done
|
||||||
userFeedback: "#49A1F2", // idk where this is
|
userFeedback: "#49A1F2", // idk where this is
|
||||||
error: "#F06F73", // red negative % - done
|
error: "#F06F73", // red negative % - done
|
||||||
warning: "#ed6c02", // idk where this is - done
|
warning: "#F2E370", // idk where this is - done
|
||||||
pnlGain: "#60C45B", // green positive % - done
|
pnlGain: "#60C45B", // green positive % - done
|
||||||
},
|
},
|
||||||
bridgeProgress: {
|
bridgeProgress: {
|
||||||
error: "#F06F73",
|
error: "#F06F73",
|
||||||
warning: "#ed6c02",
|
warning: "#F2E370",
|
||||||
success: "#60C45B",
|
success: "#60C45B",
|
||||||
},
|
},
|
||||||
validatorsColor: {
|
validatorsColor: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user