Compare commits

..

6 Commits

Author SHA1 Message Date
ce6e316feb
make bridge-in to be stored in history in any case
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-11-07 15:29:14 +03:00
e42f5db893
avoid unmounts during bridge-in because if useSWR
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-11-07 15:07:22 +03:00
b09c6b82f7
make small UI fixes for the bridge-in page
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-11-07 14:21:32 +03:00
42d5f82040
bridge-in flow finished based on new weaver logic
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-11-06 17:23:41 +03:00
132bd0986b
migrate to PAPI and integrate weaver protocol
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-11-05 21:59:19 +03:00
314abe1aba
additional UI revision; change of the bond purchase and history added
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-11-04 13:03:18 +03:00
46 changed files with 4343 additions and 526 deletions

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

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

View File

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

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

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

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="https://ipfs.io/ipfs/QmRgJ2UbzpM58te1R91bC2neWsS28F2QatBtzerM1JekyC" type="image/png" />
<link rel="icon" href="https://ghostchain.io/wp-content/uploads/2026/06/ghost-favicon.ico" type="image/ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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.

View File

@ -1,7 +1,7 @@
{
"name": "ghost-dao-interface",
"private": true,
"version": "0.7.58",
"version": "0.8.3",
"type": "module",
"scripts": {
"dev": "vite",
@ -30,6 +30,8 @@
"@tanstack/react-query": "^5.67.2",
"@tanstack/react-query-devtools": "^5.67.2",
"@wagmi/core": "^2.17.3",
"polkadot-api": "^3.3.0",
"@polkadot-api/descriptors": "file:.papi/descriptors",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-ga4": "^2.1.0",
@ -55,5 +57,11 @@
"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"
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -27,6 +27,7 @@ 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';
@ -55,6 +56,7 @@ import WifiProtectedSetupIcon from '@mui/icons-material/WifiProtectedSetup';
import { useTokenSymbol } from "../../hooks/tokens";
import { useLiveBonds } from "../../hooks/bonds/index";
import { useLocalStorage } from "../../hooks/localstorage";
import pckg from "../../../package.json"
const PREFIX = "NavContent";
@ -69,16 +71,26 @@ const StyledBox = styled(Box)(({ theme }) => ({
},
}));
const NavContent = ({ chainId, addressChainId }) => {
const NavContent = ({ chainId, addressChainId, address }) => {
const { chains } = useSwitchChain();
const chainName = useMemo(() => {
return chains.find(chain => chain.id === chainId).name.toLowerCase();
}, [chains, chainId, addressChainId])
}, [chains, chainId, addressChainId]);
const { useStorageValue, CONSTANTS } = useLocalStorage();
const { liveBonds: ghostBonds } = useLiveBonds(chainId);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { data: pendingHistory } = useStorageValue(chainId, address, CONSTANTS.pendingHistoryPrefix, {})
const pendingTransactions = useMemo(() => {
if (!pendingHistory) return null;
const count = Object.values(pendingHistory).reduce((sum, count) => sum + count, 0);
return count > 0 ? count: null;
}, [pendingHistory]);
const sortedGhostBonds = useMemo(() => sortBondsByDiscount(ghostBonds), [ghostBonds]);
const bridgeNumbers = useMemo(() => {
const connectedNetworks = Object.keys(GATEKEEPER_ADDRESSES).length;
@ -145,9 +157,6 @@ const NavContent = ({ chainId, addressChainId }) => {
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
@ -181,8 +190,10 @@ const NavContent = ({ chainId, addressChainId }) => {
</AccordionDetails>
}
/>
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Stake\u00B2`} to={`/${chainName}/bridge`} />
<NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} />
<NavItem icon={ForkRightIcon} label={`${bridgeNumbers} Bridge-In`} to={`/${chainName}/bridge-in`} />
{/* <NavItem icon={GavelIcon} label={`Governance`} to={`/${chainName}/governance`} /> */}
<NavItem icon={HistoryIcon} chipColor="warning" chip={pendingTransactions} label="Transaction History" to={`/${chainName}/history`} />
<Box className="menu-divider">
<Divider />
</Box>
@ -194,7 +205,6 @@ const NavContent = ({ chainId, addressChainId }) => {
<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">

View File

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

View File

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

View File

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

View File

@ -1,45 +1,46 @@
import { defineChain } from 'viem'
import { http, fallback, createConfig } from 'wagmi'
import { sepolia, hoodi } from 'wagmi/chains'
import { 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/'
},
},
})
// 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],
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')
]),
// [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'),
])
// [mordor.id]: fallback([
// http('https://rpc.mordor.etccooperative.org'),
// http('https://geth-mordor.etc-network.info'),
// ])
},
})

View File

@ -4,25 +4,25 @@ export const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000";
export const STAKING_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
[NetworkId.TESTNET_HOODI]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
[NetworkId.TESTNET_HOODI]: "0x5d659FDEFc9c1552A3a53f661Cf4F52b0d7dDb40",
[NetworkId.TESTNET_MORDOR]: "0xC2C579631Bf6daA93252154080fecfd68c6aa506",
};
export const BOND_DEPOSITORY_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
[NetworkId.TESTNET_HOODI]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
[NetworkId.TESTNET_HOODI]: "0x4105D6b48f0fEfA8033b8B01d89972c2CBd787bD",
[NetworkId.TESTNET_MORDOR]: "0x46BF6F7c3e96351eab7542f2B14c9f2ac6d08dF0",
};
export const DAO_TREASURY_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x05D797f9F34844594C956da58f1785997397f02E",
[NetworkId.TESTNET_HOODI]: "0x05D797f9F34844594C956da58f1785997397f02E",
[NetworkId.TESTNET_HOODI]: "0xeA5113bF7e0af48B6D0Cf1a4B1e4dD0184708a09",
[NetworkId.TESTNET_MORDOR]: "0x05D797f9F34844594C956da58f1785997397f02E",
};
export const FTSO_DAI_LP_ADDRESSES = {
export const FTSO_RESERVE_LP_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xCd1505E5d169525e0241c177aF5929A92E02276D",
[NetworkId.TESTNET_HOODI]: "0x32388605b5E83Ea79CDdC479AA9939DBCF98f59D",
[NetworkId.TESTNET_HOODI]: "0xf3F2cc4DaB20F09eee0d07d8456924927ceA1847",
[NetworkId.TESTNET_MORDOR]: "0x53B13C4722081c405ce25c7A7629fC326A49a469",
};
@ -46,43 +46,43 @@ export const WETH_ADDRESSES = {
export const GHST_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
[NetworkId.TESTNET_HOODI]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
[NetworkId.TESTNET_HOODI]: "0x594a5CD093859B3FD48aA901d86F71a7D537834a",
[NetworkId.TESTNET_MORDOR]: "0x1eCee8BfceC44e535B3Ee92Aca70507668781392",
};
export const STNK_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
[NetworkId.TESTNET_HOODI]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
[NetworkId.TESTNET_HOODI]: "0x7a7d0F214b8C03bD962B88AC0a7F543085aB817B",
[NetworkId.TESTNET_MORDOR]: "0xa31cf59baC26Dd8A8b422b999eB1Ba541C941EA7",
};
export const FTSO_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
[NetworkId.TESTNET_HOODI]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
[NetworkId.TESTNET_HOODI]: "0x07427B220934B3421223969eA61a247bFA37cF03",
[NetworkId.TESTNET_MORDOR]: "0x7ebd1224D36d64eA09312073e60f352d1383801A",
};
export const DISTRIBUTOR_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
[NetworkId.TESTNET_HOODI]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
[NetworkId.TESTNET_HOODI]: "0xe503d65C9574bD3C18c8d725cD346eCBFAE24b4d",
[NetworkId.TESTNET_MORDOR]: "0xfa524772eec78FAeD0db2cF8A831FDDa9F5B0544",
};
export const GHOST_GOVERNANCE_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xaf7Ad1b83C47405BB9aa96868bCFbb6D65e4C2a1",
[NetworkId.TESTNET_HOODI]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
[NetworkId.TESTNET_HOODI]: "0x0000000000000000000000000000000000000000",
[NetworkId.TESTNET_MORDOR]: "0xF950101af53733Ccf9309Ef4CC374B300dd43010",
};
export const BONDING_CALCULATOR_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
[NetworkId.TESTNET_HOODI]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
[NetworkId.TESTNET_HOODI]: "0xf13A5319aD8071e4dcE7776728Ef9Cc221A5c098",
[NetworkId.TESTNET_MORDOR]: "0xfA821181de76D3EAdb404dDe971A6d28289F22b3",
}
export const GATEKEEPER_ADDRESSES = {
[NetworkId.TESTNET_SEPOLIA]: "0xd735cA07984a16911222c08411A80e24EB38869B",
[NetworkId.TESTNET_HOODI]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
[NetworkId.TESTNET_HOODI]: "0xc9D5f50e17eeEAF39E0D0cDbbcF4B04FA4A4Bbd6",
[NetworkId.TESTNET_MORDOR]: "0x4823F1DC785D721eAdD2bD218E1eeD63aF67fBF4",
}

View File

@ -1,13 +1,15 @@
import { ArrowBack } from "@mui/icons-material";
import { Box, Link, Skeleton, Typography } from "@mui/material";
import { Box, Container, Skeleton, Typography, useMediaQuery } from "@mui/material";
import { useEffect, useState, useMemo, useCallback } from "react";
import { Link as RouterLink, useLocation, useParams, useNavigate } from "react-router-dom";
import { useLocation, useParams, useNavigate } from "react-router-dom";
import { useAccount, useSwitchChain } from "wagmi";
import { 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";
@ -40,6 +42,9 @@ 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();
@ -102,17 +107,9 @@ 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}
@ -120,7 +117,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}
{preparedQuoteToken.name} Bond
</Typography>
</Box>
</Box>
@ -139,30 +136,45 @@ export const BondModal = ({ bond, chainId, address, config, connect }) => {
onDecimalsChange={event => setFormatDecimalsInner(event.currentTarget.value)}
/>
<Box display="flex" flexDirection="row" justifyContent="space-between" width={["100%", "70%"]} mt="24px">
<Metric
label={`Bond Price`}
metric={
bond.isSoldOut ? (
"--"
) : (
<BondPrice
price={bond.price.inUsd}
/>
)
<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&nbsp;
<Countdown otherwise="[EXPIRED]" targetTime={bond.duration * 1000} />
</Typography>
</Box>
}
/>
<Metric
label="Market Price"
metric={<TokenPrice priceInUsd={bond.price.marketPriceInUsd} />}
/>
<Metric
label="Discount"
metric={<BondDiscount discount={bond.discount} textOnly />}
/>
</Box>
>
<MetricCollection>
<Metric
label={`Bond Price`}
tooltip="Some description about what the bond price is"
metric={bond.isSoldOut ? ( "--" ) : ( <BondPrice price={bond.price.inUsd} /> )}
/>
<Metric
label="Market Price"
metric={<TokenPrice priceInUsd={bond.price.marketPriceInUsd} />}
/>
<Metric
label="Discount"
tooltip="Some description about what the discount is"
metric={<BondDiscount discount={bond.discount} textOnly />}
/>
</MetricCollection>
</Paper>
</Container>
<Box width="100%" mt="24px">
<Box width="100%" display="flex" justifyContent="center" alignItems="center">
<BondInputArea
chainId={chainId}
bond={bond}

View File

@ -17,6 +17,7 @@ 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";
@ -28,7 +29,32 @@ const StyledBox = styled(Box, {
};
});
const BondConfirmModal = ({
export const DiscountConfirmModal = ({ isOpen, isPending, onSubmit, handleConfirmClose, discount }) => {
const theme = useTheme();
return (
<Modal
maxWidth="380px"
minHeight="200px"
open={isOpen}
headerContent={<Typography variant="h4">Confirm Bond Purchase</Typography>}
onClose={!isPending && handleConfirmClose}
>
<Box display="flex" flexDirection="column" justifyContent="space-between" alignItems="center" gap="10px">
<Typography>I confirm the bond purchase with a negative discount</Typography>
<Box display="flex" flexDirection="row" alignItems="center" justifyContent="center">
<Typography color={theme.colors.feedback.error} variant="h6">{discount}%</Typography>
</Box>
<PrimaryButton fullWidth onClick={onSubmit} disabled={isPending} loading={isPending}>
Confirm
</PrimaryButton>
</Box>
</Modal>
)
}
export const BondConfirmModal = ({
chainId,
bond,
slippage,
@ -51,6 +77,8 @@ const BondConfirmModal = ({
const [acknowledgedWarmup, setAcknowledgedWarmup] = useState(false);
const [isPending, setIsPending] = useState(false);
const { tryStoreBondInHistory } = useLocalStorage();
useEffect(() => setAcknowledgedWarmup(acknowledgedWarmup || !needsWarmup), [acknowledgedWarmup, needsWarmup]);
const AcknowledgeWarmupCheckbox = () => {
@ -104,21 +132,19 @@ 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 referral = import.meta.env.VITE_APP_REFERRAL_ADDRESS;
await purchaseBond({
const txHash = await purchaseBond({
chainId,
bondId: bond.id,
amount: spendAmountValue._value.toBigInt(),
maxPrice,
user: recipientAddress,
sender,
referral,
isNative
});
tryStoreBondInHistory(chainId, sender, txHash);
setIsPending(false);
handleConfirmCloseMaster();
}
@ -212,5 +238,3 @@ const BondConfirmModal = ({
</Modal>
);
};
export default BondConfirmModal;

View File

@ -1,7 +1,8 @@
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
import { Box, Checkbox, FormControlLabel, useMediaQuery } from "@mui/material";
import { Box, Container, Checkbox, FormControlLabel, Typography, useMediaQuery } from "@mui/material";
import { useState, useMemo, useCallback } from "react";
import { 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";
@ -9,10 +10,11 @@ import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber";
import BondDiscount from "./BondDiscount";
import BondVesting from "./BondVesting";
import BondConfirmModal from "./BondConfirmModal";
import { BondConfirmModal, DiscountConfirmModal } 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";
@ -33,6 +35,7 @@ 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();
@ -45,7 +48,9 @@ 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);
@ -60,12 +65,24 @@ 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;
@ -95,143 +112,32 @@ const BondInputArea = ({
}, [amountInBaseToken, baseTokenString, balance, parsedAmount]);
return (
<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="476px"
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
id="from"
token={
<TokenStack
chainTokenNames={bond.quoteToken.chainNames}
tokens={bond.quoteToken.icons}
sx={{ fontSize: "25px", height: "30px" }}
/>
}
tokenName={preparedQuoteToken.name}
info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)}
endString="Max"
endStringOnClick={setMax}
value={amount}
onChange={event => setAmount(event.currentTarget.value)}
inputProps={{ "data-testid": "fromInput" }}
/>}
LowerSwapCard={
<SwapCard
maxWidth="476px"
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
id="to"
token={
<TokenStack
chainTokenNames={bond.baseToken.chainNames}
tokens={bond.baseToken.icons}
sx={{ fontSize: "25px", height: "30px" }}
/>
}
tokenName={bond.baseToken.name}
value={amountInBaseToken.toString({ decimals: 9 })}
inputProps={{ "data-testid": "toInput" }}
/>
}
/>
</Box>
<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"
}
/>
<TokenAllowanceGuard
spendAmount={parsedAmount}
tokenName={bond.quoteToken.quoteTokenAddress}
owner={address}
spender={BOND_DEPOSITORY_ADDRESSES[chainId]}
decimals={parsedAmount._decimals}
approvalText={`Approve ${bond.quoteToken.name} to Bond`}
approvalPendingText={"Approving..."}
connect={connect}
width="100%"
height="60px"
isNative={preparedQuoteToken.address === undefined}
isVertical
message={
<>
First time bonding <b>{bond.quoteToken.name}</b>? <br /> Please approve ghostDAO to use your{" "}
<b>{bond.quoteToken.name}</b> for bonding.
</>
}
>
{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={() => 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}
@ -249,8 +155,139 @@ const BondInputArea = ({
sender={address}
handleConfirmClose={() => handleConfirmClose()}
/>
</Box>
);
};
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="center" gap="15px">
<SwapCollection
iconNotNeeded
UpperSwapCard={
<SwapCard
maxWidth="390px"
inputWidth={isVerySmallScreen ? "100px" : isSemiSmallScreen ? "180px" : "250px"}
id="from"
token={
<TokenStack
chainTokenNames={bond.quoteToken.chainNames}
tokens={bond.quoteToken.icons}
sx={{ fontSize: "25px", height: "30px" }}
/>
}
tokenName={preparedQuoteToken.name}
info={formatCurrency(balance, formatDecimals, preparedQuoteToken.name)}
endString="Max"
endStringOnClick={setMax}
value={amount}
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={bond.baseToken.chainNames}
tokens={bond.baseToken.icons}
sx={{ fontSize: "25px", height: "30px" }}
/>
}
tokenName={bond.baseToken.name}
value={amountInBaseToken.toString({ decimals: 9 })}
inputProps={{ "data-testid": "toInput" }}
/>
}
/>
<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
spendAmount={parsedAmount}
tokenName={bond.quoteToken.quoteTokenAddress}
owner={address}
spender={BOND_DEPOSITORY_ADDRESSES[chainId]}
decimals={parsedAmount._decimals}
approvalText={`Approve ${bond.quoteToken.name} to Bond`}
approvalPendingText={"Approving..."}
connect={connect}
width="100%"
height="60px"
isNative={preparedQuoteToken.address === undefined}
isVertical
message={
<>
First time bonding <b>{bond.quoteToken.name}</b>? <br /> Please approve ghostDAO to use your{" "}
<b>{bond.quoteToken.name}</b> for bonding.
</>
}
>
<PrimaryButton
fullWidth
disabled={incorrectInputAmount || bond.isSoldOut || (!showDisclaimer && !checked)}
onClick={() => setModalOpen(true)}
>
Bond
</PrimaryButton>
</TokenAllowanceGuard>
</Box>
</Paper>
</Box>
)
};
export default BondInputArea;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import {
addLiquidity,
addLiquidityETH,
} from "../../hooks/uniswapv2";
import { useLocalStorage } from "../../hooks/localstorage";
const PoolContainer = ({
chainSymbol,
@ -37,6 +38,7 @@ const PoolContainer = ({
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 456px)");
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
const [isPending, setIsPending] = useState(false);
const [amountTop, setAmountTop] = useState("");
@ -165,7 +167,12 @@ const PoolContainer = ({
const addLiquidityInner = async () => {
setIsPending(true);
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
const numericSecondsToWait = parseInt(secondsToWait, 10);
const safeSecondsToWait = (!isNaN(numericSecondsToWait) && numericSecondsToWait > 0)
? numericSecondsToWait
: 0;
const deadline = Math.floor(Date.now() / 1000) + safeSecondsToWait;
const shares = 100000;
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
@ -191,12 +198,16 @@ const PoolContainer = ({
deadline,
}
let txHash;
if (topIsNative || bottomIsNative) {
await addLiquidityETH(params)
txHash = await addLiquidityETH(params)
} else {
await addLiquidity(params);
txHash = await addLiquidity(params);
}
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.dex.key, txHash, CONSTANTS.dex.add);
await balanceRefetchTop();
await balanceRefetchBottom();
await pairAddressRefetch();
@ -215,7 +226,7 @@ const PoolContainer = ({
}, [pairAddress])
return (
<Box maxWidth="356px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
<SwapCollection
UpperSwapCard={
<SwapCard
@ -256,7 +267,7 @@ const PoolContainer = ({
justifyContent="space-between"
alignItems="center"
gap="0px"
maxWidth="356px"
maxWidth="390px"
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
>
<Box width="100%" display="flex" justifyContent="space-between">

View File

@ -21,6 +21,7 @@ import {
swapExactETHForTokens,
swapExactTokensForETH,
} from "../../hooks/uniswapv2";
import { useLocalStorage } from "../../hooks/localstorage";
import { EMPTY_ADDRESS } from "../../constants/addresses";
@ -45,6 +46,7 @@ const SwapContainer = ({
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 456px)");
const { tryStoreInHistory, CONSTANTS } = useLocalStorage();
const [isPending, setIsPending] = useState(false);
const [amountBottom, setAmountBottom] = useState("");
@ -161,7 +163,12 @@ const SwapContainer = ({
const swapTokens = async () => {
setIsPending(true);
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
const numericSecondsToWait = parseInt(secondsToWait, 10);
const safeSecondsToWait = (!isNaN(numericSecondsToWait) && numericSecondsToWait > 0)
? numericSecondsToWait
: 0;
const deadline = Math.floor(Date.now() / 1000) + safeSecondsToWait;
const shares = 100000;
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
@ -173,10 +180,12 @@ const SwapContainer = ({
const amountBDesired = BigInt(Math.round(parseFloat(amountBottom) * Math.pow(10, balanceBottom._decimals)));
const amountBMin = amountBDesired * bigIntSlippage / one;
let txHash;
if (isWrapping) {
await depositNative(chainId, address, amountADesired);
txHash = await depositNative(chainId, address, amountADesired);
} else if (isUnwrapping) {
await withdrawWeth(chainId, address, amountADesired);
txHash = await withdrawWeth(chainId, address, amountADesired);
} else {
const params = {
chainId,
@ -190,14 +199,16 @@ const SwapContainer = ({
};
if (topIsNative) {
await swapExactETHForTokens(params)
txHash = await swapExactETHForTokens(params)
} else if (bottomIsNative) {
await swapExactTokensForETH(params)
txHash = await swapExactTokensForETH(params)
} else {
await swapExactTokensForTokens(params);
txHash = await swapExactTokensForTokens(params);
}
}
if (txHash) tryStoreInHistory(chainId, address, CONSTANTS.dex.key, txHash, CONSTANTS.dex.swap);
await balanceRefetchTop();
await balanceRefetchBottom();
await pairReservesRefetch();
@ -207,11 +218,11 @@ const SwapContainer = ({
}
return (
<Box maxWidth="356px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
<Box maxWidth="390px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
<SwapCollection
UpperSwapCard={
<SwapCard
maxWidth="356px"
maxWidth="390px"
id="from"
token={tokenNameTop}
chainTokenName={tokenNameTop !== chainSymbol && chainSymbol}
@ -230,7 +241,7 @@ const SwapContainer = ({
}
LowerSwapCard={
<SwapCard
maxWidth="356px"
maxWidth="390px"
id="to"
token={tokenNameBottom}
chainTokenName={tokenNameBottom !== chainSymbol && chainSymbol}
@ -252,7 +263,7 @@ const SwapContainer = ({
justifyContent="space-between"
alignItems="center"
gap="0px"
maxWidth="356px"
maxWidth="390px"
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
>
<Box width="100%" display="flex" justifyContent="space-between">

View File

@ -12,7 +12,7 @@ import { ArgumentsWrapper, BooleanTrigger, ArgumentInput, ParsedCell } from "./i
import {
RESERVE_ADDRESSES,
FTSO_DAI_LP_ADDRESSES,
FTSO_RESERVE_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_DAI_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_DAI_LP_ADDRESSES[chainId])}` },
{ value: FTSO_RESERVE_LP_ADDRESSES[chainId], symbol: `${ftsoSymbol}-${reserveSymbol} LP`, label: `${ftsoSymbol}-${reserveSymbol} LP: ${shorten(FTSO_RESERVE_LP_ADDRESSES[chainId])}` },
{ value: RESERVE_ADDRESSES[chainId], symbol: reserveSymbol, label: `${reserveSymbol}: ${shorten(RESERVE_ADDRESSES[chainId])}` },
];

View File

@ -0,0 +1,660 @@
import useSWR from 'swr';
import { useMemo, useState } from "react";
import { useTransactionReceipt, useBlockNumber, useTransaction } from 'wagmi';
import { decodeEventLog, decodeFunctionData } from 'viem';
import { Box, Typography, Link, Skeleton, useTheme } from "@mui/material";
import { ss58Encode } from "@polkadot-labs/hdkd-helpers";
import { hexToBytes, bytesToHex } from "viem";
import { blake2AsU8a } from "@polkadot/util-crypto";
import CheckIcon from '@mui/icons-material/Check';
import SyncIcon from '@mui/icons-material/Sync';
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import HourglassBottomIcon from '@mui/icons-material/HourglassBottom';
import PendingIcon from '@mui/icons-material/Pending';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import Modal from "../../components/Modal/Modal";
import DataRow from "../../components/DataRow/DataRow";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { PrimaryButton } from "../../components/Button";
import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
import { executePapiBareTransaction } from "../../hooks/helpers";
import { useWeaverRoot, useWeaverProof, useWeaverSlotValues, useWeaverMaximumSlots } from "../../hooks/staking"
import { useUnstableProvider } from "../../hooks/ghost";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { shorten, formatNumber, formatCurrency } from "../../helpers";
const BridgeInModal = ({ chainId, isOpen, close, config, historyRecord }) => {
if (historyRecord === null) return null;
const [isPending, setIsPending] = useState(false);
const [copiedIndex, setCopiedIndex] = useState(null);
const { casperApi, client, account } = useUnstableProvider();
const { data: blockNumber } = useBlockNumber({ watch: true });
const { data: txData } = useTransaction({
chainId,
hash: historyRecord.txHash,
});
const copyToClipboard = (text, index) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null) , 800);
});
};
const { data: chainProperties } = useSWR(
client ? ['chainProperties', client] : null,
() => client.getChainSpecData(),
{
revalidateOnFocus: true
}
);
const functionArgs = useMemo(() => {
if (!txData || !chainProperties) return null;
const decoded = decodeFunctionData({ abi: GatekeeperAbi, data: txData.input });
const prefix = chainProperties.properties.ss58Format;
const receiverRaw = decoded?.args.at(0);
const receiver = receiverRaw ? ss58Encode(receiverRaw, prefix) : null;
return { receiver, amount: decoded?.args.at(1) };
}, [txData, chainProperties]);
const { data: networkData } = useSWR(
casperApi ? ['networkData', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostNetworks.Networks.getValue(key);
return res;
},
{
revalidateOnFocus: true
}
);
const { data: loomState } = useSWR(
casperApi ? ['loomState', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.LoomStates.getValue(key, 0n);
return res;
},
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
const chainInfo = useMemo(() => {
const client = config?.getClient();
return {
url: client?.chain?.blockExplorers?.default?.url,
symbol: client?.chain?.nativeCurrency?.symbol,
};
}, [config]);
const { data: receipt, isLoading, error } = useTransactionReceipt({
hash: historyRecord?.txHash,
query: {
enabled: !!historyRecord?.txHash && isOpen,
}
});
const allDecodedLogs = useMemo(() => {
if (!receipt?.logs) return [];
return receipt.logs.map((log) => {
try {
const decoded = decodeEventLog({
abi: GatekeeperAbi,
data: log.data,
topics: log.topics,
});
return {
eventName: decoded.eventName,
args: decoded.args,
address: log.address
};
} catch (e) {
return null;
}
}).filter(Boolean);
}, [receipt]);
const bridgeInInfo = useMemo(() => {
return allDecodedLogs.find(l => l.eventName === 'ThreadInserted');
}, [allDecodedLogs]);
const { maximumSlots } = useWeaverMaximumSlots(chainId);
const { merkleRoot: evmMerkleRoot } = useWeaverRoot(chainId, loomState?.at_block, bridgeInInfo?.args);
const { merkleProof } = useWeaverProof(chainId, loomState?.at_block, bridgeInInfo?.args);
const { slotValues } = useWeaverSlotValues(chainId, loomState?.at_block, bridgeInInfo?.args);
const localString = new Date(historyRecord.submitted).toLocaleString();
const bridgedAmount = useMemo(() => {
if (!functionArgs) return null;
return new DecimalBigNumber(
functionArgs.amount,
chainProperties?.properties.tokenDecimals ?? 18
)
}, [functionArgs, chainProperties]);
const estimatedFee = useMemo(() => {
if (!networkData) return null;
return networkData.incoming_share / 10000000;
}, [networkData]);
const estimatedReceived = useMemo(() => {
if (!functionArgs) return null;
const hundredPercent = 1000000000;
const incomingShare = networkData ? networkData.incoming_share : 0;
const result = hundredPercent - incomingShare;
const value = Number(functionArgs.amount) * result / hundredPercent;
return new DecimalBigNumber(value, chainProperties?.properties.tokenDecimals ?? 18)
}, [functionArgs, networkData, chainProperties]);
const finalization = useMemo(() => {
if (!receipt || !blockNumber || !networkData) return null;
const finalityDelay = networkData.finality_delay;
return Math.max(0, Number(finalityDelay + receipt.blockNumber - blockNumber));
}, [receipt, blockNumber, networkData]);
const rootIncluded = useMemo(() => {
if (!loomState || !receipt) return false;
return evmMerkleRoot === loomState.root_hash && receipt.blockNumber < loomState.at_block;
}, [loomState, receipt, evmMerkleRoot]);
const weaverArgs = useMemo(() => {
if (!bridgeInInfo) return null;
return {
sessionIndex: bridgeInInfo.args.session,
slotIndex: bridgeInInfo.args.index % maximumSlots,
valueIndex: bridgeInInfo.args.index / maximumSlots,
};
}, [maximumSlots, bridgeInInfo]);
const pulledThreadKey = useMemo(() => {
if (!chainId || !weaverArgs) return "0x0000000000000000000000000000000000000000000000000000000000000000";
const padU64 = (val) => "0x" + BigInt(val).toString(16).padStart(64, "0");
const metadata = [
padU64(weaverArgs.sessionIndex),
padU64(weaverArgs.valueIndex),
padU64(weaverArgs.slotIndex),
padU64(chainId)
].map(hexToBytes);
let currentHash = metadata[0];
for (let i = 1; i < metadata.length; i++) {
const combined = new Uint8Array(64);
combined.set(currentHash, 0);
combined.set(metadata[i], 32);
currentHash = blake2AsU8a(combined);
}
return bytesToHex(currentHash);
}, [chainId, weaverArgs]);
const { data: threadPulled, mutate: threadPulledMutate } = useSWR(
casperApi ? ['threadPulled', casperApi, pulledThreadKey, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.PulledThreads.getValue(pulledThreadKey);
return res;
},
{
revalidateOnFocus: true
}
);
const pullCurrentThread = async () => {
setIsPending(true);
try {
const txArgs = {
network_id: BigInt(chainId),
session: weaverArgs.sessionIndex,
thread_proof: {
type: "EvmThreadProof",
value: {
slot_index: weaverArgs.valueIndex,
proof_index: weaverArgs.slotIndex,
receiver: functionArgs.receiver,
amount: functionArgs.amount,
proof: merkleProof,
slots: slotValues,
}
}
};
const tx = casperApi.tx.GhostWeaver.pull_thread(txArgs);
const bareTx = await tx.getBareTx();
await executePapiBareTransaction({ client, bareTx });
await threadPulledMutate();
} finally {
setIsPending(false);
}
}
return (
<Modal
maxWidth="420px"
minHeight="200px"
open={isOpen}
headerContent={
<Box display="flex" flexDirection="row">
{bridgeInInfo
? <Box display="flex" gap="10px">
<Typography variant="h5">Bridge-In</Typography>
<Link
underline="always"
target="_blank"
rel="noopener noreferrer"
href={`${chainInfo.url}/tx/${historyRecord.txHash}`}
>
<Typography variant="h5">
{bridgeInInfo ? shorten(historyRecord.txHash, 7, -5) : ""}
</Typography>
</Link>
</Box>
: <Typography variant="h5">Parsing Transaction Hash</Typography>
}
</Box>
}
onClose={() => isPending ? {} : close()}
>
<Box display="flex" flexDirection="column">
<Box width="100%" gap="5px" display="flex" flexDirection="column" justifyContent="space-between" alignItems="center">
<BridgeInProgress
submissionBlock={receipt ? receipt.blockNumber : 0}
blockNumber={blockNumber}
chainId={chainId}
casperApi={casperApi}
finalization={finalization}
loomState={loomState}
rootIncluded={rootIncluded}
claimed={threadPulled === null}
/>
<Box
mt={2}
width="100%"
display="flex"
flexDirection="column"
sx={{
'& .data-row': {
margin: '0px !important',
},
'& .data-row .MuiTypography-root': {
fontSize: '13px !important',
}
}}
>
<DataRow
title="Submitted"
balance={<Typography variant="body1">{localString}</Typography>}
tooltip="some description to be added."
/>
<DataRow
title="Receiver"
tooltip="some description to be added."
balance={
<Link
style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center" }}
onClick={() => functionArgs?.receiver ? copyToClipboard(functionArgs.receiver, 0) : {}}
>
<Typography variant="body1">
{functionArgs?.receiver ? shorten(functionArgs.receiver, 11, -8) : <Skeleton width="120px" />}
</Typography>
<GhostStyledIcon
sx={{ marginLeft: "5px", width: "12px", height: "12px" }}
viewBox="0 0 25 25"
component={copiedIndex === 0 ? CheckIcon : ContentPasteIcon}
/>
</Link>
}
/>
<DataRow
title="Block Height"
tooltip="some description to be added."
balance={receipt
? <Typography variant="body1">{formatNumber(receipt.blockNumber)}</Typography>
: <Skeleton width="120px" />
}
/>
</Box>
<hr style={{ width: "100%" }} />
<Box
mb="10px"
width="100%"
display="flex"
flexDirection="column"
sx={{
'& .data-row': {
margin: '0px !important',
},
'& .data-row .MuiTypography-root': {
fontSize: '13px !important',
}
}}
>
<DataRow
title="Gross Bridged"
tooltip="some description to be added."
balance={bridgedAmount
? <Typography variant="body1">
{formatCurrency(bridgedAmount.toString(), 5, chainProperties?.properties.tokenSymbol ?? "")}
</Typography>
: <Skeleton width="120px" />
}
/>
<DataRow
title="GHOST Fee (Est.)"
tooltip="some description to be added."
balance={<Typography variant="body1">{formatNumber(estimatedFee, 2)}%</Typography>}
/>
<DataRow
title="Net Received"
tooltip="some description to be added."
balance={estimatedReceived
? <Typography variant="body1">
{formatCurrency(estimatedReceived.toString(), 5, chainProperties?.properties.tokenSymbol ?? "")}
</Typography>
: <Skeleton width="120px" />
}
/>
</Box>
<PrimaryButton
fullWidth
disabled={!rootIncluded || isPending || threadPulled === null}
onClick={pullCurrentThread}
>
{isPending
? "Pending..."
: threadPulled === null ? "Transaction Claimed" : "Claim Bridge-In"
}
</PrimaryButton>
</Box>
</Box>
</Modal>
)
}
const BridgeInProgress = ({ chainId, submissionBlock, claimed, casperApi, finalization, loomState, rootIncluded }) => {
const theme = useTheme();
const { data: weaverAuthorities } = useSWR(
casperApi ? ['weaverAuthorities', casperApi] : null,
() => casperApi.query.GhostWeaver.Authorities.getValue(),
{
revalidateOnFocus: true
}
);
const { data: networkAttestations } = useSWR(
casperApi ? ['networkAttestations', casperApi] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.NetworkAttestations.getValue(key);
return res;
},
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
const { data: tapestryDraft } = useSWR(
casperApi ? ['tapestryDraft', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.TapestryDrafts.getValue(key);
return res;
},
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
const { data: tapestryStrand } = useSWR(
casperApi ? ['tapestryStrand', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostWeaver.TapestryStrands.getValue(key);
return res;
},
{
refreshInterval: 12000,
revalidateOnFocus: true
}
);
return (
<Box p="10px 0" width="100%" display="flex" justifyContent="space-between" alignItems="center">
{finalization === null || finalization > 0
? <BlockFinalization theme={theme} finalization={finalization} />
: <SyncTarget
theme={theme}
weaverAuthorities={weaverAuthorities}
networkAttestations={networkAttestations}
rootIncluded={rootIncluded}
submissionBlock={submissionBlock}
loomState={loomState}
tapestryDraft={tapestryDraft}
/>
}
<RootVerification
theme={theme}
finalization={finalization}
rootIncluded={rootIncluded}
tapestryDraft={tapestryDraft}
tapestryStrand={tapestryStrand}
totalAuthorities={weaverAuthorities?.length ?? 0}
/>
<ProofSubmission claimed={claimed} theme={theme} rootIncluded={rootIncluded} />
</Box>
)
}
const ProofSubmission = ({ theme, claimed, rootIncluded }) => {
return (
<Box
sx={{
transition: "all 0.8s ease",
transform: (rootIncluded && !claimed) && "scale(1.2)",
color: claimed && theme.colors.primary[300]
}}
maxWidth="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
transformOrigin: "center",
animation: (rootIncluded && !claimed) && 'waitingForClaim 4s ease-in-out infinite',
'@keyframes waitingForClaim': {
'0%, 100%': { opacity: 0.4 },
'50%': { opacity: 1 },
},
}}
viewBox="0 0 25 25"
component={claimed ? CheckCircleIcon : PendingIcon}
/>
<Typography variant="caption">Bridge Status</Typography>
<Typography variant="caption">
{rootIncluded
? claimed ? "Success" : "Claim Ready"
: "Claim Pending..."
}
</Typography>
</Box>
)
}
const RootVerification = ({ theme, finalization, tapestryDraft, rootIncluded, totalAuthorities, tapestryStrand }) => {
const finalizationPending = useMemo(() => finalization > 0, [finalization]);
const rootVerificationText = useMemo(() => {
if (rootIncluded) return "Synced";
if (!tapestryDraft || finalizationPending) return "Pending...";
let maxOnesCount = 0;
tapestryStrand?.forEach(item => {
const dataArray = item[1]?.data || [];
const totalOnes = dataArray.reduce((sum, num) => {
const onesInNumber = num.toString(2).split('1').length - 1;
return sum + onesInNumber;
}, 0);
if (totalOnes > maxOnesCount) {
maxOnesCount = totalOnes;
}
});
return `${maxOnesCount} / ${totalAuthorities}`;
}, [rootIncluded, tapestryDraft, tapestryStrand, totalAuthorities, finalizationPending]);
return (
<Box
sx={{
transition: "all 0.8s ease",
transform: !rootIncluded && !finalizationPending && tapestryDraft && "scale(1.2)",
color: (rootIncluded || tapestryDraft) && !finalizationPending && theme.colors.primary[300]
}}
maxWidth="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
transformOrigin: "center",
animation: !rootIncluded && tapestryDraft && 'scanFingerPrint 4s ease-in-out infinite',
'@keyframes scanFingerPrint': {
'0%, 100%': { opacity: 0.4 },
'50%': { opacity: 1 },
},
}}
viewBox="0 0 25 25"
component={FingerprintIcon}
/>
<Typography variant="caption">Root Verification</Typography>
<Typography variant="caption">{rootVerificationText}</Typography>
</Box>
)
}
const SyncTarget = ({ weaverAuthorities, networkAttestations, tapestryDraft, submissionBlock, loomState, rootIncluded, theme }) => {
const syncTargetText = useMemo(() => {
if (rootIncluded) return "Synced";
if (!tapestryDraft || !loomState) {
const total = weaverAuthorities ? weaverAuthorities.length : 0;
const count = networkAttestations
? networkAttestations.filter(v => v !== undefined).length
: 0;
return `${count} / ${total}`
}
if (submissionBlock < loomState.at_block) return "Synced";
return `${formatNumber(Number(submissionBlock - loomState.at_block))} block behind`;
}, [rootIncluded, tapestryDraft, loomState, weaverAuthorities, submissionBlock, networkAttestations]);
return (
<Box
sx={{
transition: "all 0.8s ease",
transform: !rootIncluded && !tapestryDraft && "scale(1.2)",
color: (rootIncluded || !tapestryDraft) && theme.colors.primary[300]
}}
maxWidth="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
transformOrigin: "center",
animation: !rootIncluded && !tapestryDraft && 'rotateSyncTarget 2s ease-in-out infinite',
'@keyframes rotateSyncTarget': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(-360deg)' },
},
}}
viewBox="0 0 25 25"
component={SyncIcon}
/>
<Typography variant="caption">Sync Target</Typography>
<Typography variant="caption">{syncTargetText}</Typography>
</Box>
)
}
const BlockFinalization = ({ finalization, theme }) => {
return (
<Box
sx={{
transition: "all 0.8s ease",
transform: finalization && finalization > 0 && "scale(1.2)",
color: finalization === 0 && theme.colors.primary[300]
}}
maxWidth="120px"
display="flex"
flexDirection="column"
justifyContent="start"
alignItems="center"
>
<GhostStyledIcon
sx={{
width: "35px",
height: "35px",
animation: finalization && finalization > 0 && 'rotateHourGlass 2s ease-in-out infinite',
'@keyframes rotateHourGlass': {
'0%': { transform: 'rotate(0deg)' },
'15%': { transform: 'rotate(0deg)' },
'85%': { transform: 'rotate(180deg)' },
'100%': { transform: 'rotate(180deg)' },
},
}}
viewBox="0 0 25 25"
component={HourglassBottomIcon}
/>
<Typography variant="caption">Finalization</Typography>
<Typography variant="caption">
{(finalization ?? 0).toString()} blocks left
</Typography>
</Box>
)
}
export default BridgeInModal;

