Compare commits
No commits in common. "development" and "main" have entirely different histories.
developmen
...
main
3
.papi/descriptors/.gitignore
vendored
3
.papi/descriptors/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
!package.json
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"version": 0,
|
||||
"descriptorPath": ".papi\\descriptors",
|
||||
"entries": {
|
||||
"casper": {
|
||||
"metadata": "metadatas/casper_runtime.scale"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="https://ghostchain.io/wp-content/uploads/2026/06/ghost-favicon.ico" type="image/ico" />
|
||||
<link rel="icon" href="https://ipfs.io/ipfs/QmRgJ2UbzpM58te1R91bC2neWsS28F2QatBtzerM1JekyC" type="image/png" />
|
||||
<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 href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
Binary file not shown.
10
package.json
10
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ghost-dao-interface",
|
||||
"private": true,
|
||||
"version": "0.8.3",
|
||||
"version": "0.7.58",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -30,8 +30,6 @@
|
||||
"@tanstack/react-query": "^5.67.2",
|
||||
"@tanstack/react-query-devtools": "^5.67.2",
|
||||
"@wagmi/core": "^2.17.3",
|
||||
"polkadot-api": "^3.3.0",
|
||||
"@polkadot-api/descriptors": "file:.papi/descriptors",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-ga4": "^2.1.0",
|
||||
@ -57,11 +55,5 @@
|
||||
"sass-embedded": "^1.85.1",
|
||||
"vite": "^6.2.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@polkadot-api/json-rpc-provider": "0.2.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
|
||||
}
|
||||
|
||||
1420
pnpm-lock.yaml
1420
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
10
src/App.jsx
10
src/App.jsx
@ -36,12 +36,11 @@ const BondModalContainer = lazy(() => import("./containers/Bond/BondModal"));
|
||||
const StakeContainer = lazy(() => import("./containers/Stake/StakeContainer"));
|
||||
const TreasuryDashboard = lazy(() => import("./containers/TreasuryDashboard/TreasuryDashboard"));
|
||||
const Dex = lazy(() => import("./containers/Dex/Dex"));
|
||||
const BridgeIn = lazy(() => import("./containers/Bridge/BridgeIn"));
|
||||
const Bridge = lazy(() => import("./containers/Bridge/Bridge"));
|
||||
const NotFound = lazy(() => import("./containers/NotFound/NotFound"));
|
||||
const Governance = lazy(() => import("./containers/Governance/Governance"));
|
||||
const ProposalDetails = lazy(() => import("./containers/Governance/ProposalDetails"));
|
||||
const NewProposal = lazy(() => import("./containers/Governance/NewProposal"));
|
||||
const History = lazy(() => import("./containers/History/History"));
|
||||
|
||||
const PREFIX = "App";
|
||||
|
||||
@ -183,9 +182,9 @@ function App() {
|
||||
/>
|
||||
<nav className={classes.drawer}>
|
||||
{isSmallerScreen ? (
|
||||
<NavDrawer address={address} chainId={chainId} addressChainId={addressChainId} mobileOpen={mobileOpen} handleDrawerToggle={handleDrawerToggle} />
|
||||
<NavDrawer chainId={chainId} addressChainId={addressChainId} mobileOpen={mobileOpen} handleDrawerToggle={handleDrawerToggle} />
|
||||
) : (
|
||||
<Sidebar address={address} chainId={chainId} addressChainId={addressChainId} />
|
||||
<Sidebar chainId={chainId} addressChainId={addressChainId} />
|
||||
)}
|
||||
</nav>
|
||||
|
||||
@ -200,12 +199,11 @@ function App() {
|
||||
<Route path="bonds" element={<Bonds connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="bonds/:id" element={<BondModalContainer config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="stake" element={<StakeContainer config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="bridge-in" element={<BridgeIn 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="dex/:name" element={<Dex config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="governance" element={<Governance config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<Route path="governance/:id" element={<ProposalDetails config={config} connect={tryConnectInjected} address={address} chainId={addressChainId ? addressChainId : chainId} />} />
|
||||
<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 path="/empty" element={<NotFound
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -150,8 +150,7 @@ const NavItem = ({
|
||||
component={ArrowUpIcon}
|
||||
/>}
|
||||
</Box>
|
||||
|
||||
{chip && <Box pr="6px"><Chip p="18px" size="small" label={chip} template={chipColor} /></Box>}
|
||||
{chip && <Chip size="small" label={chip} template={chipColor} />}
|
||||
</Box>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@ -27,7 +27,6 @@ import HubIcon from '@mui/icons-material/Hub';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import GavelIcon from '@mui/icons-material/Gavel';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import CasinoIcon from '@mui/icons-material/Casino';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import BookIcon from '@mui/icons-material/Book';
|
||||
@ -56,7 +55,6 @@ import WifiProtectedSetupIcon from '@mui/icons-material/WifiProtectedSetup';
|
||||
|
||||
import { useTokenSymbol } from "../../hooks/tokens";
|
||||
import { useLiveBonds } from "../../hooks/bonds/index";
|
||||
import { useLocalStorage } from "../../hooks/localstorage";
|
||||
import pckg from "../../../package.json"
|
||||
|
||||
const PREFIX = "NavContent";
|
||||
@ -71,26 +69,16 @@ const StyledBox = styled(Box)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const NavContent = ({ chainId, addressChainId, address }) => {
|
||||
const NavContent = ({ chainId, addressChainId }) => {
|
||||
const { chains } = useSwitchChain();
|
||||
const chainName = useMemo(() => {
|
||||
return chains.find(chain => chain.id === chainId).name.toLowerCase();
|
||||
}, [chains, chainId, addressChainId]);
|
||||
|
||||
const { useStorageValue, CONSTANTS } = useLocalStorage();
|
||||
}, [chains, chainId, addressChainId])
|
||||
|
||||
const { liveBonds: ghostBonds } = useLiveBonds(chainId);
|
||||
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
const { data: pendingHistory } = useStorageValue(chainId, address, CONSTANTS.pendingHistoryPrefix, {})
|
||||
|
||||
const pendingTransactions = useMemo(() => {
|
||||
if (!pendingHistory) return null;
|
||||
const count = Object.values(pendingHistory).reduce((sum, count) => sum + count, 0);
|
||||
return count > 0 ? count: null;
|
||||
}, [pendingHistory]);
|
||||
|
||||
const sortedGhostBonds = useMemo(() => sortBondsByDiscount(ghostBonds), [ghostBonds]);
|
||||
const bridgeNumbers = useMemo(() => {
|
||||
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
||||
@ -157,6 +145,9 @@ const NavContent = ({ chainId, addressChainId, address }) => {
|
||||
to={`/${chainName}/bonds`}
|
||||
children={
|
||||
<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) => {
|
||||
return (
|
||||
<Link
|
||||
@ -190,10 +181,8 @@ const NavContent = ({ chainId, addressChainId, address }) => {
|
||||
</AccordionDetails>
|
||||
}
|
||||
/>
|
||||
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Bridge-In`} to={`/${chainName}/bridge-in`} />
|
||||
{/* <NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} /> */}
|
||||
<NavItem icon={HistoryIcon} chipColor="warning" chip={pendingTransactions} label="Transaction History" to={`/${chainName}/history`} />
|
||||
|
||||
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
|
||||
<NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />
|
||||
<Box className="menu-divider">
|
||||
<Divider />
|
||||
</Box>
|
||||
@ -205,6 +194,7 @@ const NavContent = ({ chainId, addressChainId, address }) => {
|
||||
|
||||
<Box>
|
||||
<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`} />
|
||||
<StyledBox display="flex" justifyContent="space-around" paddingY="24px">
|
||||
<Link href="https://git.ghostchain.io/ghostchain/ghost-dao-contracts" target="_blank" rel="noopener noreferrer">
|
||||
|
||||
@ -27,7 +27,7 @@ const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
|
||||
|
||||
const drawerWidth = 280;
|
||||
|
||||
const NavDrawer = ({ chainId, address, addressChainId, mobileOpen, handleDrawerToggle }) => {
|
||||
const NavDrawer = ({ chainId, addressChainId, mobileOpen, handleDrawerToggle }) => {
|
||||
const location = useLocation();
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
|
||||
@ -53,7 +53,7 @@ const NavDrawer = ({ chainId, address, addressChainId, mobileOpen, handleDrawerT
|
||||
disableBackdropTransition={!isIOS}
|
||||
disableDiscovery={isIOS}
|
||||
>
|
||||
<NavContent address={address} addressChainId={addressChainId} chainId={chainId} />
|
||||
<NavContent addressChainId={addressChainId} chainId={chainId} />
|
||||
</StyledSwipeableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,10 +4,10 @@ import { Drawer } from "@mui/material";
|
||||
import React from "react";
|
||||
import NavContent from "./NavContent";
|
||||
|
||||
const Sidebar = ({ chainId, addressChainId, address }) => (
|
||||
const Sidebar = ({ chainId, addressChainId }) => (
|
||||
<div className="sidebar" id="sidebarContent">
|
||||
<Drawer variant="permanent" anchor="left">
|
||||
<NavContent address={address} addressChainId={addressChainId} chainId={chainId} />
|
||||
<NavContent addressChainId={addressChainId} chainId={chainId} />
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import useSWR from 'swr';
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Box,
|
||||
@ -26,7 +25,7 @@ import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
|
||||
import { formatCurrency, shorten } from "../../../helpers";
|
||||
import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
import { RESERVE_ADDRESSES, FTSO_ADDRESSES } from "../../../constants/addresses";
|
||||
import { useUnstableProvider } from "../../../hooks/ghost"
|
||||
import { useUnstableProvider, useSystemAccount, useChainSpec } from "../../../hooks/ghost"
|
||||
|
||||
import { useAccount, useDisconnect, useSwitchChain } from "wagmi";
|
||||
|
||||
@ -114,44 +113,28 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
|
||||
const location = useLocation();
|
||||
const tokens = useWallet(chainId, address);
|
||||
|
||||
const { client, casperApi, account, isExtensionMissing } = useUnstableProvider();
|
||||
const { chains } = useSwitchChain();
|
||||
|
||||
const [currency, setCurrency] = useState("USD");
|
||||
const [accountIndex, setAccountIndex] = useState(0);
|
||||
const { account, isExtensionMissing } = useUnstableProvider();
|
||||
const { chains } = useSwitchChain();
|
||||
|
||||
const chainName = useMemo(() => {
|
||||
const chain = chains.find(chain => chain.id === chainId)
|
||||
if (!chain) return null
|
||||
return chain.name.toLowerCase()
|
||||
}, [chains, chainId]);
|
||||
}, [chains, chainId])
|
||||
|
||||
const { data: chainProperties } = useSWR(
|
||||
client ? ['chainProperties', client] : null,
|
||||
() => client.getChainSpecData(),
|
||||
{
|
||||
revalidateOnFocus: false
|
||||
}
|
||||
);
|
||||
|
||||
const { data: accountBalanceRaw } = useSWR(
|
||||
client ? ['accountBalance', client] : null,
|
||||
() => casperApi.query.System.Account.getValue(account),
|
||||
{
|
||||
refreshInterval: 12000,
|
||||
revalidateOnFocus: true
|
||||
}
|
||||
);
|
||||
const chainSpec = useChainSpec();
|
||||
const accountBalanceRaw = useSystemAccount({ account });
|
||||
|
||||
const accountBalance = useMemo(() => {
|
||||
const decimals = chainProperties ? chainProperties.properties.tokenDecimals : 18;
|
||||
const decimals = chainSpec ? chainSpec.properties.tokenDecimals : 18;
|
||||
if (!accountBalanceRaw) return new DecimalBigNumber(0n, decimals);
|
||||
const balance = accountBalanceRaw.data.free
|
||||
+ accountBalanceRaw.data.frozen
|
||||
+ accountBalanceRaw.data.reserved;
|
||||
|
||||
return new DecimalBigNumber(balance, decimals);
|
||||
}, [accountBalanceRaw, chainProperties]);
|
||||
}, [accountBalanceRaw, chainSpec])
|
||||
|
||||
const nativeGhst = useMemo(() => {
|
||||
return {
|
||||
@ -218,7 +201,7 @@ function InitialWalletView({ isWalletOpen, address, chainId, onClose }) {
|
||||
<WalletTotalValue
|
||||
currency={currency}
|
||||
setCurrency={setCurrency}
|
||||
chainName={chainProperties?.name ?? ""}
|
||||
chainName={chainSpec?.chainName ?? ""}
|
||||
address={account ?? "...wait"}
|
||||
tokens={nativeGhst}
|
||||
/>
|
||||
|
||||
@ -1,46 +1,45 @@
|
||||
import { defineChain } from 'viem'
|
||||
import { http, fallback, createConfig } from 'wagmi'
|
||||
import { hoodi } from 'wagmi/chains'
|
||||
import { sepolia, hoodi } from 'wagmi/chains'
|
||||
|
||||
// const mordor = defineChain({
|
||||
// id: 63,
|
||||
// name: 'Mordor',
|
||||
// nativeCurrency: {
|
||||
// decimals: 18,
|
||||
// name: 'METC',
|
||||
// symbol: 'mETC',
|
||||
// },
|
||||
// rpcUrls: {
|
||||
// default: { http: ['https://rpc.mordor.etccooperative.org'] },
|
||||
// },
|
||||
// blockExplorers: {
|
||||
// default: {
|
||||
// name: 'Blockscout',
|
||||
// url: 'https://etc-mordor.blockscout.com/'
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
export const config = createConfig({
|
||||
chains: [hoodi],
|
||||
transports: {
|
||||
// [sepolia.id]: fallback([
|
||||
// http('https://ethereum-sepolia-rpc.publicnode.com'),
|
||||
// http('https://1rpc.io/sepolia'),
|
||||
// http('https://eth-sepolia.public.blastapi.io'),
|
||||
// http('https://0xrpc.io/sep'),
|
||||
// http('https://api.zan.top/eth-sepolia'),
|
||||
// http('https://eth-sepolia.api.onfinality.io/public'),
|
||||
// http('https://sepolia.drpc.org')
|
||||
// ]),
|
||||
[hoodi.id]: fallback([
|
||||
http('https://rpc.hoodi.ethpandaops.io'),
|
||||
http('https://rpc.sentio.xyz/hoodi'),
|
||||
http('https://0xrpc.io/hoodi'),
|
||||
]),
|
||||
// [mordor.id]: fallback([
|
||||
// http('https://rpc.mordor.etccooperative.org'),
|
||||
// http('https://geth-mordor.etc-network.info'),
|
||||
// ])
|
||||
const mordor = defineChain({
|
||||
id: 63,
|
||||
name: 'Mordor',
|
||||
nativeCurrency: {
|
||||
decimals: 18,
|
||||
name: 'METC',
|
||||
symbol: 'mETC',
|
||||
},
|
||||
rpcUrls: {
|
||||
default: { http: ['https://rpc.mordor.etccooperative.org'] },
|
||||
},
|
||||
blockExplorers: {
|
||||
default: {
|
||||
name: 'Blockscout',
|
||||
url: 'https://etc-mordor.blockscout.com/'
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const config = createConfig({
|
||||
chains: [sepolia, hoodi, mordor],
|
||||
transports: {
|
||||
[sepolia.id]: fallback([
|
||||
http('https://ethereum-sepolia-rpc.publicnode.com'),
|
||||
http('https://1rpc.io/sepolia'),
|
||||
http('https://eth-sepolia.public.blastapi.io'),
|
||||
http('https://0xrpc.io/sep'),
|
||||
http('https://api.zan.top/eth-sepolia'),
|
||||
http('https://eth-sepolia.api.onfinality.io/public'),
|
||||
http('https://sepolia.drpc.org')
|
||||
]),
|
||||
[hoodi.id]: fallback([
|
||||
http('https://rpc.hoodi.ethpandaops.io'),
|
||||
http('https://0xrpc.io/hoodi'),
|
||||
]),
|
||||
[mordor.id]: fallback([
|
||||
http('https://rpc.mordor.etccooperative.org'),
|
||||
http('https://geth-mordor.etc-network.info'),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
@ -4,25 +4,25 @@ export const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000";
|
||||
|
||||
export const STAKING_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||
[NetworkId.TESTNET_HOODI]: "0x5d659FDEFc9c1552A3a53f661Cf4F52b0d7dDb40",
|
||||
[NetworkId.TESTNET_HOODI]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
|
||||
};
|
||||
|
||||
export const BOND_DEPOSITORY_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||
[NetworkId.TESTNET_HOODI]: "0x4105D6b48f0fEfA8033b8B01d89972c2CBd787bD",
|
||||
[NetworkId.TESTNET_HOODI]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
|
||||
};
|
||||
|
||||
export const DAO_TREASURY_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||
[NetworkId.TESTNET_HOODI]: "0xeA5113bF7e0af48B6D0Cf1a4B1e4dD0184708a09",
|
||||
[NetworkId.TESTNET_HOODI]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x05D797f9F34844594C956da58f1785997397f02E",
|
||||
};
|
||||
|
||||
export const FTSO_RESERVE_LP_ADDRESSES = {
|
||||
export const FTSO_DAI_LP_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D",
|
||||
[NetworkId.TESTNET_HOODI]: "0xf3F2cc4DaB20F09eee0d07d8456924927ceA1847",
|
||||
[NetworkId.TESTNET_HOODI]: "0x32388605b5E83Ea79CDdC479AA9939DBCF98f59D",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x53B13C4722081c405ce25c7A7629fC326A49a469",
|
||||
};
|
||||
|
||||
@ -46,43 +46,43 @@ export const WETH_ADDRESSES = {
|
||||
|
||||
export const GHST_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||
[NetworkId.TESTNET_HOODI]: "0x594a5CD093859B3FD48aA901d86F71a7D537834a",
|
||||
[NetworkId.TESTNET_HOODI]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
|
||||
};
|
||||
|
||||
export const STNK_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||
[NetworkId.TESTNET_HOODI]: "0x7a7d0F214b8C03bD962B88AC0a7F543085aB817B",
|
||||
[NetworkId.TESTNET_HOODI]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
|
||||
};
|
||||
|
||||
export const FTSO_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||
[NetworkId.TESTNET_HOODI]: "0x07427B220934B3421223969eA61a247bFA37cF03",
|
||||
[NetworkId.TESTNET_HOODI]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
|
||||
};
|
||||
|
||||
export const DISTRIBUTOR_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||
[NetworkId.TESTNET_HOODI]: "0xe503d65C9574bD3C18c8d725cD346eCBFAE24b4d",
|
||||
[NetworkId.TESTNET_HOODI]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
|
||||
};
|
||||
|
||||
export const GHOST_GOVERNANCE_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xaf7Ad1b83C47405BB9aa96868bCFbb6D65e4C2a1",
|
||||
[NetworkId.TESTNET_HOODI]: "0x0000000000000000000000000000000000000000",
|
||||
[NetworkId.TESTNET_HOODI]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
|
||||
};
|
||||
|
||||
export const BONDING_CALCULATOR_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||
[NetworkId.TESTNET_HOODI]: "0xf13A5319aD8071e4dcE7776728Ef9Cc221A5c098",
|
||||
[NetworkId.TESTNET_HOODI]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||
[NetworkId.TESTNET_MORDOR]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
|
||||
}
|
||||
|
||||
export const GATEKEEPER_ADDRESSES = {
|
||||
[NetworkId.TESTNET_SEPOLIA]: "0xd735cA07984a16911222c08411A80e24EB38869B",
|
||||
[NetworkId.TESTNET_HOODI]: "0xc9D5f50e17eeEAF39E0D0cDbbcF4B04FA4A4Bbd6",
|
||||
[NetworkId.TESTNET_HOODI]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
||||
[NetworkId.TESTNET_MORDOR]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
import { Box, Container, Skeleton, Typography, useMediaQuery } from "@mui/material";
|
||||
import { ArrowBack } from "@mui/icons-material";
|
||||
import { Box, Link, Skeleton, Typography } from "@mui/material";
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { useLocation, useParams, useNavigate } from "react-router-dom";
|
||||
import { Link as RouterLink, useLocation, useParams, useNavigate } from "react-router-dom";
|
||||
import { useAccount, useSwitchChain } from "wagmi";
|
||||
import { isAddress } from "viem";
|
||||
import ReactGA from "react-ga4";
|
||||
|
||||
import Countdown from "../../components/Countdown/Countdown";
|
||||
import Paper from "../../components/Paper/Paper";
|
||||
import PageTitle from "../../components/PageTitle/PageTitle";
|
||||
import Metric from "../../components/Metric/Metric";
|
||||
import MetricCollection from "../../components/Metric/MetricCollection";
|
||||
import TokenStack from "../../components/TokenStack/TokenStack";
|
||||
|
||||
import BondPrice from "./components/BondPrice";
|
||||
@ -42,9 +40,6 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
||||
const { network } = useParams();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||
|
||||
const [isSettingsOpen, setSettingsOpen] = useState(false);
|
||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
|
||||
@ -107,9 +102,17 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
||||
return (
|
||||
<Box>
|
||||
<PageTitle
|
||||
subtitle="We need this one just for alignment"
|
||||
name={
|
||||
<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
|
||||
chainTokenNames={preparedQuoteToken.chainNames}
|
||||
tokens={preparedQuoteToken.icons}
|
||||
@ -117,7 +120,7 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
||||
/>
|
||||
<Box display="flex" flexDirection="column" ml={1} justifyContent="center" alignItems="center">
|
||||
<Typography variant="h4" fontWeight={500}>
|
||||
{preparedQuoteToken.name} Bond
|
||||
{preparedQuoteToken.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -136,30 +139,18 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
||||
onDecimalsChange={event => setFormatDecimalsInner(event.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<Box display="flex" flexDirection="row" justifyContent="space-between" width={["100%", "70%"]} mt="24px">
|
||||
<Metric
|
||||
label={`Bond Price`}
|
||||
tooltip="Some description about what the bond price is"
|
||||
metric={bond.isSoldOut ? ( "--" ) : ( <BondPrice price={bond.price.inUsd} /> )}
|
||||
metric={
|
||||
bond.isSoldOut ? (
|
||||
"--"
|
||||
) : (
|
||||
<BondPrice
|
||||
price={bond.price.inUsd}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Metric
|
||||
label="Market Price"
|
||||
@ -167,14 +158,11 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
|
||||
/>
|
||||
<Metric
|
||||
label="Discount"
|
||||
tooltip="Some description about what the discount is"
|
||||
metric={<BondDiscount discount={bond.discount} textOnly />}
|
||||
/>
|
||||
</MetricCollection>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<Box width="100%" display="flex" justifyContent="center" alignItems="center">
|
||||
<Box width="100%" mt="24px">
|
||||
<BondInputArea
|
||||
chainId={chainId}
|
||||
bond={bond}
|
||||
|
||||
@ -17,7 +17,6 @@ import BondDiscount from "./BondDiscount";
|
||||
import BondVesting from "./BondVesting";
|
||||
import BondSlippage from "./BondSlippage";
|
||||
|
||||
import { useLocalStorage } from "../../../hooks/localstorage";
|
||||
import { purchaseBond } from "../../../hooks/bonds";
|
||||
import { useWarmupLength } from "../../../hooks/staking";
|
||||
|
||||
@ -29,32 +28,7 @@ const StyledBox = styled(Box, {
|
||||
};
|
||||
});
|
||||
|
||||
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 = ({
|
||||
const BondConfirmModal = ({
|
||||
chainId,
|
||||
bond,
|
||||
slippage,
|
||||
@ -77,8 +51,6 @@ export const BondConfirmModal = ({
|
||||
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const { tryStoreBondInHistory } = useLocalStorage();
|
||||
|
||||
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup]);
|
||||
|
||||
const AcknowledgeWarmupCheckbox = () => {
|
||||
@ -132,19 +104,21 @@ export const BondConfirmModal = ({
|
||||
const one = BigInt(shares * 100);
|
||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||
const bigIntSlippage = one + BigInt(Math.round(floatSlippage * shares));
|
||||
const maxPrice = bond.price.inBaseToken._value * bigIntSlippage / one;
|
||||
|
||||
const txHash = await purchaseBond({
|
||||
const maxPrice = bond.price.inBaseToken._value * bigIntSlippage / one;
|
||||
const referral = import.meta.env.VITE_APP_REFERRAL_ADDRESS;
|
||||
|
||||
await purchaseBond({
|
||||
chainId,
|
||||
bondId: bond.id,
|
||||
amount: spendAmountValue._value.toBigInt(),
|
||||
maxPrice,
|
||||
user: recipientAddress,
|
||||
sender,
|
||||
referral,
|
||||
isNative
|
||||
});
|
||||
|
||||
tryStoreBondInHistory(chainId, sender, txHash);
|
||||
setIsPending(false);
|
||||
handleConfirmCloseMaster();
|
||||
}
|
||||
@ -238,3 +212,5 @@ export const BondConfirmModal = ({
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default BondConfirmModal;
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
|
||||
import { Box, Container, Checkbox, FormControlLabel, Typography, useMediaQuery } from "@mui/material";
|
||||
import { Box, Checkbox, FormControlLabel, useMediaQuery } from "@mui/material";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
import { BOND_DEPOSITORY_ADDRESSES } from "../../../constants/addresses";
|
||||
import { shorten, formatNumber, formatCurrency } from "../../../helpers";
|
||||
@ -10,11 +9,10 @@ import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
|
||||
|
||||
import BondDiscount from "./BondDiscount";
|
||||
import BondVesting from "./BondVesting";
|
||||
import { BondConfirmModal, DiscountConfirmModal } from "./BondConfirmModal";
|
||||
import BondConfirmModal from "./BondConfirmModal";
|
||||
|
||||
import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard";
|
||||
import { PrimaryButton } from "../../../components/Button";
|
||||
import GhostStyledIcon from "../../../components/Icon/GhostIcon";
|
||||
import SwapCard from "../../../components/Swap/SwapCard";
|
||||
import SwapCollection from "../../../components/Swap/SwapCollection";
|
||||
import TokenStack from "../../../components/TokenStack/TokenStack";
|
||||
@ -35,7 +33,6 @@ const BondInputArea = ({
|
||||
preparedQuoteToken,
|
||||
connect
|
||||
}) => {
|
||||
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||
const isSemiSmallScreen = useMediaQuery("(max-width: 480px)");
|
||||
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||
const { pathname } = useLocation();
|
||||
@ -48,9 +45,7 @@ const BondInputArea = ({
|
||||
|
||||
const [amount, setAmount] = useState("");
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [discountConfirmed, setDiscountConfirmed] = useState(false);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [discountConfirmOpen, setDiscountConfirmOpen] = useState(false);
|
||||
|
||||
const parsedAmount = useMemo(() => {
|
||||
return new DecimalBigNumber(amount, bond.quoteToken.decimals);
|
||||
@ -65,24 +60,12 @@ const BondInputArea = ({
|
||||
return new DecimalBigNumber("0").gt(bond.discount);
|
||||
}, [bond]);
|
||||
|
||||
const setModalOpen = () => {
|
||||
if (showDisclaimer && !discountConfirmOpen && !discountConfirmed) setDiscountConfirmOpen(true);
|
||||
else setConfirmOpen(true);
|
||||
}
|
||||
|
||||
const handleConfirmClose = () => {
|
||||
setAmount("");
|
||||
setConfirmOpen(false);
|
||||
setDiscountConfirmOpen(false);
|
||||
refetch();
|
||||
}
|
||||
|
||||
const confirmDiscount = () => {
|
||||
setConfirmOpen(false);
|
||||
setDiscountConfirmOpen(false);
|
||||
setDiscountConfirmed(true);
|
||||
}
|
||||
|
||||
const setMax = useCallback(() => {
|
||||
if (!balance) return;
|
||||
|
||||
@ -112,56 +95,15 @@ const BondInputArea = ({
|
||||
}, [amountInBaseToken, baseTokenString, balance, parsedAmount]);
|
||||
|
||||
return (
|
||||
<Box sx={{ 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={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">
|
||||
<Box minHeight="calc(100vh - 210px)" display="flex" flexDirection="column" justifyContent="center">
|
||||
<Box display="flex" flexDirection="row" width="100%" justifyContent="center" mt="10px">
|
||||
<Box display="flex" flexDirection="column" width="100%" maxWidth="476px">
|
||||
<Box mb="21px">
|
||||
<SwapCollection
|
||||
iconNotNeeded
|
||||
UpperSwapCard={
|
||||
<SwapCard
|
||||
maxWidth="390px"
|
||||
maxWidth="476px"
|
||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||
id="from"
|
||||
token={
|
||||
@ -181,7 +123,7 @@ const BondInputArea = ({
|
||||
/>}
|
||||
LowerSwapCard={
|
||||
<SwapCard
|
||||
maxWidth="390px"
|
||||
maxWidth="476px"
|
||||
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
|
||||
id="to"
|
||||
token={
|
||||
@ -197,63 +139,6 @@ 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>
|
||||
|
||||
<TokenAllowanceGuard
|
||||
@ -276,18 +161,96 @@ 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
|
||||
fullWidth
|
||||
disabled={incorrectInputAmount || bond.isSoldOut || (!showDisclaimer && !checked)}
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={incorrectInputAmount || bond.isSoldOut || (showDisclaimer && !checked)}
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
>
|
||||
Bond
|
||||
</PrimaryButton>
|
||||
</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>
|
||||
</Paper>
|
||||
</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;
|
||||
|
||||
@ -75,7 +75,6 @@ const BreakoutModal = ({ chainId, address }) => {
|
||||
if (isClaimBondOpened && warmupPeriod > 0) return "Bond in Warm-up"
|
||||
}, [isStakingOpened, isClaimBondOpened, warmupPeriod]);
|
||||
|
||||
// TODO: make it global helpers
|
||||
const bridgeNumbers = useMemo(() => {
|
||||
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
|
||||
const number = 1 + connectedNetworks * 3;
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,505 +0,0 @@
|
||||
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;
|
||||
@ -1,48 +0,0 @@
|
||||
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,17 +41,12 @@ import PoolContainer from "./PoolContainer";
|
||||
import SwapContainer from "./SwapContainer";
|
||||
import TokenModal from "./TokenModal";
|
||||
|
||||
const DEADLINE_PREFIX = "dex-deadline";
|
||||
const SLIPPAGE_PREFIX = "dex-slippage";
|
||||
const DECIMALS_PREFIX = "dex-decimals";
|
||||
const DESTINATION_PREFIX = "dex-destination";
|
||||
|
||||
const Dex = ({ chainId, address, connect, config }) => {
|
||||
const location = useLocation();
|
||||
const pathname = useParams();
|
||||
|
||||
const theme = useTheme();
|
||||
const { useStorageValue, setStorageValue, CONSTANTS } = useLocalStorage();
|
||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
|
||||
const isSmallScreen = useMediaQuery("(max-width: 650px)");
|
||||
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
|
||||
@ -64,12 +59,12 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
const [topTokenListOpen, setTopTokenListOpen] = useState(false);
|
||||
const [bottomTokenListOpen, setBottomTokenListOpen] = useState(false);
|
||||
|
||||
const { data: secondsToWait } = useStorageValue(chainId, address, DEADLINE_PREFIX, "60");
|
||||
const { data: slippage } = useStorageValue(chainId, address, SLIPPAGE_PREFIX, "5");
|
||||
const { data: formatDecimals } = useStorageValue(chainId, address, DECIMALS_PREFIX, "5");
|
||||
const { data: actualDestinationAddress } = useStorageValue(chainId, address, DESTINATION_PREFIX, address);
|
||||
|
||||
const [secondsToWait, setSecondsToWait] = useState(() => getStorageValue(chainId, address, "dex-deadline", "60"));
|
||||
const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "dex-slippage", "5"));
|
||||
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "dex-decimals", "5"));
|
||||
const [actualDestinationAddress, setActualDestinationAddress] = useState(() => getStorageValue(chainId, address, "dex-destination", address));
|
||||
const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress);
|
||||
|
||||
const [tokenAddressTop, setTokenAddressTop] = useState(EMPTY_ADDRESS);
|
||||
const [tokenAddressBottom, setTokenAddressBottom] = useState(FTSO_ADDRESSES[chainId]);
|
||||
|
||||
@ -197,20 +192,23 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
return dexInfo ? dexInfo.isFork : false;
|
||||
}, [chainId, pathname])
|
||||
|
||||
const setSlippage = useCallback((value) => {
|
||||
const setSlippageInner = useCallback((value) => {
|
||||
const maybeValue = parseFloat(value);
|
||||
if (!maybeValue || parseFloat(value) <= 100) {
|
||||
setStorageValue(chainId, address, SLIPPAGE_PREFIX, value);
|
||||
setSlippage(value);
|
||||
setStorageValue(chainId, address, "dex-slippage", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
|
||||
const setSecondsToWait = useCallback((value) => {
|
||||
setStorageValue(chainId, address, DEADLINE_PREFIX, value);
|
||||
const setSecondsToWaitInner = useCallback((value) => {
|
||||
setSecondsToWait(value);
|
||||
setStorageValue(chainId, address, "dex-deadline", value);
|
||||
}, [chainId, address]);
|
||||
|
||||
const setFormatDecimals = useCallback((value) => {
|
||||
const setFormatDecimalsInner = useCallback((value) => {
|
||||
if (Number(value) <= 17) {
|
||||
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
|
||||
setFormatDecimals(value);
|
||||
setStorageValue(chainId, address, "dex-decimals", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
|
||||
@ -218,7 +216,8 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
const cleanedValue = value.trim();
|
||||
setDestinationAddress(value);
|
||||
if (isAddress(cleanedValue)) {
|
||||
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
|
||||
setActualDestinationAddress(value);
|
||||
setStorageValue(chainId, address, "dex-destination", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
|
||||
@ -279,7 +278,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
type="text"
|
||||
id="slippage-dex"
|
||||
value={slippage}
|
||||
onChange={event => setSlippage(event.currentTarget.value)}
|
||||
onChange={event => setSlippageInner(event.currentTarget.value)}
|
||||
endAdornment="%"
|
||||
/>
|
||||
</FormControl>
|
||||
@ -292,7 +291,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
</Box>
|
||||
|
||||
<Box mt="32px">
|
||||
<InputLabel htmlFor="dex-deadline">Transaction deadline</InputLabel>
|
||||
<InputLabel htmlFor="recipient">Transaction deadline</InputLabel>
|
||||
<Box mt="8px">
|
||||
<FormControl variant="outlined" color="primary" fullWidth>
|
||||
<OutlinedInput
|
||||
@ -300,7 +299,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
type="number"
|
||||
id="seconds-to-wait"
|
||||
value={secondsToWait}
|
||||
onChange={event => setSecondsToWait(event.currentTarget.value)}
|
||||
onChange={event => setSecondsToWaitInner(event.currentTarget.value)}
|
||||
endAdornment="seconds"
|
||||
/>
|
||||
</FormControl>
|
||||
@ -313,7 +312,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
</Box>
|
||||
|
||||
<Box mt="32px">
|
||||
<InputLabel htmlFor="dex-decimals">Decimal representation</InputLabel>
|
||||
<InputLabel htmlFor="recipient">Decimal representation</InputLabel>
|
||||
<Box mt="8px">
|
||||
<FormControl variant="outlined" color="primary" fullWidth>
|
||||
<OutlinedInput
|
||||
@ -321,7 +320,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
type="number"
|
||||
id="decimals-to-wait"
|
||||
value={formatDecimals}
|
||||
onChange={event => setFormatDecimals(event.currentTarget.value)}
|
||||
onChange={event => setFormatDecimalsInner(event.currentTarget.value)}
|
||||
endAdornment="decimals"
|
||||
/>
|
||||
</FormControl>
|
||||
@ -334,7 +333,7 @@ const Dex = ({ chainId, address, connect, config }) => {
|
||||
</Box>
|
||||
|
||||
<Box mt="32px">
|
||||
<InputLabel htmlFor="dex-recipient">
|
||||
<InputLabel htmlFor="recipient">
|
||||
{`${actualDestinationAddress ? "Custom" : "Default"} destination address`}
|
||||
</InputLabel>
|
||||
<Box mt="8px">
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
addLiquidity,
|
||||
addLiquidityETH,
|
||||
} from "../../hooks/uniswapv2";
|
||||
import { useLocalStorage } from "../../hooks/localstorage";
|
||||
|
||||
const PoolContainer = ({
|
||||
chainSymbol,
|
||||
@ -38,7 +37,6 @@ const PoolContainer = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery("(max-width: 456px)");
|
||||
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
|
||||
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [amountTop, setAmountTop] = useState("");
|
||||
@ -167,12 +165,7 @@ const PoolContainer = ({
|
||||
const addLiquidityInner = async () => {
|
||||
setIsPending(true);
|
||||
|
||||
const numericSecondsToWait = parseInt(secondsToWait, 10);
|
||||
const safeSecondsToWait = (!isNaN(numericSecondsToWait) && numericSecondsToWait > 0)
|
||||
? numericSecondsToWait
|
||||
: 0;
|
||||
|
||||
const deadline = Math.floor(Date.now() / 1000) + safeSecondsToWait;
|
||||
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
|
||||
const shares = 100000;
|
||||
const one = BigInt(shares * 100);
|
||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||
@ -198,16 +191,12 @@ const PoolContainer = ({
|
||||
deadline,
|
||||
}
|
||||
|
||||
let txHash;
|
||||
|
||||
if (topIsNative || bottomIsNative) {
|
||||
txHash = await addLiquidityETH(params)
|
||||
await addLiquidityETH(params)
|
||||
} else {
|
||||
txHash = await addLiquidity(params);
|
||||
await addLiquidity(params);
|
||||
}
|
||||
|
||||
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.dex.key, txHash, CONSTANTS.dex.add);
|
||||
|
||||
await balanceRefetchTop();
|
||||
await balanceRefetchBottom();
|
||||
await pairAddressRefetch();
|
||||
@ -226,7 +215,7 @@ const PoolContainer = ({
|
||||
}, [pairAddress])
|
||||
|
||||
return (
|
||||
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
|
||||
<Box maxWidth="356px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
|
||||
<SwapCollection
|
||||
UpperSwapCard={
|
||||
<SwapCard
|
||||
@ -267,7 +256,7 @@ const PoolContainer = ({
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap="0px"
|
||||
maxWidth="390px"
|
||||
maxWidth="356px"
|
||||
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
|
||||
>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
|
||||
@ -21,7 +21,6 @@ import {
|
||||
swapExactETHForTokens,
|
||||
swapExactTokensForETH,
|
||||
} from "../../hooks/uniswapv2";
|
||||
import { useLocalStorage } from "../../hooks/localstorage";
|
||||
|
||||
import { EMPTY_ADDRESS } from "../../constants/addresses";
|
||||
|
||||
@ -46,7 +45,6 @@ const SwapContainer = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery("(max-width: 456px)");
|
||||
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
|
||||
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [amountBottom, setAmountBottom] = useState("");
|
||||
@ -163,12 +161,7 @@ const SwapContainer = ({
|
||||
const swapTokens = async () => {
|
||||
setIsPending(true);
|
||||
|
||||
const numericSecondsToWait = parseInt(secondsToWait, 10);
|
||||
const safeSecondsToWait = (!isNaN(numericSecondsToWait) && numericSecondsToWait > 0)
|
||||
? numericSecondsToWait
|
||||
: 0;
|
||||
|
||||
const deadline = Math.floor(Date.now() / 1000) + safeSecondsToWait;
|
||||
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
|
||||
const shares = 100000;
|
||||
const one = BigInt(shares * 100);
|
||||
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
|
||||
@ -180,12 +173,10 @@ const SwapContainer = ({
|
||||
const amountBDesired = BigInt(Math.round(parseFloat(amountBottom) * Math.pow(10, balanceBottom._decimals)));
|
||||
const amountBMin = amountBDesired * bigIntSlippage / one;
|
||||
|
||||
let txHash;
|
||||
|
||||
if (isWrapping) {
|
||||
txHash = await depositNative(chainId, address, amountADesired);
|
||||
await depositNative(chainId, address, amountADesired);
|
||||
} else if (isUnwrapping) {
|
||||
txHash = await withdrawWeth(chainId, address, amountADesired);
|
||||
await withdrawWeth(chainId, address, amountADesired);
|
||||
} else {
|
||||
const params = {
|
||||
chainId,
|
||||
@ -199,16 +190,14 @@ const SwapContainer = ({
|
||||
};
|
||||
|
||||
if (topIsNative) {
|
||||
txHash = await swapExactETHForTokens(params)
|
||||
await swapExactETHForTokens(params)
|
||||
} else if (bottomIsNative) {
|
||||
txHash = await swapExactTokensForETH(params)
|
||||
await swapExactTokensForETH(params)
|
||||
} else {
|
||||
txHash = await swapExactTokensForTokens(params);
|
||||
await swapExactTokensForTokens(params);
|
||||
}
|
||||
}
|
||||
|
||||
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.dex.key, txHash, CONSTANTS.dex.swap);
|
||||
|
||||
await balanceRefetchTop();
|
||||
await balanceRefetchBottom();
|
||||
await pairReservesRefetch();
|
||||
@ -218,11 +207,11 @@ const SwapContainer = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
|
||||
<Box maxWidth="356px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
|
||||
<SwapCollection
|
||||
UpperSwapCard={
|
||||
<SwapCard
|
||||
maxWidth="390px"
|
||||
maxWidth="356px"
|
||||
id="from"
|
||||
token={tokenNameTop}
|
||||
chainTokenName={tokenNameTop !== chainSymbol && chainSymbol}
|
||||
@ -241,7 +230,7 @@ const SwapContainer = ({
|
||||
}
|
||||
LowerSwapCard={
|
||||
<SwapCard
|
||||
maxWidth="390px"
|
||||
maxWidth="356px"
|
||||
id="to"
|
||||
token={tokenNameBottom}
|
||||
chainTokenName={tokenNameBottom !== chainSymbol && chainSymbol}
|
||||
@ -263,7 +252,7 @@ const SwapContainer = ({
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap="0px"
|
||||
maxWidth="390px"
|
||||
maxWidth="356px"
|
||||
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
|
||||
>
|
||||
<Box width="100%" display="flex" justifyContent="space-between">
|
||||
|
||||
@ -12,7 +12,7 @@ import { ArgumentsWrapper, BooleanTrigger, ArgumentInput, ParsedCell } from "./i
|
||||
|
||||
import {
|
||||
RESERVE_ADDRESSES,
|
||||
FTSO_RESERVE_LP_ADDRESSES,
|
||||
FTSO_DAI_LP_ADDRESSES,
|
||||
BOND_DEPOSITORY_ADDRESSES,
|
||||
} from "../../../../constants/addresses";
|
||||
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]);
|
||||
|
||||
const possibleTokens = [
|
||||
{ value: FTSO_RESERVE_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_RESERVE_LP_ADDRESSES[chainId])}` },
|
||||
{ value: FTSO_DAI_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_DAI_LP_ADDRESSES[chainId])}` },
|
||||
{ value: RESERVE_ADDRESSES[chainId], symbol: reserveSymbol, label: `${reserveSymbol}: ${shorten(RESERVE_ADDRESSES[chainId])}` },
|
||||
];
|
||||
|
||||
|
||||
@ -1,660 +0,0 @@
|
||||
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;
|
||||
@ -1,192 +0,0 @@
|
||||
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;
|
||||
@ -1,23 +0,0 @@
|
||||
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;
|
||||
@ -1,452 +0,0 @@
|
||||
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;
|
||||
@ -1,49 +0,0 @@
|
||||
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;
|
||||
@ -1,160 +0,0 @@
|
||||
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,6 +78,7 @@ export const StakeContainer = ({ chainId, address, config, connect }) => {
|
||||
Rebase
|
||||
<Countdown otherwise="Ready" targetTime={epoch.end * 1000} />
|
||||
</Typography>
|
||||
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
|
||||
@ -12,7 +12,6 @@ import Token from "../../../components/Token/Token";
|
||||
import { PrimaryButton, SecondaryButton } from "../../../components/Button";
|
||||
import { TokenAllowanceGuard } from "../../../components/TokenAllowanceGuard/TokenAllowanceGuard";
|
||||
|
||||
import { useLocalStorage } from "../../../hooks/localstorage";
|
||||
import { useWarmupLength, stake, unstake, wrap, unwrap } from "../../../hooks/staking";
|
||||
import { formatNumber } from "../../../helpers";
|
||||
|
||||
@ -23,8 +22,6 @@ const StakeConfirmationModal = (props) => {
|
||||
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
|
||||
|
||||
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup])
|
||||
|
||||
const AcknowledgeWarmupCheckbox = () => {
|
||||
@ -76,13 +73,11 @@ const StakeConfirmationModal = (props) => {
|
||||
setIsPending(true);
|
||||
const actionAmount = new DecimalBigNumber(props.amount, props.spendDecimals);
|
||||
let isRebase = false;
|
||||
let txHash = null;
|
||||
let txName = null;
|
||||
|
||||
switch (props.action.toUpperCase()) {
|
||||
case "STAKE":
|
||||
isRebase = props.bottomToken === props.stnkSymbol;
|
||||
txHash = await stake(
|
||||
await stake(
|
||||
props.chainId,
|
||||
props.address,
|
||||
actionAmount._value.toBigInt(),
|
||||
@ -92,11 +87,10 @@ const StakeConfirmationModal = (props) => {
|
||||
props.stnkSymbol,
|
||||
props.ghstSymbol
|
||||
);
|
||||
txName = CONSTANTS.stake.stake;
|
||||
break;
|
||||
case "UNSTAKE":
|
||||
isRebase = props.upperToken === props.stnkSymbol;
|
||||
txHash = await unstake(
|
||||
await unstake(
|
||||
props.chainId,
|
||||
props.address,
|
||||
actionAmount._value.toBigInt(),
|
||||
@ -106,32 +100,27 @@ const StakeConfirmationModal = (props) => {
|
||||
props.stnkSymbol,
|
||||
props.ghstSymbol
|
||||
);
|
||||
txName = CONSTANTS.stake.unstake;
|
||||
break;
|
||||
case "WRAP":
|
||||
txHash = await wrap(
|
||||
await wrap(
|
||||
props.chainId,
|
||||
props.address,
|
||||
actionAmount._value.toBigInt(),
|
||||
props.ghstSymbol
|
||||
);
|
||||
txName = CONSTANTS.stake.wrap;
|
||||
break;
|
||||
case "UNWRAP":
|
||||
txHash = await unwrap(
|
||||
await unwrap(
|
||||
props.chainId,
|
||||
props.address,
|
||||
actionAmount._value.toBigInt(),
|
||||
props.stnkSymbol
|
||||
);
|
||||
txName = CONSTANTS.stake.unwrap;
|
||||
break;
|
||||
default:
|
||||
console.log("Wrong action to be executed!");
|
||||
}
|
||||
|
||||
if (txHash && txName) tryStoreInHistory(props.chainId, props.address, CONSTANTS.stake.key, txHash, txName);
|
||||
|
||||
setIsPending(false);
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
@ -67,10 +67,6 @@ const StyledBox = styled(Box)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const DECIMALS_PREFIX = "stake-decimals";
|
||||
const IS_CLAIM_PREFIX = "stake-isClaim";
|
||||
const IS_TRIGGER_PREFIX = "stake-isTrigger";
|
||||
|
||||
export const StakeInputArea = ({
|
||||
chainSymbol,
|
||||
upperToken,
|
||||
@ -88,11 +84,11 @@ export const StakeInputArea = ({
|
||||
const [bottomTokenModalOpen, setBottomTokenModalOpen] = useState(false);
|
||||
const [confirmationModalOpen, setConfirmationModalOpen] = useState(false);
|
||||
|
||||
const { useStorageValue, setStorageValue } = useLocalStorage();
|
||||
const { getStorageValue, setStorageValue } = useLocalStorage();
|
||||
|
||||
const { data: formatDecimals } = useStorageValue(chainId, address, DECIMALS_PREFIX, "5");
|
||||
const { data: isClaim } = useStorageValue(chainId, address, IS_CLAIM_PREFIX, true);
|
||||
const { data: isTrigger } = useStorageValue(chainId, address, IS_TRIGGER_PREFIX, true);
|
||||
const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "stake-decimals", "5"));
|
||||
const [isClaim, setIsClaim] = useState(() => getStorageValue(chainId, address, "stake-isClaim", true));
|
||||
const [isTrigger, setIsTrigger] = useState(() => getStorageValue(chainId, address, "stake-isTrigger", true));
|
||||
|
||||
const [amount, setAmount] = useState("");
|
||||
const [receiveAmount, setReceiveAmount] = useState("");
|
||||
@ -107,17 +103,20 @@ export const StakeInputArea = ({
|
||||
const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK");
|
||||
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
|
||||
|
||||
const setIsClaim = useCallback((value) => {
|
||||
setStorageValue(chainId, address, IS_CLAIM_PREFIX, value);
|
||||
const setIsClaimInner = useCallback((value) => {
|
||||
setIsClaim(value);
|
||||
setStorageValue(chainId, address, "stake-isClaim", value);
|
||||
}, [chainId, address]);
|
||||
|
||||
const setIsTrigger = useCallback((value) => {
|
||||
setStorageValue(chainId, address, IS_TRIGGER_PREFIX, value);
|
||||
const setIsTriggerInner = useCallback((value) => {
|
||||
setIsTrigger(value);
|
||||
setStorageValue(chainId, address, "stake-isTrigger", value);
|
||||
}, [chainId, address]);
|
||||
|
||||
const setFormatDecimals = useCallback((value) => {
|
||||
const setFormatDecimalsInner = useCallback((value) => {
|
||||
if (Number(value) <= 17) {
|
||||
setStorageValue(chainId, address, DECIMALS_PREFIX, value);
|
||||
setFormatDecimals(value);
|
||||
setStorageValue(chainId, address, "stake-decimals", value);
|
||||
}
|
||||
}, [chainId, address]);
|
||||
|
||||
@ -252,11 +251,11 @@ export const StakeInputArea = ({
|
||||
open={settingsModalOpen}
|
||||
onClose={closeSettingModal}
|
||||
formatDecimals={formatDecimals}
|
||||
onFormatDecimalsChange={event => setFormatDecimals(event.currentTarget.value)}
|
||||
onFormatDecimalsChange={event => setFormatDecimalsInner(event.currentTarget.value)}
|
||||
isClaim={isClaim}
|
||||
onIsClaimChanged={setIsClaim}
|
||||
onIsClaimChanged={setIsClaimInner}
|
||||
isTrigger={isTrigger}
|
||||
onTriggerRebaseChange={setIsTrigger}
|
||||
onTriggerRebaseChange={setIsTriggerInner}
|
||||
/>
|
||||
<StakeConfirmationModal
|
||||
chainSymbol={chainSymbol}
|
||||
|
||||
@ -273,26 +273,24 @@ export const useNotes = (chainId, address) => {
|
||||
return { notes, refetch };
|
||||
}
|
||||
|
||||
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, isNative }) => {
|
||||
const args = [bondId, amount, maxPrice, user];
|
||||
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => {
|
||||
const args = [bondId, amount, maxPrice, user, referral];
|
||||
const messages = {
|
||||
replacedMsg: "Bond transaction was replaced. Wait for inclusion please.",
|
||||
successMsg: `Bond successfully purchased for ${shorten(user)}! Wait until it'll mature before claim.`,
|
||||
errorMsg: "Bond transaction failed. Check logs for error detalization.",
|
||||
};
|
||||
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
messages,
|
||||
abi: BondAbi,
|
||||
address: BOND_DEPOSITORY_ADDRESSES[chainId],
|
||||
functionName: "deposit",
|
||||
account: sender,
|
||||
account: user,
|
||||
value: isNative ? amount : undefined
|
||||
});
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const redeem = async ({ chainId, user, isGhst, indexes }) => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { createContext, useContext, useState, useMemo } from "react";
|
||||
import { claim } from "../../hooks/staking";
|
||||
|
||||
const emptyFunction = () => {};
|
||||
const BreakoutModalContext = createContext();
|
||||
|
||||
@ -2,102 +2,76 @@ import { createContext, useEffect, useContext, useState, useMemo, useCallback, u
|
||||
import { Unstable } from "@substrate/connect-discovery"
|
||||
import { createClient } from "@polkadot-api/substrate-client"
|
||||
import { getObservableClient } from "@polkadot-api/observable-client"
|
||||
import { getInjectedExtensions, connectInjectedExtension } from "polkadot-api/pjs-signer";
|
||||
import useSWR from "swr"
|
||||
|
||||
import { createClient as createClientOther } from "polkadot-api";
|
||||
import { getSmProvider } from "polkadot-api/sm-provider"
|
||||
import { startFromWorker } from "polkadot-api/smoldot/from-worker"
|
||||
import SmWorker from "polkadot-api/smoldot/worker?worker"
|
||||
|
||||
import { casper as casperMetadata } from "@polkadot-api/descriptors";
|
||||
import { casperChainSpec } from "./chainSpecs";
|
||||
|
||||
const GHOST_CONNECT_EXTENSION = "GHOST Connect Browser Extension"
|
||||
const MAX_BLOCK_TIMEOUT = 15000
|
||||
const DEFAULT_CHAIN_ID = "0xcaf9c5ddccc7ba6f96149a6b8a81c26a3641742795cd5e7e0aee79bf3f4e77b7"
|
||||
const DEFAULT_CHAIN_ID = "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3"
|
||||
const UnstableProvider = createContext(null)
|
||||
export const useUnstableProvider = () => useContext(UnstableProvider)
|
||||
|
||||
export const UnstableProviderProvider = ({ children }) => {
|
||||
const [client, setClient] = useState(undefined);
|
||||
const [status, setStatus] = useState("loading");
|
||||
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
|
||||
const [providerIndex, setProviderIndex] = useState(0);
|
||||
|
||||
const [selectedExtension, setSelectedExtension] = useState(null);
|
||||
const [selectedAccountIndex, setSelectedAccountIndex] = useState(0);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const { data: providerDetails } = useSWR("getGhostProviders", () =>
|
||||
Unstable.getSubstrateConnectExtensionProviders()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
let provider = null;
|
||||
const providerDetail = useMemo(() => providerDetails?.at(providerIndex), [providerDetails, providerIndex]);
|
||||
|
||||
try {
|
||||
const smoldot = startFromWorker(new SmWorker());
|
||||
provider = createClientOther(
|
||||
getSmProvider(() => smoldot.addChain({ chainSpec: casperChainSpec })),
|
||||
const { data: provider } = useSWR(
|
||||
() => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.provider` : null,
|
||||
() => providerDetail ? providerDetail.provider : null
|
||||
);
|
||||
|
||||
const { data: accounts } = useSWR(
|
||||
() => providerDetail ? `providerDetail.${providerDetail.info.uuid}.provider.getAccounts(${chainId})` : null,
|
||||
async () => {
|
||||
if (!providerDetail) return []
|
||||
const activeProvider = await providerDetail.provider;
|
||||
return activeProvider.getAccounts(chainId);
|
||||
},
|
||||
)
|
||||
|
||||
if (isMounted) {
|
||||
setClient(provider);
|
||||
setStatus("connected");
|
||||
}
|
||||
const account = useMemo(() => {
|
||||
if (Array.isArray(accounts)) return accounts?.at(0)?.address;
|
||||
if (typeof accounts === 'string') return accounts?.address;
|
||||
return undefined
|
||||
}, [accounts])
|
||||
|
||||
} catch (error) {
|
||||
console.error("Smoldot initialization failed:", error);
|
||||
if (isMounted) setStatus("error");
|
||||
}
|
||||
const connectionState = useMemo(() => {
|
||||
if (!providerDetail) return 'no-extension';
|
||||
if (!provider) return 'loading';
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (provider) provider.destroy();
|
||||
};
|
||||
}, []);
|
||||
const chains = provider.getChains();
|
||||
if (chains[chainId]) return 'connected';
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== "connected") return;
|
||||
return 'wrong-network';
|
||||
}, [providerDetail, provider, chainId]);
|
||||
|
||||
let isMounted = true;
|
||||
(async () => {
|
||||
const extensions = getInjectedExtensions();
|
||||
const targetExt = extensions.find(ext => ext === GHOST_CONNECT_EXTENSION);
|
||||
const client = useMemo(() => {
|
||||
if (!provider || !chainId) return undefined;
|
||||
|
||||
if (!targetExt) {
|
||||
console.warn("Ghost Connect extension not found in window.injectedWeb3");
|
||||
return;
|
||||
}
|
||||
const chain = provider.getChains()[chainId];
|
||||
if (!chain) return undefined;
|
||||
|
||||
const ext = await connectInjectedExtension(targetExt);
|
||||
if (isMounted) setSelectedExtension(ext);
|
||||
})();
|
||||
return createClient(chain.connect)
|
||||
}, [provider, chainId]);
|
||||
|
||||
return () => { isMounted = false; };
|
||||
}, [status]);
|
||||
|
||||
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 observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
|
||||
const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
status,
|
||||
client,
|
||||
isExtensionMissing: connectionState === "no-extension",
|
||||
providerDetails,
|
||||
providerDetail,
|
||||
connectProviderByIndex: setProviderIndex,
|
||||
chainId,
|
||||
account,
|
||||
accounts,
|
||||
casperApi,
|
||||
finalizedBlock$,
|
||||
}), [accounts, status, client, finalizedBlock$]);
|
||||
client,
|
||||
setChainId,
|
||||
chainHead$
|
||||
}), [providerDetails, providerDetail, chainId, client, account, chainHead$]);
|
||||
|
||||
return (
|
||||
<UnstableProvider.Provider value={value}>
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
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,
|
||||
STNK_ADDRESSES,
|
||||
GHST_ADDRESSES,
|
||||
FTSO_RESERVE_LP_ADDRESSES,
|
||||
FTSO_DAI_LP_ADDRESSES,
|
||||
WETH_ADDRESSES,
|
||||
} from "../constants/addresses";
|
||||
import { config } from "../config";
|
||||
@ -134,10 +134,10 @@ export const getTokenAddress = (chainId, name) => {
|
||||
address = GHST_ADDRESSES[chainId];
|
||||
break;
|
||||
case "GDAI_FTSO":
|
||||
address = FTSO_RESERVE_LP_ADDRESSES[chainId];
|
||||
address = FTSO_DAI_LP_ADDRESSES[chainId];
|
||||
break;
|
||||
case "RESERVE_FTSO":
|
||||
address = FTSO_RESERVE_LP_ADDRESSES[chainId];
|
||||
address = FTSO_DAI_LP_ADDRESSES[chainId];
|
||||
break;
|
||||
case "WETH":
|
||||
address = WETH_ADDRESSES[chainId];
|
||||
@ -173,7 +173,7 @@ export const getTokenIcons = (chainId, address) => {
|
||||
case GHST_ADDRESSES[chainId]:
|
||||
icons = ["GHST"];
|
||||
break;
|
||||
case FTSO_RESERVE_LP_ADDRESSES[chainId]:
|
||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
||||
icons = ["WETH", "FTSO"];
|
||||
break;
|
||||
default:
|
||||
@ -185,7 +185,7 @@ export const getTokenIcons = (chainId, address) => {
|
||||
export const getChainTokenNames = (chainId, address) => {
|
||||
let icons = [undefined];
|
||||
switch (address) {
|
||||
case FTSO_RESERVE_LP_ADDRESSES[chainId]:
|
||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
||||
icons = [undefined, "WETH"];
|
||||
break;
|
||||
case RESERVE_ADDRESSES[chainId]:
|
||||
@ -199,7 +199,7 @@ export const getChainTokenNames = (chainId, address) => {
|
||||
|
||||
export const getBondNameDisplayName = (chainId, tokenAddress, baseTokenSymbol) => {
|
||||
let stringValue = tokenNameConverter(chainId, "WETH")
|
||||
if (tokenAddress.toUpperCase() === FTSO_RESERVE_LP_ADDRESSES[chainId].toUpperCase()) {
|
||||
if (tokenAddress.toUpperCase() === FTSO_DAI_LP_ADDRESSES[chainId].toUpperCase()) {
|
||||
stringValue = `${baseTokenSymbol}-${stringValue} LP`;
|
||||
}
|
||||
return stringValue;
|
||||
@ -214,7 +214,7 @@ export const getTokenPurchaseLink = (chainId, tokenAddress, chainName) => {
|
||||
purchaseUrl = `https://app.dao.ghostchain.io/#/${chainName}/wrapper`;
|
||||
}
|
||||
break;
|
||||
case FTSO_RESERVE_LP_ADDRESSES[chainId]:
|
||||
case FTSO_DAI_LP_ADDRESSES[chainId]:
|
||||
purchaseUrl += `?pool=true&from=${RESERVE_ADDRESSES[chainId]}&to=${FTSO_ADDRESSES[chainId]}`;
|
||||
break;
|
||||
}
|
||||
@ -248,7 +248,6 @@ export const executeOnChainTransaction = async ({
|
||||
onReplaced: () => toast(messages.replacedMsg),
|
||||
chainId
|
||||
});
|
||||
// TODO: onReplaced
|
||||
|
||||
toast.success(messages.successMsg);
|
||||
|
||||
@ -259,40 +258,3 @@ export const executeOnChainTransaction = async ({
|
||||
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,117 +1,26 @@
|
||||
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();
|
||||
export const useLocalStorage = () => useContext(LocalStorageContext);
|
||||
|
||||
const CONSTANTS = {
|
||||
possibleHistoryTypes: POSSIBLE_HISTORY_TYPES,
|
||||
defaultHistoryTypes: DEFAULT_HISTORY_TYPES,
|
||||
historyPrefix: HISTORY_PREFIX,
|
||||
pendingHistoryPrefix: PENDING_HISTORY_PREIFX,
|
||||
|
||||
bridgeIn: { key: BRIDGE_IN, name: BRIDGE_IN },
|
||||
bridgeOut: { key: BRIDGE_OUT, name: BRIDGE_OUT },
|
||||
dex: { key: DEX_ACTION, swap: DEX_ACTION_SWAP, add: DEX_ACTION_ADD },
|
||||
stake: {
|
||||
key: STAKE_ACTION,
|
||||
stake: STAKE_ACTION_STAKE,
|
||||
unstake: UNSTAKE_ACTION_STAKE,
|
||||
wrap: WRAP_ACTION_STAKE,
|
||||
unwrap: UNWRAP_ACTION_STAKE,
|
||||
},
|
||||
};
|
||||
|
||||
export const LocalStorageProvider = ({ children }) => {
|
||||
const getStorageKey = (chainId, address, target) => {
|
||||
return `${chainId}:${address}:${target}`;
|
||||
}
|
||||
|
||||
const useStorageValue = (chainId, address, target, defaultValue) => {
|
||||
return useSWR(
|
||||
chainId && address ? ["local", chainId, address, target] : null,
|
||||
([_, chainId, address, target]) => {
|
||||
const getStorageValue = (chainId, address, target, defaultValue) => {
|
||||
const key = getStorageKey(chainId, address, target);
|
||||
const stored = localStorage.getItem(key);
|
||||
return stored ? JSON.parse(stored) : defaultValue;
|
||||
},
|
||||
{ revalidateOnFocus: false, fallbackData: defaultValue }
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const setStorageValue = async (chainId, address, target, value) => {
|
||||
const setStorageValue = (chainId, address, target, value) => {
|
||||
const key = getStorageKey(chainId, address, target);
|
||||
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 (
|
||||
<LocalStorageContext.Provider value={{
|
||||
CONSTANTS,
|
||||
tryStoreInHistory,
|
||||
useStorageValue,
|
||||
clearPendingHistory,
|
||||
setStorageValue
|
||||
}}>
|
||||
<LocalStorageContext.Provider value={{ getStorageValue, setStorageValue }}>
|
||||
{children}
|
||||
</LocalStorageContext.Provider>
|
||||
)
|
||||
|
||||
@ -5,7 +5,7 @@ import { useCurrentIndex, useGhostedSupply } from "../staking";
|
||||
import { useUniswapV2PairReserves } from "../uniswapv2";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import {
|
||||
FTSO_RESERVE_LP_ADDRESSES,
|
||||
FTSO_DAI_LP_ADDRESSES,
|
||||
RESERVE_ADDRESSES,
|
||||
FTSO_ADDRESSES,
|
||||
CEX_TICKERS,
|
||||
@ -135,7 +135,7 @@ export const useReservePrice = (chainId) => {
|
||||
};
|
||||
|
||||
export const useFtsoPrice = (chainId) => {
|
||||
const { reserves, tokens, refetch } = useUniswapV2PairReserves(chainId, FTSO_RESERVE_LP_ADDRESSES[chainId]);
|
||||
const { reserves, tokens, refetch } = useUniswapV2PairReserves(chainId, FTSO_DAI_LP_ADDRESSES[chainId]);
|
||||
|
||||
const reservePrice = useReservePrice(chainId);
|
||||
const reserveAddress = RESERVE_ADDRESSES[chainId];
|
||||
|
||||
@ -14,92 +14,9 @@ import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
|
||||
|
||||
import { shorten } from "../../helpers";
|
||||
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
|
||||
import { executeOnChainTransaction, getTokenDecimals } from "../helpers";
|
||||
import { executeOnChainTransaction } from "../helpers";
|
||||
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) => {
|
||||
const circulatingSupply = useCirculatingSupply(chainId, "STNK");
|
||||
const { gatekeeperAddress } = useGatekeeperAddress(chainId);
|
||||
@ -115,11 +32,11 @@ export const useGatekeeperApy = (chainId) => {
|
||||
|
||||
const amountIn = new DecimalBigNumber(
|
||||
(metadata?.amountIn ?? 0n) + (amountInHistory[chainId] ?? 0n),
|
||||
getTokenDecimals("GHST"),
|
||||
18,
|
||||
);
|
||||
const amountOut = new DecimalBigNumber(
|
||||
(metadata?.amountOut ?? 0n) + (amountOutHistory[chainId] ?? 0n),
|
||||
getTokenDecimals("GHST"),
|
||||
18,
|
||||
);
|
||||
|
||||
const deployedAt = metadata?.deployedAt ?? 0;
|
||||
@ -264,7 +181,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}.`,
|
||||
errorMsg: "Staking transaction failed. Check logs for error detalization.",
|
||||
};
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
messages,
|
||||
@ -273,7 +190,6 @@ export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSym
|
||||
address: STAKING_ADDRESSES[chainId],
|
||||
functionName: "stake",
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const unstake = async (chainId, account, amount, isTrigger, isRebase, ftsoSymbol, stnkSymbol, ghstSymbol) => {
|
||||
@ -283,7 +199,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)}.`,
|
||||
errorMsg: "Unstake transaction failed. Check logs for error detalization.",
|
||||
};
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
messages,
|
||||
@ -292,7 +208,6 @@ export const unstake = async (chainId, account, amount, isTrigger, isRebase, fts
|
||||
address: STAKING_ADDRESSES[chainId],
|
||||
functionName: "unstake",
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const forfeit = async (chainId, account, ftsoSymbol) => {
|
||||
@ -301,7 +216,7 @@ export const forfeit = async (chainId, account, ftsoSymbol) => {
|
||||
successMsg: `Tokens forfeited successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
|
||||
errorMsg: "Forfeit transaction failed. Check logs for error detalization.",
|
||||
};
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
messages,
|
||||
account,
|
||||
@ -310,7 +225,6 @@ export const forfeit = async (chainId, account, ftsoSymbol) => {
|
||||
address: STAKING_ADDRESSES[chainId],
|
||||
functionName: "forfeit",
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => {
|
||||
@ -320,7 +234,7 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) =>
|
||||
successMsg: `${isStnk ? stnkSymbol : ghstSymbol} tokens claimed successfully to ${shorten(account)}.`,
|
||||
errorMsg: "Claim transaction failed. Check logs for error detalization.",
|
||||
};
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
messages,
|
||||
@ -329,7 +243,6 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) =>
|
||||
address: STAKING_ADDRESSES[chainId],
|
||||
functionName: "claim",
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const breakout = async (chainId, account, receiver, amount) => {
|
||||
@ -348,6 +261,7 @@ export const breakout = async (chainId, account, receiver, amount) => {
|
||||
address: STAKING_ADDRESSES[chainId],
|
||||
functionName: "breakout",
|
||||
});
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
@ -367,7 +281,6 @@ export const unwrap = async (chainId, account, amount, stnkSymbol) => {
|
||||
address: STAKING_ADDRESSES[chainId],
|
||||
functionName: "unwrap",
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const wrap = async (chainId, account, amount, ghstSymbol) => {
|
||||
@ -386,14 +299,13 @@ export const wrap = async (chainId, account, amount, ghstSymbol) => {
|
||||
address: STAKING_ADDRESSES[chainId],
|
||||
functionName: "wrap",
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const ghost = async (chainId, account, receiver, amount) => {
|
||||
const args = [receiver, amount];
|
||||
const messages = {
|
||||
replacedMsg: "Bridge transaction was replaced. Wait for inclusion please.",
|
||||
successMsg: `Amount successfully bridged! Check Transaction History to trace the progress.`,
|
||||
successMsg: `Amount successfully bridged! Check tx hash status or wait for slow clap finalization.`,
|
||||
errorMsg: "Bridge transaction failed. Check logs for error detalization.",
|
||||
};
|
||||
const txHash = await executeOnChainTransaction({
|
||||
@ -405,5 +317,6 @@ export const ghost = async (chainId, account, receiver, amount) => {
|
||||
address: STAKING_ADDRESSES[chainId],
|
||||
functionName: "ghost",
|
||||
});
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ export const swapExactETHForTokens = async ({
|
||||
deadline
|
||||
];
|
||||
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
abi: RouterAbi,
|
||||
@ -43,8 +43,6 @@ export const swapExactETHForTokens = async ({
|
||||
messages: swapMessages,
|
||||
value: amountADesired,
|
||||
});
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const swapExactTokensForETH = async ({
|
||||
@ -65,7 +63,7 @@ export const swapExactTokensForETH = async ({
|
||||
deadline
|
||||
];
|
||||
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
abi: RouterAbi,
|
||||
@ -74,8 +72,6 @@ export const swapExactTokensForETH = async ({
|
||||
account: address,
|
||||
messages: swapMessages,
|
||||
});
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const swapExactTokensForTokens = async ({
|
||||
@ -96,7 +92,7 @@ export const swapExactTokensForTokens = async ({
|
||||
deadline
|
||||
];
|
||||
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
abi: RouterAbi,
|
||||
@ -105,8 +101,6 @@ export const swapExactTokensForTokens = async ({
|
||||
account: address,
|
||||
messages: swapMessages,
|
||||
});
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const addLiquidity = async ({
|
||||
@ -132,7 +126,7 @@ export const addLiquidity = async ({
|
||||
deadline
|
||||
];
|
||||
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
abi: RouterAbi,
|
||||
@ -141,8 +135,6 @@ export const addLiquidity = async ({
|
||||
account: address,
|
||||
messages: addMessages,
|
||||
});
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
export const addLiquidityETH = async ({
|
||||
@ -180,7 +172,7 @@ export const addLiquidityETH = async ({
|
||||
deadline
|
||||
];
|
||||
|
||||
const txHash = await executeOnChainTransaction({
|
||||
await executeOnChainTransaction({
|
||||
chainId,
|
||||
args,
|
||||
abi: RouterAbi,
|
||||
@ -190,6 +182,4 @@ export const addLiquidityETH = async ({
|
||||
messages: addMessages,
|
||||
value: amountETHDesired,
|
||||
});
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
@ -18,12 +18,12 @@ export const darkPalette = {
|
||||
success: "#60C45B", // idk where this is - done
|
||||
userFeedback: "#49A1F2", // idk where this is
|
||||
error: "#F06F73", // red negative % - done
|
||||
warning: "#F2E370", // idk where this is - done
|
||||
warning: "#ed6c02", // idk where this is - done
|
||||
pnlGain: "#60C45B", // green positive % - done
|
||||
},
|
||||
bridgeProgress: {
|
||||
error: "#F06F73",
|
||||
warning: "#F2E370",
|
||||
warning: "#ed6c02",
|
||||
success: "#60C45B",
|
||||
},
|
||||
validatorsColor: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user