View File

@ -0,0 +1,192 @@
import { useTransactionReceipt, useToken } from 'wagmi';
import { decodeEventLog } from 'viem';
import { useMemo } from "react";
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import { Box, Typography, Link } from "@mui/material";
import Token from "../../components/Token/Token";
import Modal from "../../components/Modal/Modal";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { PrimaryButton } from "../../components/Button";
import { abi as UniswapV2PairAbi } from "../../abi/UniswapV2Pair.json";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { formatNumber, shorten } from "../../helpers";
const TokenBox = ({ tokenAmount, tokenSymbol, decimals, chainTokenSymbol }) => {
return (
<Box display="flex" flexDirection="column" width="70px">
<Token
parentSx={{
height: "80px",
"& svg:nth-of-type(2)": {
transform: "translate(10%, -50%)",
zIndex: 100,
}
}}
sx={{ transform: "scale(1.65)", }}
chainTokenName={chainTokenSymbol}
name={tokenSymbol}
/>
<Box display="flex" flexDirection="column" alignItems="center">
<Typography>{formatNumber(
new DecimalBigNumber(tokenAmount ? tokenAmount : 0n, decimals),
5
)}</Typography>
</Box>
</Box>
)
}
const DexViewModal = ({ chainId, isOpen, close, config, historyRecord }) => {
if (historyRecord === null) return null;
const { data: receipt, isLoading, error } = useTransactionReceipt({
hash: historyRecord?.txHash,
query: {
enabled: !!historyRecord?.txHash && isOpen,
}
});
const allDecodedLogs = useMemo(() => {
if (!receipt?.logs) return [];
return receipt.logs.map((log) => {
try {
const decoded = decodeEventLog({
abi: UniswapV2PairAbi,
data: log.data,
topics: log.topics,
});
return {
eventName: decoded.eventName,
args: decoded.args,
address: log.address
};
} catch (e) {
return null;
}
}).filter(Boolean);
}, [receipt]);
const swapInfo = useMemo(() => {
const swapEvent = allDecodedLogs.find(l => l.eventName === 'Swap');
if (!swapEvent) return null;
const isZeroForOne = swapEvent.args.amount0In > 0n;
const transferLogs = allDecodedLogs.filter(l => l.eventName === 'Transfer');
const tokenInAddress = isZeroForOne
? transferLogs.find(t => t.args.value === swapEvent.args.amount0In)?.address
: transferLogs.find(t => t.args.value === swapEvent.args.amount1In)?.address;
const tokenOutAddress = isZeroForOne
? transferLogs.find(t => t.args.value === swapEvent.args.amount1Out)?.address
: transferLogs.find(t => t.args.value === swapEvent.args.amount0Out)?.address;
return {
from: swapEvent.args.sender,
to: swapEvent.args.to,
tokenInAddress,
tokenOutAddress,
amountIn: isZeroForOne ? swapEvent.args.amount0In : swapEvent.args.amount1In,
amountOut: isZeroForOne ? swapEvent.args.amount1Out : swapEvent.args.amount0Out,
};
}, [allDecodedLogs]);
const { data: tokenIn } = useToken({
address: swapInfo?.tokenInAddress,
scopeKey: `token-${swapInfo ? swapInfo.tokenInAddress : ""}-${chainId}`,
chainId
});
const { data: tokenOut } = useToken({
address: swapInfo?.tokenOutAddress,
scopeKey: `token-${swapInfo ? swapInfo.tokenOutAddress : ""}-${chainId}`,
chainId
});
const chainInfo = useMemo(() => {
const client = config?.getClient();
return {
url: client?.chain?.blockExplorers?.default?.url,
symbol: client?.chain?.nativeCurrency?.symbol,
};
}, [config]);
return (
<Modal
maxWidth="420px"
minHeight="200px"
open={isOpen}
headerText={
<>
{swapInfo && "DEX Swap"}
{!swapInfo && "Parsing Transaction Hash"}
</>
}
onClose={() => close()}
>
{swapInfo && <Box display="flex" flexDirection="column" gap="15px">
<Box width="100%" display="flex" flexDirection="column" justifyContent="space-between" alignItems="flex-start">
<Box display="flex" justifyContent="space-between" width="100%">
<Typography>{tokenIn ? tokenIn.symbol : "Unknown"} Sender</Typography>
<Link
href={`${chainInfo.url}/address/${swapInfo ? swapInfo.from : ""}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{swapInfo ? shorten(swapInfo.from, 12, -10) : ""}</Typography>
</Link>
</Box>
<Box display="flex" justifyContent="space-between" width="100%">
<Typography>{tokenOut ? tokenOut.symbol : "Unknown"} Receiver</Typography>
<Link
href={`${chainInfo.url}/address/${swapInfo ? swapInfo.to : ""}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{swapInfo ? shorten(swapInfo.to, 12, -10) : ""}</Typography>
</Link>
</Box>
</Box>
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-around" alignItems="center">
<TokenBox
tokenAmount={swapInfo ? swapInfo.amountIn : 0n}
tokenSymbol={tokenIn ? tokenIn.symbol : "Unknown"}
decimals={tokenIn ? tokenIn.decimals : 18}
chainTokenSymbol={tokenIn?.symbol.toUpperCase() === "WETH" ? undefined : chainInfo.symbol}
/>
<GhostStyledIcon sx={{ fontSize: '24px' }} component={ArrowRightIcon} />
<TokenBox
tokenAmount={swapInfo ? swapInfo.amountOut : 0n}
tokenSymbol={tokenOut ? tokenOut.symbol : "Unknown"}
decimals={tokenOut ? tokenOut.decimals : 18}
chainTokenSymbol={tokenOut?.symbol.toUpperCase() === "WETH" ? undefined : chainInfo.symbol}
/>
</Box>
<PrimaryButton
fullWidth
component="a"
href={`${chainInfo.url}/tx/${historyRecord.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
View Transaction Details
</PrimaryButton>
</Box>}
</Modal>
)
}
export default DexViewModal;

View File

@ -0,0 +1,23 @@
import { Box, Typography } from "@mui/material";
import Modal from "../../components/Modal/Modal";
import { PrimaryButton } from "../../components/Button";
const EraseModal = ({ isOpen, close, submit, isAll }) => {
return (
<Modal
maxWidth="350px"
minHeight="200px"
open={isOpen}
headerText={`Erase ${isAll ? "All Records" : "Record"}`}
onClose={() => close()}
>
<Box display="flex" flexDirection="column" justifyContent="space-between" gap="20px">
<Typography>{`Erasing ${isAll ? "all records" : "this record"} is permanent, and cannot be recovered.`}</Typography>
<PrimaryButton fullWidth onClick={submit}>Erase {isAll ? "All Records" : ""}</PrimaryButton>
</Box>
</Modal>
)
}
export default EraseModal;

View File

@ -0,0 +1,452 @@
import {
Box,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
Container,
FormControlLabel,
FormControl,
Checkbox,
Link,
useMediaQuery,
useTheme,
} from "@mui/material";
import { useLocation } from "react-router-dom";
import { useEffect, useState, useMemo, useCallback } from "react";
import useSWR from 'swr';
import ReactGA from "react-ga4";
import SettingsIcon from '@mui/icons-material/Settings';
import Modal from "../../components/Modal/Modal";
import Paper from "../../components/Paper/Paper";
import PageTitle from "../../components/PageTitle/PageTitle";
import TokenStack from "../../components/TokenStack/TokenStack";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import Chip from "../../components/Chip/Chip";
import { PrimaryButton, TertiaryButton } from "../../components/Button";
import { Tab, Tabs } from "../../components/Tabs/Tabs";
import { useTokenSymbol } from "../../hooks/tokens";
import { useLocalStorage } from "../../hooks/localstorage";
import { useUnstableProvider } from "../../hooks/ghost";
import { shorten } from "../../helpers";
import { networkAvgBlockSpeed } from "../../constants";
import EraseModal from "./EraseModal";
import SettingsModal from "./SettingsModal";
import DexViewModal from "./DexViewModal";
import StakingViewModal from "./StakingViewModal";
import BridgeInModal from "./BridgeInModal";
const HistoryItem = ({ item, chainId, config, currentTime, eraseRecord, setViewModalOpen, isSmallScreen, finalityDelay }) => {
const theme = useTheme();
const isVerySmallScreen = useMediaQuery("(max-width: 450px)");
const localString = new Date(item.submitted).toLocaleString();
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO")
const chainInfo = useMemo(() => {
const client = config?.getClient();
return {
url: client?.chain?.blockExplorers?.default?.url,
symbol: client?.chain?.nativeCurrency?.symbol,
};
}, [config]);
const chipStatus = useMemo(() => {
const delay = Number(finalityDelay * networkAvgBlockSpeed(chainId)) * 1000;
const width = isVerySmallScreen ? "21px" : "100px";
if (item.submitted + delay > currentTime) {
return <Chip sx={{ width: width }} label={isVerySmallScreen ? "" : "Pending"} template={"gray"} />
}
return <Chip sx={{ width: width }} label={isVerySmallScreen ? "" : "Ready"} template={"success"} />
}, [item, currentTime, isVerySmallScreen, finalityDelay])
if (isSmallScreen || isVerySmallScreen) {
return (
<>
<Box display="flex" flexDirection="column" gap="10px">
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
flexDirection={isVerySmallScreen ? "row" : "row"}
>
<Box display="flex" gap="20px" p="10px 0">
<TokenStack chainTokenNames={[chainInfo.symbol]} tokens={["FTSO"]} />
<Box display="flex" flexDirection="column">
<Typography>{localString}</Typography>
<Link
href={`${chainInfo.url}/tx/${item.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{shorten(item.txHash)}</Typography>
</Link>
</Box>
</Box>
<Box display="flex" flexDirection="column" alignItems="flex-end">
<Typography>{item?.name ?? "Unknown"}</Typography>
{chipStatus}
</Box>
</Box>
<Box display="flex" flexDirection="row" gap="20px">
<TertiaryButton fullWidth onClick={eraseRecord}>Erase</TertiaryButton>
<TertiaryButton fullWidth onClick={setViewModalOpen}>View</TertiaryButton>
</Box>
</Box>
</>
)
}
return (
<TableRow>
<TableCell sx={{ padding: "10px 0" }}>
<Box display="flex" alignItems="center">
<TokenStack chainTokenNames={["ETH"]} tokens={["FTSO"]} />
<Box ml="10px" display="flex" flexDirection="column" alignItems="flex-start">
<Typography>{item?.name ?? "Unknown"}</Typography>
<Link
href={`${chainInfo.url}/tx/${item.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{shorten(item.txHash, 7, -5)}</Typography>
</Link>
</Box>
</Box>
</TableCell>
<TableCell align="center" sx={{ padding: "10px 0" }}>
{chipStatus}
</TableCell>
<TableCell sx={{ padding: "10px 0" }}>
<Typography>{localString}</Typography>
</TableCell>
<TableCell sx={{ padding: "10px 0" }}>
<Box display="flex" flexDirection="row" gap="20px">
<TertiaryButton fullWidth onClick={eraseRecord}>Erase</TertiaryButton>
<TertiaryButton fullWidth onClick={setViewModalOpen}>View</TertiaryButton>
</Box>
</TableCell>
</TableRow>
)
}
const HistoryList = ({ chainId, storedTransactions, historyType, config, eraseRecord }) => {
const isSmallScreen = useMediaQuery("(max-width: 900px)");
const [currentTime, setCurrentTime] = useState(Date.now());
const [viewModalOpen, setViewModalOpen] = useState(null);
const { CONSTANTS } = useLocalStorage();
const { casperApi } = useUnstableProvider();
const { data: networkData, error, isLoading } = useSWR(
casperApi ? ['networkData', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostNetworks.Networks.getValue(key);
return res;
},
{
revalidateOnFocus: true
}
);
const finalityDelay = useMemo(() => {
return networkData ? networkData.finality_delay : 420n;
}, [networkData]);
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentTime(Date.now());
}, 1000);
return () => clearInterval(intervalId);
}, []);
const historyContainer = useMemo(() => {
if (isSmallScreen) {
return (
<Box display="flex" flexDirection="column" gap="50px">
{storedTransactions?.map((item, i) => {
return (
<HistoryItem
key={i}
item={item}
config={config}
chainId={chainId}
currentTime={currentTime}
finalityDelay={finalityDelay}
eraseRecord={() => eraseRecord(i)}
setViewModalOpen={() => setViewModalOpen(i)}
isSmallScreen={isSmallScreen}
/>
)
})}
</Box>
)
} else {
return (
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell style={{ padding: "8px 0" }}>Transaction</TableCell>
<TableCell style={{ padding: "8px 0" }} align="center">Status</TableCell>
<TableCell style={{ padding: "8px 0" }}>Submitted</TableCell>
<TableCell style={{ padding: "8px 0" }}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{storedTransactions?.map((item, i) => {
return (
<HistoryItem
key={i}
item={item}
config={config}
currentTime={currentTime}
finalityDelay={finalityDelay}
eraseRecord={() => eraseRecord(i)}
setViewModalOpen={() => setViewModalOpen(i)}
isSmallScreen={isSmallScreen}
/>
)
})}
</TableBody>
</Table>
</TableContainer>
)
}
}, [isSmallScreen, storedTransactions, config]);
return (
<>
{viewModalOpen !== null && historyType === CONSTANTS.dex.key && (
<DexViewModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{viewModalOpen !== null && historyType === CONSTANTS.stake.key && (
<StakingViewModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{viewModalOpen !== null && historyType === CONSTANTS.bridgeIn.key && (
<BridgeInModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{historyContainer}
</>
)
}
const History = ({ chainId, config, tryConnectInjected, address }) => {
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const [historyTypeIndex, setHistoryTypeIndex] = useState(0);
const [erasingIndex, setErasingIndex] = useState(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const [erasingOpen, setErasingOpen] = useState(false);
const location = useLocation();
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location]);
const {
CONSTANTS,
useStorageValue,
setStorageValue,
clearPendingHistory
} = useLocalStorage();
const { data: storedHistoryTypes } = useStorageValue(chainId, address, CONSTANTS.historyPrefix, CONSTANTS.defaultHistoryTypes);
const { data: storedTransactions } = useStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []);
const { data: pendingHistory } = useStorageValue(chainId, address, CONSTANTS.pendingHistoryPrefix, {});
const handleHistoryTypesChange = useCallback((values) => {
setStorageValue(chainId, address, CONSTANTS.historyPrefix, values);
}, [chainId, address]);
const handleConfirmErase = useCallback(() => {
if (erasingIndex !== null) {
const updated = storedTransactions.filter((_, index) => index !== erasingIndex);
setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], updated);
} else {
setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []);
}
setErasingOpen(false);
setErasingIndex(null);
}, [storedTransactions, erasingIndex, chainId, address, historyTypeIndex]);
useEffect(() => {
clearPendingHistory(chainId, address, storedHistoryTypes.at(historyTypeIndex));
}, [storedHistoryTypes, historyTypeIndex])
return (
<Box minHeight="calc(100vh - 43px)">
<PageTitle name="History" subtitle="A complete record of your activity on the ghostDAO protocol" />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
}}
>
<EraseModal
key="EraseModal"
isOpen={erasingOpen}
close={() => {
setErasingOpen(false);
setErasingIndex(null);
}}
submit={handleConfirmErase}
isAll={erasingIndex === null}
/>
<SettingsModal
key="SettingsModal"
isOpen={settingsOpen}
close={() => setSettingsOpen(false)}
onChange={handleHistoryTypesChange}
selectedValues={storedHistoryTypes}
possibleValues={CONSTANTS.possibleHistoryTypes}
/>
<Box mt="15px">
<Paper
fullWidth
enableBackground
topRight={
<Box style={{ position: "absolute", top: "28px", right: "20px", zIndex: 10 }}>
<GhostStyledIcon
component={SettingsIcon}
viewBox="0 0 23 23"
style={{ display: "flex", cursor: "pointer" }}
onClick={() => setSettingsOpen(true)}
/>
</Box>
}
headerContent={
<Box
width="100%"
display="flex"
flexDirection="column"
justifyContent="space-between"
alignItems="center"
gap="15px"
>
<Tabs
centered={!isSemiSmallScreen}
orientation={isSemiSmallScreen ? "vertical" : "horizontal"}
textColor="primary"
indicatorColor="primary"
value={storedHistoryTypes[historyTypeIndex]}
aria-label="History menu"
onChange={(_, selectedValue) => {
const index = storedHistoryTypes.indexOf(selectedValue);
if (index !== -1) setHistoryTypeIndex(index);
}}
TabIndicatorProps={{ style: { display: "none" } }}
>
{storedHistoryTypes.map((type, i) => {
return (
<Tab
key={i}
value={type}
aria-label={`history-${i}-button`}
style={{ fontSize: "1.2rem" }}
label={
<Box display="flex" alignItems="center" gap="5px">
<Typography variant="h6">{type}</Typography>
{pendingHistory && pendingHistory[type] > 0 && (
<Chip
template="warning"
strong={true}
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
height: "16px",
minWidth: "16px",
width: "auto",
fontSize: "8px",
borderRadius: "100%"
}}
sx={{
"& .MuiChip-label": {
padding: 0,
lineHeight: "16px",
display: "block"
}
}}
label={pendingHistory[type]}
/>
)}
</Box>
}
/>
)
})}
</Tabs>
<PrimaryButton
fullWidth
onClick={() => setErasingOpen(true)}
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
>
Erase All Records
</PrimaryButton>
</Box>
}
>
<HistoryList
chainId={chainId}
config={config}
eraseRecord={(index) => {
setErasingIndex(index);
setErasingOpen(true);
}}
historyType={storedHistoryTypes[historyTypeIndex]}
storedTransactions={storedTransactions}
/>
</Paper>
</Box>
</Container>
</Box>
);
}
export default History;

View File

@ -0,0 +1,49 @@
import { Box, FormControlLabel, Checkbox, Typography } from "@mui/material";
import Modal from "../../components/Modal/Modal";
import { CheckBoxOutlineBlank, CheckBoxOutlined } from "@mui/icons-material";
const SettingsModal = ({ isOpen, close, submit, selectedValues, possibleValues, onChange }) => {
const handleCheckboxChange = (type, isChecked) => {
const updatedTypes = possibleValues.filter(v => {
if (v === type) return isChecked
return selectedValues.includes(v)
})
onChange(updatedTypes);
};
return (
<Modal
key="SettingsModal"
maxWidth="350px"
minHeight="200px"
open={isOpen}
headerText="History Settings"
onClose={() => close()}
>
<Box display="flex" flexDirection="column" justifyContent="space-between" gap="20px">
{possibleValues.map((type, i) => {
const isChecked = selectedValues.includes(type);
return (
<FormControlLabel
key={i}
control={
<Checkbox
inputProps={{ "data-testid": `history-type-${type}` }}
id={`history-type-${type}`}
checked={isChecked}
onChange={event => handleCheckboxChange(type, event.target.checked)}
icon={<CheckBoxOutlineBlank viewBox="0 0 24 24" />}
checkedIcon={<CheckBoxOutlined viewBox="0 0 24 24" />}
/>
}
label={<Typography variant="body1">{isChecked ? "Disable" : "Enable"} {type} History Storage</Typography>}
/>
)
})}
</Box>
</Modal>
)
}
export default SettingsModal;

View File

@ -0,0 +1,160 @@
import { useTransactionReceipt } from 'wagmi';
import { decodeEventLog } from 'viem';
import { useMemo } from "react";
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import { Box, Typography, Link } from "@mui/material";
import Token from "../../components/Token/Token";
import Modal from "../../components/Modal/Modal";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { PrimaryButton } from "../../components/Button";
import { abi as StakingAbi } from "../../abi/GhostStaking.json";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { formatNumber, shorten } from "../../helpers";
import { useTokenSymbol } from "../../hooks/tokens";
import { getTokenDecimals } from "../../hooks/helpers";
const TokenBox = ({ tokenAmount, tokenSymbol, decimals, chainTokenSymbol }) => {
return (
<Box display="flex" flexDirection="column" width="70px">
<Token
parentSx={{
height: "80px",
"& svg:nth-of-type(2)": {
transform: "translate(10%, -50%)",
zIndex: 100,
}
}}
sx={{ transform: "scale(1.65)", }}
chainTokenName={chainTokenSymbol}
name={tokenSymbol}
/>
<Box display="flex" flexDirection="column" alignItems="center">
<Typography>{tokenSymbol}</Typography>
<Typography>{formatNumber(
new DecimalBigNumber(tokenAmount ? tokenAmount : 0n, decimals),
5
)}</Typography>
</Box>
</Box>
)
}
const StakingViewModal = ({ chainId, isOpen, close, config, historyRecord }) => {
if (historyRecord === null) return null;
const { data: receipt, isLoading, error } = useTransactionReceipt({
hash: historyRecord?.txHash,
query: {
enabled: !!historyRecord?.txHash && isOpen,
}
});
const allDecodedLogs = useMemo(() => {
if (!receipt?.logs) return [];
return receipt.logs.map((log) => {
try {
const decoded = decodeEventLog({
abi: StakingAbi,
data: log.data,
topics: log.topics,
});
return {
eventName: decoded.eventName,
args: decoded.args,
address: log.address
};
} catch (e) {
return null;
}
}).filter(Boolean);
}, [receipt]);
const stakeInfo = useMemo(() => {
return allDecodedLogs.find(l => l.eventName === 'Staked');
}, [allDecodedLogs]);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const ftsoDecimals = getTokenDecimals("FTSO");
const chainInfo = useMemo(() => {
const client = config?.getClient();
return {
url: client?.chain?.blockExplorers?.default?.url,
symbol: client?.chain?.nativeCurrency?.symbol,
};
}, [config]);
return (
<Modal
maxWidth="420px"
minHeight="200px"
open={isOpen}
headerText={
<>
{stakeInfo && "Staked"}
{!stakeInfo && "Parsing Transaction Hash"}
</>
}
onClose={() => close()}
>
{stakeInfo && <Box display="flex" flexDirection="column" gap="15px">
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<Token
parentSx={{
height: "80px",
paddingLeft: "10px",
"& svg:nth-of-type(2)": {
transform: "translate(50%, -50%)",
zIndex: 100,
}
}}
sx={{ transform: "scale(1.65)", }}
chainTokenName={chainInfo.symbol}
name={ftsoSymbol ? ftsoSymbol : ""}
/>
<Box flexBasis="75%" display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between">
<Typography>Receiver</Typography>
<Link
href={`${chainInfo.url}/address/${stakeInfo.args.receiver}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{shorten(stakeInfo.args.receiver, 9, -7)}</Typography>
</Link>
</Box>
<Box display="flex" justifyContent="space-between">
<Typography>Amount</Typography>
<Typography>{formatNumber(
new DecimalBigNumber(stakeInfo ? stakeInfo.args.amount : 0n, ftsoDecimals),
5
)} {ftsoSymbol}</Typography>
</Box>
</Box>
</Box>
<PrimaryButton
fullWidth
component="a"
href={`${chainInfo.url}/tx/${historyRecord.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
View Transaction Details
</PrimaryButton>
</Box>}
</Modal>
)
}
export default StakingViewModal;

View File

@ -78,7 +78,6 @@ export const StakeContainer = ({ chainId, address, config, connect }) => {
Rebase&nbsp;
<Countdown otherwise="Ready" targetTime={epoch.end * 1000} />
</Typography>
</Box>
}
>

View File

@ -12,6 +12,7 @@ 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";
@ -22,6 +23,8 @@ 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 = () => {
@ -73,11 +76,13 @@ 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;
await stake(
txHash = await stake(
props.chainId,
props.address,
actionAmount._value.toBigInt(),
@ -87,10 +92,11 @@ const StakeConfirmationModal = (props) => {
props.stnkSymbol,
props.ghstSymbol
);
txName = CONSTANTS.stake.stake;
break;
case "UNSTAKE":
isRebase = props.upperToken === props.stnkSymbol;
await unstake(
txHash = await unstake(
props.chainId,
props.address,
actionAmount._value.toBigInt(),
@ -100,27 +106,32 @@ const StakeConfirmationModal = (props) => {
props.stnkSymbol,
props.ghstSymbol
);
txName = CONSTANTS.stake.unstake;
break;
case "WRAP":
await wrap(
txHash = await wrap(
props.chainId,
props.address,
actionAmount._value.toBigInt(),
props.ghstSymbol
);
txName = CONSTANTS.stake.wrap;
break;
case "UNWRAP":
await unwrap(
txHash = 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();
}
@ -156,7 +167,7 @@ const StakeConfirmationModal = (props) => {
chainTokenName={props.chainSymbol}
name={props.upperToken}
/>
<Typography>{props.upperToken}</Typography>
<Typography>{props.upperToken}</Typography>
</Box>
</Box>
<GhostStyledIcon component={ArrowRightIcon} />

View File

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

View File

@ -273,24 +273,26 @@ export const useNotes = (chainId, address) => {
return { notes, refetch };
}
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, referral, isNative }) => {
const args = [bondId, amount, maxPrice, user, referral];
export const purchaseBond = async ({ chainId, bondId, amount, maxPrice, user, sender, isNative }) => {
const args = [bondId, amount, maxPrice, user];
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.",
};
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
messages,
abi: BondAbi,
address: BOND_DEPOSITORY_ADDRESSES[chainId],
functionName: "deposit",
account: user,
account: sender,
value: isNative ? amount : undefined
});
return txHash;
}
export const redeem = async ({ chainId, user, isGhst, indexes }) => {

View File

@ -1,5 +1,4 @@
import { createContext, useContext, useState, useMemo } from "react";
import { claim } from "../../hooks/staking";
const emptyFunction = () => {};
const BreakoutModalContext = createContext();

View File

@ -2,76 +2,102 @@ 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 = "0x475e48fab52f3d0587b6b03101d224560c549e984d1dee197b7d8b55830e7da3"
const DEFAULT_CHAIN_ID = "0xcaf9c5ddccc7ba6f96149a6b8a81c26a3641742795cd5e7e0aee79bf3f4e77b7"
const UnstableProvider = createContext(null)
export const useUnstableProvider = () => useContext(UnstableProvider)
export const UnstableProviderProvider = ({ children }) => {
const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID);
const [providerIndex, setProviderIndex] = useState(0);
const [client, setClient] = useState(undefined);
const [status, setStatus] = useState("loading");
const { data: providerDetails } = useSWR("getGhostProviders", () =>
Unstable.getSubstrateConnectExtensionProviders()
);
const [selectedExtension, setSelectedExtension] = useState(null);
const [selectedAccountIndex, setSelectedAccountIndex] = useState(0);
const [accounts, setAccounts] = useState([]);
const providerDetail = useMemo(() => providerDetails?.at(providerIndex), [providerDetails, providerIndex]);
useEffect(() => {
let isMounted = true;
let provider = null;
const { data: provider } = useSWR(
() => providerDetail ? `ghostProviderDetail.${providerDetail.info.uuid}.provider` : null,
() => providerDetail ? providerDetail.provider : null
);
try {
const smoldot = startFromWorker(new SmWorker());
provider = createClientOther(
getSmProvider(() => smoldot.addChain({ chainSpec: casperChainSpec })),
)
const { data: accounts } = useSWR(
() => providerDetail ? `providerDetail.${providerDetail.info.uuid}.provider.getAccounts(${chainId})` : null,
async () => {
if (!providerDetail) return []
const activeProvider = await providerDetail.provider;
return activeProvider.getAccounts(chainId);
},
)
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]);
const observableClient = useMemo(() => client ? getObservableClient(client) : undefined, [client]);
const chainHead$ = useMemo(() => observableClient?.chainHead$(), [observableClient]);
useEffect(() => {
if (!selectedExtension) return;
const extensionAccounts = selectedExtension.getAccounts();
setAccounts(extensionAccounts)
const unsubscribe = selectedExtension.subscribe((updatedAccounts) => {
setAccounts(updatedAccounts);
});
return unsubscribe;
}, [selectedExtension]);
const account = useMemo(() => accounts?.at(selectedAccountIndex)?.address, [accounts, selectedAccountIndex]);
const casperApi = useMemo(() => client?.getTypedApi(casperMetadata), [client]);
const finalizedBlock$ = useMemo(() => client?.finalizedBlock$, [client]);
const value = useMemo(() => ({
isExtensionMissing: connectionState === "no-extension",
providerDetails,
providerDetail,
connectProviderByIndex: setProviderIndex,
chainId,
account,
status,
client,
setChainId,
chainHead$
}), [providerDetails, providerDetail, chainId, client, account, chainHead$]);
account,
accounts,
casperApi,
finalizedBlock$,
}), [accounts, status, client, finalizedBlock$]);
return (
<UnstableProvider.Provider value={value}>

View File

@ -0,0 +1,36 @@
export const casperChainSpec = `{
"badBlocks": null,
"bootNodes": [
"/dns/bootnode007.ghostchain.io/tcp/30335/p2p/12D3KooWF9SWxz9dmy6vfndQhoxqCa7PESaoFWEiF8Jkqh4xKDRf",
"/dns/bootnode007.ghostchain.io/tcp/30336/ws/p2p/12D3KooWF9SWxz9dmy6vfndQhoxqCa7PESaoFWEiF8Jkqh4xKDRf",
"/dns/bootnode007.ghostchain.io/tcp/443/wss/p2p/12D3KooWF9SWxz9dmy6vfndQhoxqCa7PESaoFWEiF8Jkqh4xKDRf",
"/dns/bootnode70.chain.ghostchain.io/tcp/30334/p2p/12D3KooWP3h5dSdqcpvsCr7fp1jyfqDj291QDZ68a4gY6VQ8T1nW",
"/dns/bootnode70.chain.ghostchain.io/tcp/30335/ws/p2p/12D3KooWP3h5dSdqcpvsCr7fp1jyfqDj291QDZ68a4gY6VQ8T1nW",
"/dns/bootnode70.chain.ghostchain.io/tcp/30336/wss/p2p/12D3KooWP3h5dSdqcpvsCr7fp1jyfqDj291QDZ68a4gY6VQ8T1nW"
],
"chainType": "Live",
"forkBlocks": null,
"genesis": {
"stateRootHash": "0xb4784a3c8f2f29ba3f191d563f1166f08f4e0337e114679c0f24ef77d06e10e4"
},
"id": "casper_staging_testnet",
"lightSyncState": {
"babeEpochChanges": "0x042c315b655c7144df4bfdc41837eeacb84d80aee06eaba51f6941a0c7a26cb1bf5d090000015e9de408000000000ea2e40800000000049b2e88b8ce43a677c1ca90f62134800b7e510c0c353dad719cad82faf6136cfb0d0e0000010ea2e40800000000bea6e408000000000000082c315b655c7144df4bfdc41837eeacb84d80aee06eaba51f6941a0c7a26cb1bf5d0900000103000000000000005e9de40800000000b00400000000000014daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e0100000000000000e0f0a776ecc9fa5e1f22e2fa001fe3fba5aea52b9444bc894b45589d4213247501000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f01000000000000002ff7c765405e9c11a59525a08173bb1fa59e5e281090f8e4d1824469f513d0cc01000000000000000400000000000000029b2e88b8ce43a677c1ca90f62134800b7e510c0c353dad719cad82faf6136cfb0d0e00000104000000000000000ea2e40800000000b00400000000000014daaaaab6a6e574099e24ae9bb75b543610edef9d374fa85a378edb573b47615f010000000000000074fa7381a7a74b316afb6793a00387eed9d95d46a69866cbb316b5d9c918af0e0100000000000000e0f0a776ecc9fa5e1f22e2fa001fe3fba5aea52b9444bc894b45589d4213247501000000000000006c4dd88b43e2011cf9a6a73d53446336ac9e04cdd4ca23587df63187ac455e4901000000000000003c944c704cae203619b9e7a5a4b6742736da6a8e76c762291bebdc7652cfec2f0100000000000000dea465ff8bf16938cc9ead89bf9fdb5d9c495fad61fd952b2067ebdc51c4e1c10100000000000000040000000000000002",
"babeFinalizedBlockWeight": 1039,
"finalizedBlockHeader": "0x6ed6c2debc2e5fd4e7db0d14426d1b904e6c101f2c62a71b8e891c217ba79079f9433280c0257a860f793833bd6bed6b9f86094400aa8e5ea24a81752f0f046ff5ed560f7231760c622ae35d3968fbab033ed34d950b57a7937ae45c411d1d384748080642414245b50103000000004fa0e40800000000a2c7d71b4e1f94c0208c9730ab93955b08c8d7ac86d2fc6ec5f62cc3272a5f1ec0e28c798461372109ee3e12f03ff66ea5ad9cac5b1c37d7280d2333d0226f0865ea77a801bad79785a8ca83c87f0dbf1e36936a6f65cd7b125a41c52f38f80105424142450101caaa6fe5f9df57930e2ac08aef43b75d5477d410c73c3a8e6dea9d5503b9fd25eae1d12b6604194389e22f3f4265d581eb162d1dc5a9034247fe33765dddbe8c",
"grandpaAuthoritySet": "0x1455446f9a7aa99ced06b317c80ce90d56b84e56526775683af2525969e8da0b640100000000000000236d2fa03f4ed8cb65de7e514d7540159b328f1c170dd402b094ad7fbf5472180100000000000000b63c5a0cf342b9b04931bc8ed74d7d0165ab99ab5f8a4514797d4b299a4501fe01000000000000008f9ea20bf4a807a8e710f7559dece86e94672b5b361de157bdaa5c1f37849f8d01000000000000000aa3a88f6b777c95c3dfe7e997b76798413f16aa325f34824cae0c9102b281d5010000000000000000000000000000000001fe1000000000"
},
"name": "Casper Staging Testnet",
"properties": {
"ss58Format": 1996,
"tokenDecimals": 18,
"tokenSymbol": "CSPR"
},
"protocolId": "cspr",
"telemetryEndpoints": [
[
"/dns/telemetry.ghostchain.io/tcp/443/x-parity-wss/%2Fsubmit%2F",
0
]
]
}`;

View File

@ -6,7 +6,7 @@ import {
FTSO_ADDRESSES,
STNK_ADDRESSES,
GHST_ADDRESSES,
FTSO_DAI_LP_ADDRESSES,
FTSO_RESERVE_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_DAI_LP_ADDRESSES[chainId];
address = FTSO_RESERVE_LP_ADDRESSES[chainId];
break;
case "RESERVE_FTSO":
address = FTSO_DAI_LP_ADDRESSES[chainId];
address = FTSO_RESERVE_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_DAI_LP_ADDRESSES[chainId]:
case FTSO_RESERVE_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_DAI_LP_ADDRESSES[chainId]:
case FTSO_RESERVE_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_DAI_LP_ADDRESSES[chainId].toUpperCase()) {
if (tokenAddress.toUpperCase() === FTSO_RESERVE_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_DAI_LP_ADDRESSES[chainId]:
case FTSO_RESERVE_LP_ADDRESSES[chainId]:
purchaseUrl += `?pool=true&from=${RESERVE_ADDRESSES[chainId]}&to=${FTSO_ADDRESSES[chainId]}`;
break;
}
@ -248,6 +248,7 @@ export const executeOnChainTransaction = async ({
onReplaced: () => toast(messages.replacedMsg),
chainId
});
// TODO: onReplaced
toast.success(messages.successMsg);
@ -258,3 +259,40 @@ 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;
}
};

View File

@ -1,26 +1,117 @@
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 getStorageValue = (chainId, address, target, defaultValue) => {
const key = getStorageKey(chainId, address, target);
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
}
const useStorageValue = (chainId, address, target, defaultValue) => {
return useSWR(
chainId && address ? ["local", chainId, address, target] : null,
([_, chainId, address, target]) => {
const key = getStorageKey(chainId, address, target);
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
},
{ revalidateOnFocus: false, fallbackData: defaultValue }
);
};
const setStorageValue = (chainId, address, target, value) => {
const setStorageValue = async (chainId, address, target, value) => {
const key = getStorageKey(chainId, address, target);
localStorage.setItem(key, JSON.stringify(value));
await mutate(["local", chainId, address, target]);
}
const 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={{ getStorageValue, setStorageValue }}>
<LocalStorageContext.Provider value={{
CONSTANTS,
tryStoreInHistory,
useStorageValue,
clearPendingHistory,
setStorageValue
}}>
{children}
</LocalStorageContext.Provider>
)

View File

@ -5,7 +5,7 @@ import { useCurrentIndex, useGhostedSupply } from "../staking";
import { useUniswapV2PairReserves } from "../uniswapv2";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import {
FTSO_DAI_LP_ADDRESSES,
FTSO_RESERVE_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_DAI_LP_ADDRESSES[chainId]);
const { reserves, tokens, refetch } = useUniswapV2PairReserves(chainId, FTSO_RESERVE_LP_ADDRESSES[chainId]);
const reservePrice = useReservePrice(chainId);
const reserveAddress = RESERVE_ADDRESSES[chainId];

View File

@ -14,9 +14,92 @@ import { abi as GatekeeperAbi } from "../../abi/GhostGatekeeper.json";
import { shorten } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { executeOnChainTransaction } from "../helpers";
import { executeOnChainTransaction, getTokenDecimals } from "../helpers";
import { useCirculatingSupply } from "../tokens";
export const 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);
@ -32,11 +115,11 @@ export const useGatekeeperApy = (chainId) => {
const amountIn = new DecimalBigNumber(
(metadata?.amountIn ?? 0n) + (amountInHistory[chainId] ?? 0n),
18,
getTokenDecimals("GHST"),
);
const amountOut = new DecimalBigNumber(
(metadata?.amountOut ?? 0n) + (amountOutHistory[chainId] ?? 0n),
18,
getTokenDecimals("GHST"),
);
const deployedAt = metadata?.deployedAt ?? 0;
@ -181,7 +264,7 @@ export const stake = async (chainId, account, amount, isRebase, isClaim, ftsoSym
successMsg: `${ftsoSymbol} tokens staked successfully! Wait for the warm-up period to claim your ${isRebase ? stnkSymbol : ghstSymbol}.`,
errorMsg: "Staking transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
messages,
@ -190,6 +273,7 @@ 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) => {
@ -199,7 +283,7 @@ export const unstake = async (chainId, account, amount, isTrigger, isRebase, fts
successMsg: `${isRebase ? stnkSymbol : ghstSymbol} tokens unstaked successfully! Check your ${ftsoSymbol} balance on ${shorten(account)}.`,
errorMsg: "Unstake transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
messages,
@ -208,6 +292,7 @@ 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) => {
@ -216,7 +301,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.",
};
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
messages,
account,
@ -225,6 +310,7 @@ export const forfeit = async (chainId, account, ftsoSymbol) => {
address: STAKING_ADDRESSES[chainId],
functionName: "forfeit",
});
return txHash;
}
export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) => {
@ -234,7 +320,7 @@ export const claim = async (chainId, account, isStnk, stnkSymbol, ghstSymbol) =>
successMsg: `${isStnk ? stnkSymbol : ghstSymbol} tokens claimed successfully to ${shorten(account)}.`,
errorMsg: "Claim transaction failed. Check logs for error detalization.",
};
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
messages,
@ -243,6 +329,7 @@ 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) => {
@ -261,7 +348,6 @@ export const breakout = async (chainId, account, receiver, amount) => {
address: STAKING_ADDRESSES[chainId],
functionName: "breakout",
});
return txHash;
}
@ -281,6 +367,7 @@ export const unwrap = async (chainId, account, amount, stnkSymbol) => {
address: STAKING_ADDRESSES[chainId],
functionName: "unwrap",
});
return txHash;
}
export const wrap = async (chainId, account, amount, ghstSymbol) => {
@ -299,13 +386,14 @@ 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 tx hash status or wait for slow clap finalization.`,
successMsg: `Amount successfully bridged! Check Transaction History to trace the progress.`,
errorMsg: "Bridge transaction failed. Check logs for error detalization.",
};
const txHash = await executeOnChainTransaction({
@ -317,6 +405,5 @@ export const ghost = async (chainId, account, receiver, amount) => {
address: STAKING_ADDRESSES[chainId],
functionName: "ghost",
});
return txHash;
}

View File

@ -33,7 +33,7 @@ export const swapExactETHForTokens = async ({
deadline
];
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
abi: RouterAbi,
@ -43,6 +43,8 @@ export const swapExactETHForTokens = async ({
messages: swapMessages,
value: amountADesired,
});
return txHash;
}
export const swapExactTokensForETH = async ({
@ -63,7 +65,7 @@ export const swapExactTokensForETH = async ({
deadline
];
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
abi: RouterAbi,
@ -72,6 +74,8 @@ export const swapExactTokensForETH = async ({
account: address,
messages: swapMessages,
});
return txHash;
}
export const swapExactTokensForTokens = async ({
@ -92,7 +96,7 @@ export const swapExactTokensForTokens = async ({
deadline
];
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
abi: RouterAbi,
@ -101,6 +105,8 @@ export const swapExactTokensForTokens = async ({
account: address,
messages: swapMessages,
});
return txHash;
}
export const addLiquidity = async ({
@ -126,7 +132,7 @@ export const addLiquidity = async ({
deadline
];
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
abi: RouterAbi,
@ -135,6 +141,8 @@ export const addLiquidity = async ({
account: address,
messages: addMessages,
});
return txHash;
}
export const addLiquidityETH = async ({
@ -172,7 +180,7 @@ export const addLiquidityETH = async ({
deadline
];
await executeOnChainTransaction({
const txHash = await executeOnChainTransaction({
chainId,
args,
abi: RouterAbi,
@ -182,4 +190,6 @@ export const addLiquidityETH = async ({
messages: addMessages,
value: amountETHDesired,
});
return txHash;
}

View File

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