diff --git a/.gitignore b/.gitignore index 2cfb326..730e58d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,7 @@ yarn-debug.log* yarn-error.log* .env -src/metadata/metaportConfig*.ts +src/data/metaportConfig*.ts src/meta/ src/metadata/chainsData.json diff --git a/build.sh b/build.sh index d62de93..9ea5bdb 100644 --- a/build.sh +++ b/build.sh @@ -7,7 +7,7 @@ set -e export DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" METAPORT_CONFIG_PATH=$DIR/config/$NETWORK_NAME.ts -METAPORT_CONFIG_PATH_SRC=$DIR/src/metadata/metaportConfig.ts +METAPORT_CONFIG_PATH_SRC=$DIR/src/data/metaportConfig.ts echo "Copying ${METAPORT_CONFIG_PATH} -> ${METAPORT_CONFIG_PATH_SRC}..." cp $METAPORT_CONFIG_PATH $METAPORT_CONFIG_PATH_SRC diff --git a/bun.lockb b/bun.lockb index a2d21f4..1b49de2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/config/mainnet.ts b/config/mainnet.ts index 79450dd..f2e9b7b 100644 --- a/config/mainnet.ts +++ b/config/mainnet.ts @@ -63,12 +63,6 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { symbol: 'HMT', iconUrl: 'https://s2.coinmarketcap.com/static/img/coins/64x64/10347.png' }, - ubxs: { - name: 'UBXS Token', - symbol: 'UBXS', - decimals: '6', - iconUrl: 'https://s2.coinmarketcap.com/static/img/coins/64x64/17242.png' - }, razor: { "decimals": "18", "name": "RAZOR Network", diff --git a/index.html b/index.html index 00dc09f..a7a7475 100644 --- a/index.html +++ b/index.html @@ -26,6 +26,7 @@ + diff --git a/package.json b/package.json index c7f5f03..8255040 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "portal", "private": true, - "version": "2.0.2", + "version": "2.1.0", "type": "module", "scripts": { "dev": "vite", @@ -15,7 +15,7 @@ "@mdx-js/rollup": "^2.3.0", "@mui/icons-material": "^5.14.8", "@mui/material": "^5.14.5", - "@skalenetwork/metaport": "2.0.2-develop.1", + "@skalenetwork/metaport": "2.0.3-develop.3", "@types/react-copy-to-clipboard": "^5.0.4", "@vercel/analytics": "^1.0.2", "eslint-config-prettier": "^9.0.0", @@ -35,6 +35,7 @@ "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.4", + "bun-types": "^1.0.17", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", diff --git a/public/apps.png b/public/apps.png new file mode 100644 index 0000000..a50f6ce Binary files /dev/null and b/public/apps.png differ diff --git a/public/bridge.png b/public/bridge.png new file mode 100644 index 0000000..28d038f Binary files /dev/null and b/public/bridge.png differ diff --git a/public/chains.jpg b/public/chains.jpg deleted file mode 100644 index a44132b..0000000 Binary files a/public/chains.jpg and /dev/null differ diff --git a/public/chains.png b/public/chains.png new file mode 100644 index 0000000..892b8c9 Binary files /dev/null and b/public/chains.png differ diff --git a/public/portfolio.png b/public/portfolio.png new file mode 100644 index 0000000..7039970 Binary files /dev/null and b/public/portfolio.png differ diff --git a/public/stats.png b/public/stats.png new file mode 100644 index 0000000..8ab3336 Binary files /dev/null and b/public/stats.png differ diff --git a/src/App.scss b/src/App.scss index afa5d12..bf72dfd 100644 --- a/src/App.scss +++ b/src/App.scss @@ -86,6 +86,8 @@ body { padding-right: 12px; padding-left: 8px; + min-height: 0 !important; + .mp__iconConnect { margin-left: 10px; width: 18pt !important; @@ -142,7 +144,7 @@ body { .br__modalInner { - max-height: calc(90vh - 300px); + max-height: calc(90vh - 250px); display: flex; /* relevant part */ flex-direction: column; @@ -158,15 +160,20 @@ body { overflow: hidden; } -.br__modal { - .MuiContainer-root { - padding: 0px !important; +.br__modalScroll { + @media screen and (min-height: 200px) { + max-height: calc(75vh - 180px); + } + + @media screen and (min-height: 670px) and (min-width: 300px) { + max-height: calc(75vh - 520px); } - .br__modalScroll { - height: 100%; - overflow-y: auto; + @media screen and (min-height: 670px) and (min-width: 900px) { + max-height: calc(75vh - 350px); } + + overflow-y: auto; } .drawerIconRi { @@ -176,10 +183,14 @@ body { margin-right: 12px !important; } -.br__modalScroll { - ::-webkit-scrollbar { - display: unset !important; - } +// .br__modalScroll { +// ::-webkit-scrollbar { +// display: unset !important; +// } +// } + +::-webkit-scrollbar-track { + height: 5px !important; } ::-webkit-scrollbar-thumb { @@ -219,24 +230,34 @@ body::-webkit-scrollbar { } .br__tile { - box-shadow: 0px -4px 6px rgba(255, 255, 255, 0.05); + // box-shadow: 0px -4px 6px rgba(255, 255, 255, 0.05); transform-style: preserve-3d; transition: all 0.3s ease; } .br__tile:hover { - box-shadow: 0px -4px 6px rgba(255, 255, 255, 0.1); transform: scale(1.05); } .br__tile { - height: 140px; + height: 150px; + border-radius: 25px !important; + border: 1px solid #171616 !important; +} + +.pageCard { + height: 100% !important; border-radius: 25px !important; border: 1px rgb(44 44 44) solid !important; + + svg { + width: 20px; + height: 20px; + } } .br__tileLogo { - height: 95px; + height: 105px; img { max-height: 100%; @@ -322,23 +343,46 @@ body::-webkit-scrollbar { width: 100%; } +.btn { + text-transform: none !important; + font-size: 0.8025rem !important; + line-height: 1.6 !important; + letter-spacing: 0.02857em !important; + font-weight: 600 !important; + padding: 0.9em 3.5em !important; + border-radius: $sk-border-radius !important; + box-shadow: none !important; +} + +.outlined { + background: rgba(41, 255, 148, 0.08) +} + +.roundBtn { + min-width: 40px !important; + padding: 8px 11px !important; +} + .cardBtn { background: rgba(0, 0, 0, .506) !important; - border: 1px solid hsla(0, 0%, 51%, .35) !important; + border: 1px solid hsla(0, 0%, 51%, .1) !important; text-transform: none !important; - color: white !important; - padding: 5px 10px 5px 15px !important; - font-size: 0.6125rem !important; + color: #ffffffe6 !important; + padding: 6px 10px 6px 12px !important; + font-size: 0.7rem !important; + min-width: 55px; + + width: 100%; + + font-weight: 700 !important; svg { - width: 10pt; - height: 10pt; + width: 8pt; + height: 8pt; } - .MuiButton-endIcon { - margin-left: 5px !important; - margin-right: 2px !important; - // margin: 0 !important; + .MuiButton-startIcon { + margin-right: 5px !important; } } @@ -424,9 +468,11 @@ code { } .chainDetails { + max-width: calc(100vw - 32px); + .logo { - height: 60pt; - margin: 30px 0; + height: 65pt; + margin: 35px 0; width: 100%; text-align: center; @@ -437,22 +483,53 @@ code { } } - .titleSection { - background: rgba(0, 0, 0, 0.6) !important; - border-radius: 25px; - padding: 20px; - } - .titleBadge { background: rgba(0, 0, 0, 0.6) !important; border-radius: 25px; - padding: 10px 15px; + padding: 5px; svg { width: 12px; height: 12px; } } + + .monthInputWrap { + border-radius: 25px; + background: rgba(41, 255, 148, 0.08); + padding-left: 20px; + } + + .monthInput { + width: 35pt; + + input { + font-weight: 500; + line-height: 0.9; + } + + .MuiInputBase-input { + padding: 0 !important; + } + + .MuiInput-root::after { + display: none !important; + border-bottom: none !important; + } + + .MuiInput-root::before { + display: none !important; + border-bottom: none !important; + } + } + + +} + +.titleSection { + background: rgba(0, 0, 0, 0.6); + border-radius: 25px; + padding: 20px; } button:focus { @@ -472,4 +549,68 @@ button:focus { .MuiAccordion-rounded { box-shadow: none !important; +} + +.startCardText { + margin: 120px 10px 10px 10px; + background: #00000066; + backdrop-filter: blur(10px) brightness(0.8); + border-radius: 25px; + padding: 10px 15px; +} + +.startCardBg { + background-repeat: no-repeat; + background-size: cover; + border: none !important; + border: 1px solid #171616 !important; + box-shadow: none !important; +} + +.startCardbridge { + background-image: url(/bridge.png); +} + +.startCardchains { + background-image: url(/chains.png); +} + +.startCardapps { + background-image: url(/apps.png); +} + + +.startCardstats { + background-image: url(/stats.png); +} + +.startCardportfolio { + background-image: url(/portfolio.png); +} + + +.blackP { + color: #000000; +} + +.divider { + border-bottom: 1px $border-color solid; + padding-bottom: 10px; +} + +.flexi { + display: inline-flex; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; + /* <-- Apparently some margin are still there even though it's hidden */ +} + +input[type=number] { + -moz-appearance: textfield; + /* Firefox */ } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 84d2a5f..7b39af9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,7 @@ import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles' import Portal from './Portal' -import { METAPORT_CONFIG } from './metadata/metaportConfig' +import { METAPORT_CONFIG } from './data/metaportConfig' import { createMuiTheme } from './core/themes' import { META_TAGS } from './core/meta' diff --git a/src/Portal.tsx b/src/Portal.tsx index c35eb29..207dd9f 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -21,27 +21,22 @@ * @copyright SKALE Labs 2023-Present */ -import { useState } from 'react' - import Box from '@mui/material/Box' import CssBaseline from '@mui/material/CssBaseline' import Header from './Header' import SkDrawer from './SkDrawer' import Router from './Router' -import TermsModal from './components/TermsModal' import { useMetaportStore, useWagmiAccount, Debug, cls, cmn } from '@skalenetwork/metaport' export default function Portal() { - const [termsAccepted, setTermsAccepted] = useState(false) const mpc = useMetaportStore((state) => state.mpc) const { address } = useWagmiAccount() if (!mpc) return
return ( -
diff --git a/src/Router.tsx b/src/Router.tsx index d5ee23c..bc0f985 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -2,23 +2,27 @@ import './App.scss' import { useState, useEffect } from 'react' import { Helmet } from 'react-helmet' -import { useLocation } from 'react-router-dom' -import { Routes, Route } from 'react-router-dom' +import { useLocation, Routes, Route } from 'react-router-dom' import { useMetaportStore, PROXY_ENDPOINTS, type MetaportState } from '@skalenetwork/metaport' -import Bridge from './components/Bridge' -import Faq from './components/Faq' -import Terms from './components/Terms' -import Network from './components/Network' -import Schain from './components/Schain' -import Stats from './components/Stats' -import Apps from './components/Apps' +import Bridge from './pages/Bridge' +import Faq from './pages/Faq' +import Terms from './pages/Terms' +import Network from './pages/Chains' +import Schain from './pages/Schain' +import Stats from './pages/Stats' +import Apps from './pages/Apps' import App from './components/App' -import History from './components/History' -import Portfolio from './components/Portfolio' -import Admin from './components/Admin' +import History from './pages/History' +import Portfolio from './pages/Portfolio' +import Admin from './pages/Admin' +import Start from './pages/Start' +import TermsModal from './components/TermsModal' import { getHistoryFromStorage, setHistoryToStorage } from './core/transferHistory' +import { BRIDGE_PAGES } from './core/constants' +import { pricingLaunchTsReached } from './core/paymaster' + // import chainsJson from './chainsJson.json'; export default function Router() { @@ -26,6 +30,7 @@ export default function Router() { const currentUrl = `${window.location.origin}${location.pathname}${location.search}` const [schains, setSchains] = useState([]) + const [termsAccepted, setTermsAccepted] = useState(false) const mpc = useMetaportStore((state: MetaportState) => state.mpc) const transfersHistory = useMetaportStore((state) => state.transfersHistory) @@ -53,13 +58,26 @@ export default function Router() { setSchains(schains) } + function isBridgePage(): boolean { + return BRIDGE_PAGES.some( + (pathname) => location.pathname === pathname || location.pathname.includes(pathname) + ) + } + + if (!termsAccepted && isBridgePage()) { + return ( + + ) + } + return (
- } /> + } /> + } /> } /> @@ -83,9 +101,11 @@ export default function Router() { } /> } /> - - } /> - + {pricingLaunchTsReached(mpc.config.skaleNetwork) ? ( + + } /> + + ) : null}
) diff --git a/src/SkBottomNavigation.tsx b/src/SkBottomNavigation.tsx index 587e2c8..c2e3af6 100644 --- a/src/SkBottomNavigation.tsx +++ b/src/SkBottomNavigation.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import { useState, useEffect } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import Box from '@mui/material/Box' @@ -9,7 +9,7 @@ import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined' import AccountBalanceWalletOutlinedIcon from '@mui/icons-material/AccountBalanceWalletOutlined' export default function SkBottomNavigation() { - const [value, setValue] = React.useState(0) + const [value, setValue] = useState(0) const navigate = useNavigate() const location = useLocation() diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index 83315a1..91d84cd 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -1,5 +1,5 @@ -import { Link, useLocation } from 'react-router-dom' import { cls, cmn } from '@skalenetwork/metaport' +import { useLocation, Link } from 'react-router-dom' import Box from '@mui/material/Box' @@ -13,7 +13,6 @@ import ListItemIcon from '@mui/material/ListItemIcon' import ListItemText from '@mui/material/ListItemText' import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontalCircleOutlined' -import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined' import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' import HistoryIcon from '@mui/icons-material/History' import InsertChartOutlinedIcon from '@mui/icons-material/InsertChartOutlined' @@ -21,9 +20,11 @@ import InsertChartOutlinedIcon from '@mui/icons-material/InsertChartOutlined' // import WalletOutlinedIcon from '@mui/icons-material/WalletOutlined' import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' import DonutLargeRoundedIcon from '@mui/icons-material/DonutLargeRounded' +import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined' + import { DUNE_SKALE_URL } from './core/constants' -const drawerWidth = 240 +const drawerWidth = 220 export default function SkDrawer() { const location = useLocation() @@ -42,13 +43,27 @@ export default function SkDrawer() { > -

Bridge

+ + + + + + + + + +

Bridge

+ + + @@ -83,7 +98,7 @@ export default function SkDrawer() { */} - + {/* - + */}

Network

@@ -103,7 +118,9 @@ export default function SkDrawer() { diff --git a/src/components/AccordionLink.tsx b/src/components/AccordionLink.tsx new file mode 100644 index 0000000..0afd750 --- /dev/null +++ b/src/components/AccordionLink.tsx @@ -0,0 +1,55 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file AccordionLink.tsx + * @copyright SKALE Labs 2023-Present + */ + +import { ReactElement } from 'react' + +import ButtonBase from '@mui/material/ButtonBase' +import AddCircleRoundedIcon from '@mui/icons-material/AddCircleRounded' + +import { cmn, cls, styles } from '@skalenetwork/metaport' +import { Link } from 'react-router-dom' + +export default function AccordionLink(props: { + title: string + url: string + icon?: ReactElement + className?: string +}) { + return ( +
+ + +
+ {props.icon ? ( +
+ {props.icon} +
+ ) : null} +

{props.title}

+ +
+
+ +
+ ) +} diff --git a/src/components/AccordionSection.tsx b/src/components/AccordionSection.tsx index eb60916..ae05995 100644 --- a/src/components/AccordionSection.tsx +++ b/src/components/AccordionSection.tsx @@ -35,12 +35,13 @@ export default function AccordionSection(props: { expanded: string | false panel: string title: string + subtitle?: string children: ReactElement | ReactElement[] icon?: ReactElement className?: string }) { return ( -
+
props.handleChange(props.panel)} className={cls(cmn.fullWidth, cmn.flex, cmn.pleft, cmn.bordRad)} @@ -52,6 +53,7 @@ export default function AccordionSection(props: {
) : null}

{props.title}

+

{props.subtitle}

{props.expanded === props.panel ? ( ) : ( diff --git a/src/components/AccountMenu.tsx b/src/components/AccountMenu.tsx index 1a6720b..fd8420a 100644 --- a/src/components/AccountMenu.tsx +++ b/src/components/AccountMenu.tsx @@ -35,6 +35,7 @@ import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward' import HistoryIcon from '@mui/icons-material/History' import SignalCellularAltOutlinedIcon from '@mui/icons-material/SignalCellularAltOutlined' import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded' +import LooksRoundedIcon from '@mui/icons-material/LooksRounded' import { cls, styles, cmn, RainbowConnectButton } from '@skalenetwork/metaport' @@ -48,23 +49,47 @@ export default function AccountMenu(props: any) { setAnchorEl(null) } - if (!props.address) return return (
- - + ) + }} +
- {props.address.substring(0, 5) + - '...' + - props.address.substring(props.address.length - 3)} - - + + ) : ( + + + + )} +
{getCategoryIcon(props.category)}

{props.category}

diff --git a/src/components/CategorySection.tsx b/src/components/CategorySection.tsx index 9819e07..f1d68ed 100644 --- a/src/components/CategorySection.tsx +++ b/src/components/CategorySection.tsx @@ -49,17 +49,9 @@ export default function CategorySection(props: {

{props.category}

- + {schains.map((schain: any[]) => ( - + ))} diff --git a/src/components/ChainAccordion.tsx b/src/components/ChainAccordion.tsx index 037bd64..3ce0804 100644 --- a/src/components/ChainAccordion.tsx +++ b/src/components/ChainAccordion.tsx @@ -27,6 +27,7 @@ import Grid from '@mui/material/Grid' import ConstructionRoundedIcon from '@mui/icons-material/ConstructionRounded' import PlaylistAddCheckCircleRoundedIcon from '@mui/icons-material/PlaylistAddCheckCircleRounded' import AccountBalanceWalletRoundedIcon from '@mui/icons-material/AccountBalanceWalletRounded' +import AdminPanelSettingsRoundedIcon from '@mui/icons-material/AdminPanelSettingsRounded' import { cmn, @@ -42,6 +43,7 @@ import { import VerifiedContracts from './VerifiedContracts' import CopySurface from './CopySurface' import AccordionSection from './AccordionSection' +import AccordionLink from './AccordionLink' import { getRpcUrl, @@ -52,14 +54,16 @@ import { HTTPS_PREFIX, WSS_PREFIX } from '../core/chain' +import { pricingLaunchTsReached } from '../core/paymaster' export default function ChainAccordion(props: { schainName: string mpc: MetaportCore className?: string }) { - const proxyBase = PROXY_ENDPOINTS[props.mpc.config.skaleNetwork] - const explorerBase = BASE_EXPLORER_URLS[props.mpc.config.skaleNetwork] + const network = props.mpc.config.skaleNetwork + const proxyBase = PROXY_ENDPOINTS[network] + const explorerBase = BASE_EXPLORER_URLS[network] const rpcUrl = getRpcUrl(proxyBase, props.schainName, HTTPS_PREFIX) const rpcWssUrl = getRpcWsUrl(proxyBase, props.schainName, WSS_PREFIX) @@ -188,6 +192,15 @@ export default function ChainAccordion(props: { explorerUrl={explorerUrl} /> + {pricingLaunchTsReached(network) ? ( + } + url={`/admin/${props.schainName}`} + /> + ) : ( +
+ )} ) } diff --git a/src/components/ChainAccordion/index.ts b/src/components/ChainAccordion/index.ts deleted file mode 100644 index 5f2df1e..0000000 --- a/src/components/ChainAccordion/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../ChainAccordion' diff --git a/src/components/ChainCard.tsx b/src/components/ChainCard.tsx index 0811b1d..7378243 100644 --- a/src/components/ChainCard.tsx +++ b/src/components/ChainCard.tsx @@ -27,23 +27,16 @@ import { cls, chainBg, getChainAlias, - BASE_EXPLORER_URLS, CHAINS_META, type interfaces } from '@skalenetwork/metaport' import Button from '@mui/material/Button' -import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward' -import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded' - import ChainLogo from './ChainLogo' -import { getExplorerUrl } from '../core/chain' import { MAINNET_CHAIN_LOGOS } from '../core/constants' export default function ChainCard(props: { skaleNetwork: interfaces.SkaleNetwork; schain: any[] }) { - const explorerUrl = getExplorerUrl(BASE_EXPLORER_URLS[props.skaleNetwork], props.schain[0]) - function getChainShortAlias(meta: interfaces.ChainsMetadataMap, name: string): string { return meta[name] && meta[name].shortAlias !== undefined ? meta[name].shortAlias! : name } @@ -71,28 +64,33 @@ export default function ChainCard(props: { skaleNetwork: interfaces.SkaleNetwork cmn.flex, cmn.flexcv, cmn.mbott10, - cmn.mleft10, + 'br__tileBott', 'fullWidth' )} > -
- - - - - - -
+ + +
-

- {getChainAlias(props.skaleNetwork, props.schain[0], undefined, true)} -

) diff --git a/src/components/ChainCategories.tsx b/src/components/ChainCategories.tsx index eb2c1aa..4919d6a 100644 --- a/src/components/ChainCategories.tsx +++ b/src/components/ChainCategories.tsx @@ -29,19 +29,29 @@ import { Link } from 'react-router-dom' export default function ChainCategories(props: { category: string | string[] | undefined - className?: string + alias: string }) { if (!props.category) return return ( -
- - - +
+
+
+ + + +
+

|

+
+
+

{props.alias}

+
+
+
-
+
{isString(props.category) ? ( ) : ( diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx new file mode 100644 index 0000000..a25d42f --- /dev/null +++ b/src/components/ConnectWallet.tsx @@ -0,0 +1,60 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file ConnectWallet.tsx + * @copyright SKALE Labs 2023-Present + */ + +import Button from '@mui/material/Button' +import LooksRoundedIcon from '@mui/icons-material/LooksRounded' +import { cmn, cls, SkPaper, RainbowConnectButton } from '@skalenetwork/metaport' + +export default function ConnectWallet(props: { tile?: boolean; className?: string }) { + return ( +
+ +
+

Connect your wallet to continue

+
+
+
+ + {({ openConnectModal }) => { + return ( + + ) + }} + +
+
+
+
+
+
+ ) +} diff --git a/src/components/CopySurface.tsx b/src/components/CopySurface.tsx index 8a1a406..6d80ae1 100644 --- a/src/components/CopySurface.tsx +++ b/src/components/CopySurface.tsx @@ -59,7 +59,7 @@ export default function CopySurface(props: {
- +
{props.tokenMetadata ? ( diff --git a/src/components/Faq/index.ts b/src/components/Faq/index.ts deleted file mode 100644 index 70d0a8d..0000000 --- a/src/components/Faq/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Faq' diff --git a/src/components/FaqAccordion/FaqAccordion.tsx b/src/components/FaqAccordion.tsx similarity index 94% rename from src/components/FaqAccordion/FaqAccordion.tsx rename to src/components/FaqAccordion.tsx index 42f73ef..f15bd1c 100644 --- a/src/components/FaqAccordion/FaqAccordion.tsx +++ b/src/components/FaqAccordion.tsx @@ -24,8 +24,8 @@ import { useState } from 'react' import { cls, cmn, SkPaper } from '@skalenetwork/metaport' -import AccordionSection from '../AccordionSection' -import { FAQ } from '../../core/constants' +import AccordionSection from './AccordionSection' +import { FAQ } from '../core/constants' export default function FaqAccordion() { const [expanded, setExpanded] = useState(false) diff --git a/src/components/FaqAccordion/index.ts b/src/components/FaqAccordion/index.ts deleted file mode 100644 index be3051e..0000000 --- a/src/components/FaqAccordion/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FaqAccordion' diff --git a/src/components/HelpZen.tsx b/src/components/HelpZen.tsx new file mode 100644 index 0000000..3db55ac --- /dev/null +++ b/src/components/HelpZen.tsx @@ -0,0 +1,111 @@ +import { Link } from 'react-router-dom' + +import { useEffect, useState, MouseEvent } from 'react' +import Box from '@mui/material/Box' +import Tooltip from '@mui/material/Tooltip' +import IconButton from '@mui/material/IconButton' +import Menu from '@mui/material/Menu' +import MenuItem from '@mui/material/MenuItem' +import QuestionMarkRoundedIcon from '@mui/icons-material/QuestionMarkRounded' +import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined' +import MarkUnreadChatAltRoundedIcon from '@mui/icons-material/MarkUnreadChatAltRounded' +import { cls, styles, cmn } from '@skalenetwork/metaport' + +export default function HelpZen() { + const [anchorEl, setAnchorEl] = useState(null) + const [openZen, setOpenZen] = useState(false) + + const open = Boolean(anchorEl) + + const handleClick = (event: MouseEvent) => { + setAnchorEl(event.currentTarget) + } + const handleClose = () => { + setAnchorEl(null) + } + + function handleClickZen() { + window.zE('messenger', openZen ? 'close' : 'open') + } + + useEffect(() => { + window.zE('messenger', 'close') + window.zE('messenger:on', 'open', () => { + setOpenZen(true) + }) + window.zE('messenger:on', 'close', () => { + setOpenZen(false) + }) + }, []) + + return ( +
+ + + + + + + + + + Open support chat + + + + Bridge FAQ + + + +
+ ) +} diff --git a/src/components/HelpZen/HelpZen.tsx b/src/components/HelpZen/HelpZen.tsx deleted file mode 100644 index 28f6837..0000000 --- a/src/components/HelpZen/HelpZen.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { useEffect } from 'react' -import Box from '@mui/material/Box' -import Button from '@mui/material/Button' -import Tooltip from '@mui/material/Tooltip' -import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded' - -import { cls, styles, cmn } from '@skalenetwork/metaport' - -export default function HelpZen() { - const [open, setOpen] = React.useState(false) - - useEffect(() => { - window.zE('messenger', 'close') - window.zE('messenger:on', 'open', () => { - setOpen(true) - }) - window.zE('messenger:on', 'close', () => { - setOpen(false) - }) - }, []) - - function handleClick() { - window.zE('messenger', open ? 'close' : 'open') - } - - return ( - - - - - - - - ) -} diff --git a/src/components/HelpZen/index.ts b/src/components/HelpZen/index.ts deleted file mode 100644 index 0d91c63..0000000 --- a/src/components/HelpZen/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './HelpZen' diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx new file mode 100644 index 0000000..78d22f3 --- /dev/null +++ b/src/components/Loader.tsx @@ -0,0 +1,40 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Loader.tsx + * @copyright SKALE Labs 2023-Present + */ + +import { cmn, cls } from '@skalenetwork/metaport' +import CircularProgress from '@mui/material/CircularProgress' + +export default function Loader(props: { text: string }) { + return ( +
+
+
+ +
+
+

{props.text}

+
+
+
+ ) +} diff --git a/src/components/MonthSelector.tsx b/src/components/MonthSelector.tsx new file mode 100644 index 0000000..db58ae8 --- /dev/null +++ b/src/components/MonthSelector.tsx @@ -0,0 +1,144 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file MonthSelector.tsx + * @copyright SKALE Labs 2023-Present + */ + +import { useState } from 'react' +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined' +import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded' +import { cmn, cls } from '@skalenetwork/metaport' + +import { formatTimePeriod } from '../core/timeHelper' + +const MONTH_RECOMMENDATIONS = [1, 2, 3, 6, 12, 18, 24] + +export default function MonthSelector(props: { + max: number + topupPeriod: number + setTopupPeriod: any + setErrorMsg: (errorMsg: string | undefined) => void + className?: string +}) { + const [monthRecommendations, setMonthRecommendations] = useState(MONTH_RECOMMENDATIONS) + const [openCustom, setOpenCustom] = useState(false) + const [customPeriod, setCustomPeriod] = useState() + const [textPeriod, setTextPeriod] = useState() + + if (!monthRecommendations.includes(props.max) && props.max > 0) { + setMonthRecommendations([...monthRecommendations, props.max]) + } + + if (props.max <= 0) { + return

No topup periods available

+ } + + return ( +
+ {monthRecommendations + .filter((x) => x <= props.max) + .map((month: any, i: number) => ( + + ))} + {openCustom ? ( +
+
+ ) => { + if ( + parseFloat(event.target.value) < 0 || + !Number.isInteger(Number(event.target.value)) + ) { + setTextPeriod('') + return + } + setTextPeriod(event.target.value) + }} + className={cls(cmn.mri10, 'monthInput')} + placeholder="0" + /> + +
+ +
+ ) : ( + + )} +
+ ) +} diff --git a/src/components/MoreMenu/MoreMenu.tsx b/src/components/MoreMenu.tsx similarity index 97% rename from src/components/MoreMenu/MoreMenu.tsx rename to src/components/MoreMenu.tsx index 2c459cf..cf7c1cf 100644 --- a/src/components/MoreMenu/MoreMenu.tsx +++ b/src/components/MoreMenu.tsx @@ -38,8 +38,8 @@ import InventoryOutlinedIcon from '@mui/icons-material/InventoryOutlined' import { cls, styles, cmn } from '@skalenetwork/metaport' -import { DISCORD_INVITE_URL } from '../../core/constants' -import discordLogo from '../../assets/discord-mark-white.svg' +import { DISCORD_INVITE_URL } from '../core/constants' +import discordLogo from '../assets/discord-mark-white.svg' export default function MoreMenu() { const [anchorEl, setAnchorEl] = useState(null) @@ -60,7 +60,7 @@ export default function MoreMenu() { aria-haspopup="true" aria-expanded={open ? 'true' : undefined} onClick={handleClick} - className={cls(styles.paperGrey, cmn.pPrim, cmn.mleft10)} + className={cls(styles.paperGrey, cmn.pPrim, cmn.mleft5)} style={{ width: '34px', height: '34px' }} > diff --git a/src/components/MoreMenu/index.ts b/src/components/MoreMenu/index.ts deleted file mode 100644 index 025a075..0000000 --- a/src/components/MoreMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './MoreMenu' diff --git a/src/components/NetworkSwitch.tsx b/src/components/NetworkSwitch.tsx index 68fdb1c..109544c 100644 --- a/src/components/NetworkSwitch.tsx +++ b/src/components/NetworkSwitch.tsx @@ -48,8 +48,8 @@ export default function NetworkSwitch(props: { mpc: MetaportCore }) { return (
- -
-
- + + +

{chainAlias}

+

{chainDescription}

- {props.chainMeta?.url ? ( - -
- + +
+
+ {props.chainMeta?.url ? ( + + + + ) : null} +
+ + } /> -
+ +
diff --git a/src/components/Admin.tsx b/src/components/SkStack.tsx similarity index 51% rename from src/components/Admin.tsx rename to src/components/SkStack.tsx index 45f9364..5ff9d0c 100644 --- a/src/components/Admin.tsx +++ b/src/components/SkStack.tsx @@ -17,33 +17,27 @@ */ /** - * @file Admin.tsx + * @file SkStack.tsx * @copyright SKALE Labs 2023-Present */ -import { useEffect } from 'react' -import Container from '@mui/material/Container' +import { ReactElement } from 'react' import Stack from '@mui/material/Stack' -import { useParams } from 'react-router-dom' - -import { cmn, cls, type MetaportCore, getChainAlias } from '@skalenetwork/metaport' - -export default function Admin(props: { mpc: MetaportCore }) { - let { name } = useParams() - name = name ?? '' - const alias = getChainAlias(props.mpc.config.skaleNetwork, name) - - useEffect(() => {}, []) +export default function SkStack(props: { + className?: string + children?: ReactElement | ReactElement[] +}) { return ( - - -
-

Manage {alias}

-
-

Manage your SKALE Chain

-
-
-
+ + {props.children} + ) } diff --git a/src/components/Terms/index.ts b/src/components/Terms/index.ts deleted file mode 100644 index 99068c1..0000000 --- a/src/components/Terms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Terms' diff --git a/src/components/TermsModal.tsx b/src/components/TermsModal.tsx index e7380dd..e018465 100644 --- a/src/components/TermsModal.tsx +++ b/src/components/TermsModal.tsx @@ -21,41 +21,28 @@ * @copyright SKALE Labs 2022-Present */ -import React, { type Dispatch, type SetStateAction } from 'react' -import { useLocation } from 'react-router-dom' +import { useState, type Dispatch, type SetStateAction } from 'react' -import Modal from '@mui/material/Modal' import Container from '@mui/material/Container' import Button from '@mui/material/Button' import Link from '@mui/material/Link' import Grid from '@mui/material/Grid' import Box from '@mui/material/Box' -import DesktopMacRoundedIcon from '@mui/icons-material/DesktopMacRounded' import KeyRoundedIcon from '@mui/icons-material/KeyRounded' import LockRoundedIcon from '@mui/icons-material/LockRounded' import GradingRoundedIcon from '@mui/icons-material/GradingRounded' import { type MetaportCore, SkPaper, cls, cmn, styles } from '@skalenetwork/metaport' -import { BRIDGE_PAGES, PORTAL_URLS } from '../core/constants' -import TermsOfService from './Terms/terms-of-service.mdx' -import logo from '../assets/skale_lg.svg' - -const style = { - width: '100vw', - height: '100vh', - outline: 'none', - backdropFilter: 'blur(10px)', - WebkitBackdropFilter: 'blur(10px)' -} +import { PORTAL_URLS } from '../core/constants' +import TermsOfService from '../data/terms-of-service.mdx' export default function TermsModal(props: { mpc: MetaportCore termsAccepted: boolean setTermsAccepted: Dispatch> }) { - const location = useLocation() - const [scrolled, setScrolled] = React.useState(false) + const [scrolled, setScrolled] = useState(false) const portalUrl = PORTAL_URLS[props.mpc.config.skaleNetwork] ?? PORTAL_URLS.mainnet @@ -64,97 +51,80 @@ export default function TermsModal(props: { return 'Agree to terms' } - function isBridgePage(): boolean { - return ( - BRIDGE_PAGES.some( - (pathname) => location.pathname === pathname || location.pathname.includes(pathname) - ) || location.pathname === '/' - ) - } - function handleTermsScroll(e: any): void { const diff = e.target.scrollHeight - e.target.scrollTop - e.target.clientHeight const bottom = Math.abs(diff) < 15 setScrolled(bottom) } - if (props.termsAccepted || !isBridgePage()) return null + if (props.termsAccepted) return null return ( - -
- - - logo - - - - -
- -

- This website is for Desktop Use Only -

-
-
-
- - -
- -

- SKALE will NEVER ask you for your seed phrase or private keys -

-
-
-
- - -
- -

- Make sure you are connected to the correct bridge and only use this official - link: -
- - {portalUrl} - -

-
-
-
-
-
- -
- -

- Before you use the SKALE Bridge, you must review the terms of service carefully - and confirm below. -

-
-
- -
+
+ +
+

SKALE Bridge Terms of Use

+
+

+ Review the terms of service carefully and confirm +

+ + + + +
+ +

+ SKALE will NEVER ask you for your seed phrase or private keys +

+
+
+
+ + +
+ +

+ Make sure you are connected to the correct bridge and only use this official + link: + + {portalUrl} + +

+
+
+
+
+ +
+ +

+ Before you use the SKALE Bridge, you must review the terms of service carefully and + confirm below. +

+
+
+
- - - - -
- +
+
+ + + +
) } diff --git a/src/components/Tile.tsx b/src/components/Tile.tsx new file mode 100644 index 0000000..947cc75 --- /dev/null +++ b/src/components/Tile.tsx @@ -0,0 +1,94 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file LinkSurface.tsx + * @copyright SKALE Labs 2023-Present + */ + +import { ReactElement } from 'react' +import { useTheme } from '@mui/material/styles' +import LinearProgress from '@mui/material/LinearProgress' +import { cmn, cls, styles } from '@skalenetwork/metaport' + +import { DueDateStatus } from '../core/paymaster' + +export default function Tile(props: { + text?: string + value?: string + textRi?: string + icon?: ReactElement + className?: string + grow?: boolean + color?: DueDateStatus + progressColor?: DueDateStatus + progress?: number + children?: ReactElement | ReactElement[] +}) { + const theme = useTheme() + const color = props.color ? theme.palette[props.color].main : 'rgba(0, 0, 0, 0.6)' + return ( +
+ {props.text ? ( +
+ {props.icon ? ( +
{props.icon}
+ ) : null} +

{props.text}

+

{props.textRi}

+
+ ) : null} +
+ {props.value ? ( +

+ {props.value} +

+ ) : null} + {props.progress ? ( + + ) : null} +
+ {props.children} +
+ ) +} diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx new file mode 100644 index 0000000..6a5b0f8 --- /dev/null +++ b/src/components/Toggle.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react' +import ToggleButton from '@mui/material/ToggleButton' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' + +export default function ToggleButtons() { + const [alignment, setAlignment] = useState('left') + + const handleAlignment = (_: React.MouseEvent, newAlignment: string | null) => { + setAlignment(newAlignment) + } + + return ( + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + 8 + + + 9 + + + 10 + + + 11 + + + 12 + + + ) +} diff --git a/src/components/TokenSurface.tsx b/src/components/TokenSurface.tsx new file mode 100644 index 0000000..e3e888c --- /dev/null +++ b/src/components/TokenSurface.tsx @@ -0,0 +1,107 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file TokenSurface.tsx + * @copyright SKALE Labs 2021-Present + */ + +import { useState, useEffect } from 'react' + +import { CopyToClipboard } from 'react-copy-to-clipboard' +import Tooltip from '@mui/material/Tooltip' +import ButtonBase from '@mui/material/ButtonBase' +import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded' +import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded' +import { cmn, cls, styles, TokenIcon, ChainIcon, interfaces } from '@skalenetwork/metaport' + +import { DEFAULT_ERC20_DECIMALS } from '../core/constants' + +export default function TokenSurface(props: { + title: string + value: string | null | undefined + className?: string + tokenMetadata?: interfaces.TokenMetadata + chainName?: string + skaleNetwork?: interfaces.SkaleNetwork +}) { + const [copied, setCopied] = useState(false) + + const handleClick = () => { + setCopied(true) + } + + useEffect(() => { + if (copied) { + const timer = setTimeout(() => { + setCopied(false) + }, 1000) + return () => clearTimeout(timer) + } + }, [copied]) + + if (!props.value) return + return ( +
+ + + +
+
+ {props.tokenMetadata ? ( +
+ +
+ ) : null} + + {props.chainName && props.skaleNetwork ? ( +
+ +
+ ) : null} +

+ {props.title} + {props.tokenMetadata + ? ` (${props.tokenMetadata.decimals ?? DEFAULT_ERC20_DECIMALS})` + : null} +

+
+

{props.value}

+
+ {copied ? ( + + ) : ( + + )} +
+
+
+
+ ) +} diff --git a/src/components/Topup.tsx b/src/components/Topup.tsx new file mode 100644 index 0000000..5ada203 --- /dev/null +++ b/src/components/Topup.tsx @@ -0,0 +1,151 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Topup.tsx + * @copyright SKALE Labs 2023-Present + */ + +import { Link } from 'react-router-dom' +import Button from '@mui/material/Button' + +import { Collapse } from '@mui/material' +import TollIcon from '@mui/icons-material/Toll' +import MoreTimeIcon from '@mui/icons-material/MoreTime' +import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded' + +import { cmn, cls, type MetaportCore, fromWei, toWei } from '@skalenetwork/metaport' + +import Tile from './Tile' +import SkStack from './SkStack' +import MonthSelector from './MonthSelector' +import Loader from './Loader' + +import { PaymasterInfo, divideBigInts, truncateDecimals } from '../core/paymaster' +import { DEFAULT_ERC20_DECIMALS } from '../core/constants' +import { formatTimePeriod, monthsBetweenNowAndTimestamp } from '../core/timeHelper' + +export default function Topup(props: { + mpc: MetaportCore + name: string + topupPeriod: number + setTopupPeriod: any + info: PaymasterInfo + tokenBalance: bigint | undefined + topupChain: () => Promise + btnText: string | undefined + errorMsg: string | undefined + setErrorMsg: (errorMsg: string | undefined) => void + loading: boolean +}) { + if (props.tokenBalance === undefined) return + + const chainPriceSkl = divideBigInts(props.info.schainPricePerMonth, props.info.oneSklPrice) + const totalPriceSkl = chainPriceSkl * props.topupPeriod + const totalPriceWei = toWei(totalPriceSkl.toString(), DEFAULT_ERC20_DECIMALS) + + const tokenBalanceSkl = fromWei(props.tokenBalance, DEFAULT_ERC20_DECIMALS) + + const topupPeriodText = formatTimePeriod(props.topupPeriod, 'month') + const helperText = `${truncateDecimals(chainPriceSkl.toString(), 6)} SKL x ${topupPeriodText}` + + const balanceOk = props.tokenBalance >= totalPriceWei + const topupBtnText = balanceOk ? 'Top-up chain' : 'Insufficient funds' + + const untilDueDateMonths = monthsBetweenNowAndTimestamp( + props.info.schain.paidUntil, + props.info.effectiveTimestamp + ) + const maxTopupPeriod = Number(props.info.maxReplenishmentPeriod) - untilDueDateMonths + + return ( +
+ + } + children={ + + } + grow + /> + + + } + grow + /> + } + color={balanceOk ? undefined : 'error'} + /> + + + + } + color="error" + grow + children={ + + } + /> + + +
+
+ + {!balanceOk ? ( + + + + ) : null} +
+
+
+ ) +} diff --git a/src/core/constants.ts b/src/core/constants.ts index 45b55ca..83c5fec 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -21,14 +21,14 @@ * @copyright SKALE Labs 2022-Present */ -import FAQ from '../faq.json' +import FAQ from '../data/faq.json' export const MAINNET_CHAIN_NAME = 'mainnet' export const DASHBOARD_URL = 'https://app.geckoboard.com/v5/dashboards/LISYTRBEVGCVGL57/inception' export const DUNE_SKALE_URL = 'https://dune.com/projects/SKALE' -export const BRIDGE_PAGES = ['/transfer', '/bridge/history', '/portfolio', '/other/faq'] +export const BRIDGE_PAGES = ['/bridge', '/transfer', '/bridge/history', '/portfolio', '/other/faq'] export const DEFAULT_ERC20_DECIMALS = '18' @@ -42,3 +42,9 @@ import * as MAINNET_CHAIN_LOGOS from '../meta/logos' export { FAQ, MAINNET_CHAIN_LOGOS } export const DISCORD_INVITE_URL = 'https://discord.com/invite/gM5XBy6' + +export const MS_MULTIPLIER = 1000 +export const AVG_MONTH_LENGTH = 30.436875 + +const _DEFAULT_UPDATE_INTERVAL_SECONDS = 10 +export const DEFAULT_UPDATE_INTERVAL_MS = _DEFAULT_UPDATE_INTERVAL_SECONDS * MS_MULTIPLIER diff --git a/src/core/paymaster.ts b/src/core/paymaster.ts new file mode 100644 index 0000000..c2b22f8 --- /dev/null +++ b/src/core/paymaster.ts @@ -0,0 +1,120 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file constants.ts + * @copyright SKALE Labs 2022-Present + */ + +import { Contract, id, InterfaceAbi } from 'ethers' +import { MetaportCore, interfaces } from '@skalenetwork/metaport' +import PAYMASTER_INFO from '../data/paymaster' +import { getCurrentTsBigInt } from './timeHelper' + +export interface PaymasterInfo { + maxReplenishmentPeriod: bigint + oneSklPrice: bigint + schainPricePerMonth: bigint + skaleToken: string + schain: { + name: string + paidUntil: bigint + } + effectiveTimestamp?: bigint +} + +export type DueDateStatus = 'primary' | 'warning' | 'error' | 'success' + +export const DEFAULT_PAYMASTER_INFO: PaymasterInfo = { + maxReplenishmentPeriod: 0n, + oneSklPrice: 0n, + schainPricePerMonth: 0n, + skaleToken: '', + schain: { + name: '', + paidUntil: 0n + } +} + +export function divideBigInts(a: bigint, b: bigint): number { + return Number((a * 10000n) / b) / 10000 +} + +export function getPaymasterChain(skaleNetwork: interfaces.SkaleNetwork): string { + return PAYMASTER_INFO.networks[skaleNetwork].chain +} + +export function getPaymasterAddress(skaleNetwork: interfaces.SkaleNetwork): string { + return PAYMASTER_INFO.networks[skaleNetwork].address +} + +export function getPaymasterLaunchTs(skaleNetwork: interfaces.SkaleNetwork): bigint { + return BigInt(PAYMASTER_INFO.networks[skaleNetwork].launchTs) +} + +export function getPaymasterAbi(): InterfaceAbi { + return PAYMASTER_INFO.abi +} + +export function initPaymaster(mpc: MetaportCore): Contract { + const network = mpc.config.skaleNetwork + const paymasterAddress = getPaymasterAddress(network) + const paymasterChain = getPaymasterChain(network) + const provider = mpc.provider(paymasterChain) + return new Contract(paymasterAddress, getPaymasterAbi(), provider) +} + +export function pricingLaunchTsReached(skaleNetwork: interfaces.SkaleNetwork): boolean { + return getPaymasterLaunchTs(skaleNetwork) < getCurrentTsBigInt() +} + +export async function getPaymasterInfo( + paymaster: Contract, + targetChainName: string, + skaleNetwork: interfaces.SkaleNetwork +): Promise { + const rawData = await Promise.all([ + paymaster.maxReplenishmentPeriod(), + paymaster.oneSklPrice(), + paymaster.schainPricePerMonth(), + paymaster.skaleToken(), + paymaster.schains(id(targetChainName)) + ]) + + let effectiveTimestamp + if (skaleNetwork === 'legacy') { + effectiveTimestamp = await paymaster.effectiveTimestamp() + } + + return { + maxReplenishmentPeriod: rawData[0], + oneSklPrice: rawData[1], + schainPricePerMonth: rawData[2], + skaleToken: rawData[3], + schain: { + name: rawData[4][1], + paidUntil: rawData[4][2] + }, + effectiveTimestamp + } +} + +export function truncateDecimals(input: string, numDecimals: number): string { + const delimiter = input.includes(',') ? ',' : '.' + const [integerPart, decimalPart = ''] = input.split(delimiter) + return `${integerPart}${delimiter}${decimalPart.slice(0, numDecimals)}` +} diff --git a/src/core/timeHelper.ts b/src/core/timeHelper.ts new file mode 100644 index 0000000..57cf6b2 --- /dev/null +++ b/src/core/timeHelper.ts @@ -0,0 +1,93 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file timeHelper.ts + * @copyright SKALE Labs 2022-Present + */ + +import { AVG_MONTH_LENGTH } from './constants' + +export function formatBigIntTimestampSeconds(timestamp: bigint): string { + const date = new Date(Number(timestamp * 1000n)) + const day = date.getUTCDate().toString().padStart(2, '0') + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0') + const year = date.getUTCFullYear() + return `${day}.${month}.${year}` +} + +export function daysBetweenTimestamps(from: bigint, to: bigint): number { + const diffInSec = Number(from - to) + const diffInDays = diffInSec / (60 * 60 * 24) + return Math.round(diffInDays) * -1 +} + +export function monthsBetweenTimestamps(from: bigint, to: bigint): number { + const diffInDays = daysBetweenTimestamps(from, to) + return Math.round(diffInDays / AVG_MONTH_LENGTH) +} + +export function getCurrentTsBigInt(customCurrentTs?: bigint): bigint { + if (customCurrentTs !== undefined && customCurrentTs !== null) return customCurrentTs + return BigInt(Math.round(new Date().getTime() / 1000)) +} + +export function daysBetweenNowAndTimestamp(timestamp: bigint, customCurrentTs?: bigint): number { + return daysBetweenTimestamps(getCurrentTsBigInt(customCurrentTs), timestamp) +} + +export function monthsBetweenNowAndTimestamp(timestamp: bigint, customCurrentTs?: bigint): number { + return monthsBetweenTimestamps(getCurrentTsBigInt(customCurrentTs), timestamp) +} + +export function calculateElapsedPercentage( + tsInSeconds: bigint, + maxReplenishmentPeriod: bigint, + customCurrentTs?: bigint +): number { + const tsDate = new Date(Number(tsInSeconds * 1000n)) + const futureTime = new Date() + futureTime.setMonth(futureTime.getMonth() + Number(maxReplenishmentPeriod)) + + const currentTime = new Date() + const currentTimeMs = customCurrentTs ? Number(customCurrentTs) * 1000 : currentTime.getTime() + + const tsTimeMs = tsDate.getTime() + const futureTimeMs = futureTime.getTime() + + const totalDuration = futureTimeMs - currentTimeMs + const elapsedDuration = tsTimeMs - currentTimeMs + + let percentage = (elapsedDuration / totalDuration) * 100 + percentage = Math.max(0, Math.min(percentage, 100)) + + return percentage +} + +export function formatTimePeriod(count: number | bigint, type: 'day' | 'month'): string { + count = Number(count) + if (count === 0) { + return `>1 ${type}` + } + if (type === 'day') { + return Math.abs(count) === 1 ? `${count} day` : `${count} days` + } else if (type === 'month') { + return Math.abs(count) === 1 ? `${count} month` : `${count} months` + } else { + throw new Error('Invalid type provided. Type must be "day" or "month".') + } +} diff --git a/src/faq.json b/src/data/faq.json similarity index 100% rename from src/faq.json rename to src/data/faq.json diff --git a/src/data/paymaster.ts b/src/data/paymaster.ts new file mode 100644 index 0000000..a4e2c38 --- /dev/null +++ b/src/data/paymaster.ts @@ -0,0 +1,925 @@ +export default { + networks: { + mainnet: { + chain: 'elated-tan-skat', + address: '0x', + launchTs: '0' + }, + staging: { + chain: 'staging-legal-crazy-castor', + address: '0x9E444978d11E7e753017ce3329B01663D5D78240', + launchTs: '0' + }, + legacy: { + chain: 'international-villainous-zaurak', + address: '0x1f55e3Bce4B973dcC8540188f8F2038DC89891E6', + launchTs: '0' + }, + regression: { + chain: '', + address: '0x', + launchTs: '0' + } + }, + abi: [ + { + inputs: [ + { + internalType: 'address', + name: 'authority', + type: 'address' + } + ], + name: 'AccessManagedInvalidAuthority', + type: 'error' + }, + { + inputs: [ + { + internalType: 'address', + name: 'caller', + type: 'address' + }, + { + internalType: 'uint32', + name: 'delay', + type: 'uint32' + } + ], + name: 'AccessManagedRequiredDelay', + type: 'error' + }, + { + inputs: [ + { + internalType: 'address', + name: 'caller', + type: 'address' + } + ], + name: 'AccessManagedUnauthorized', + type: 'error' + }, + { + inputs: [], + name: 'AccessToEmptyHeap', + type: 'error' + }, + { + inputs: [], + name: 'AccessToEmptyPriorityQueue', + type: 'error' + }, + { + inputs: [], + name: 'CannotAddToThePast', + type: 'error' + }, + { + inputs: [], + name: 'CannotSetValueInThePast', + type: 'error' + }, + { + inputs: [], + name: 'ClearUnprocessed', + type: 'error' + }, + { + inputs: [], + name: 'ImportantDataRemoving', + type: 'error' + }, + { + inputs: [], + name: 'IncorrectTimeInterval', + type: 'error' + }, + { + inputs: [], + name: 'InvalidInitialization', + type: 'error' + }, + { + inputs: [], + name: 'NotInitializing', + type: 'error' + }, + { + inputs: [], + name: 'QueueEmpty', + type: 'error' + }, + { + inputs: [], + name: 'QueueFull', + type: 'error' + }, + { + inputs: [], + name: 'QueueOutOfBounds', + type: 'error' + }, + { + inputs: [], + name: 'ReplenishmentPeriodIsTooBig', + type: 'error' + }, + { + inputs: [], + name: 'RootDoesNotHaveParent', + type: 'error' + }, + { + inputs: [ + { + internalType: 'SchainHash', + name: 'hash', + type: 'bytes32' + } + ], + name: 'SchainAddingError', + type: 'error' + }, + { + inputs: [ + { + internalType: 'SchainHash', + name: 'hash', + type: 'bytes32' + } + ], + name: 'SchainDeletionError', + type: 'error' + }, + { + inputs: [ + { + internalType: 'SchainHash', + name: 'hash', + type: 'bytes32' + } + ], + name: 'SchainNotFound', + type: 'error' + }, + { + inputs: [], + name: 'SchainPriceIsNotSet', + type: 'error' + }, + { + inputs: [], + name: 'SkaleTokenIsNotSet', + type: 'error' + }, + { + inputs: [], + name: 'SklPriceIsNotSet', + type: 'error' + }, + { + inputs: [], + name: 'SklPriceIsOutdated', + type: 'error' + }, + { + inputs: [], + name: 'TimeIntervalIsAlreadyProcessed', + type: 'error' + }, + { + inputs: [], + name: 'TimeIntervalIsNotProcessed', + type: 'error' + }, + { + inputs: [], + name: 'TimestampIsOutOfValues', + type: 'error' + }, + { + inputs: [ + { + internalType: 'address', + name: 'spender', + type: 'address' + }, + { + internalType: 'uint256', + name: 'required', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'allowed', + type: 'uint256' + } + ], + name: 'TooSmallAllowance', + type: 'error' + }, + { + inputs: [], + name: 'TransferFailure', + type: 'error' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'id', + type: 'uint256' + } + ], + name: 'ValidatorAddingError', + type: 'error' + }, + { + inputs: [ + { + internalType: 'address', + name: 'validatorAddress', + type: 'address' + } + ], + name: 'ValidatorAddressAlreadyExists', + type: 'error' + }, + { + inputs: [ + { + internalType: 'address', + name: 'validatorAddress', + type: 'address' + } + ], + name: 'ValidatorAddressNotFound', + type: 'error' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'id', + type: 'uint256' + } + ], + name: 'ValidatorDeletionError', + type: 'error' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'id', + type: 'uint256' + }, + { + internalType: 'Timestamp', + name: 'when', + type: 'uint256' + } + ], + name: 'ValidatorHasBeenRemoved', + type: 'error' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'id', + type: 'uint256' + } + ], + name: 'ValidatorNotFound', + type: 'error' + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'authority', + type: 'address' + } + ], + name: 'AuthorityUpdated', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint64', + name: 'version', + type: 'uint64' + } + ], + name: 'Initialized', + type: 'event' + }, + { + inputs: [ + { + internalType: 'string', + name: 'name', + type: 'string' + } + ], + name: 'addSchain', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'id', + type: 'uint256' + }, + { + internalType: 'address', + name: 'validatorAddress', + type: 'address' + } + ], + name: 'addValidator', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [], + name: 'allowedSklPriceLag', + outputs: [ + { + internalType: 'Seconds', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'authority', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'to', + type: 'address' + } + ], + name: 'claim', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'validatorId', + type: 'uint256' + }, + { + internalType: 'address', + name: 'to', + type: 'address' + } + ], + name: 'claimFor', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'Timestamp', + name: 'before', + type: 'uint256' + } + ], + name: 'clearHistory', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'Paymaster.DebtId', + name: '', + type: 'uint256' + } + ], + name: 'debts', + outputs: [ + { + internalType: 'Timestamp', + name: 'from', + type: 'uint256' + }, + { + internalType: 'Timestamp', + name: 'to', + type: 'uint256' + }, + { + internalType: 'SKL', + name: 'amount', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'debtsBegin', + outputs: [ + { + internalType: 'Paymaster.DebtId', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'debtsEnd', + outputs: [ + { + internalType: 'Paymaster.DebtId', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'validatorId', + type: 'uint256' + } + ], + name: 'getActiveNodesNumber', + outputs: [ + { + internalType: 'uint256', + name: 'number', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'validatorId', + type: 'uint256' + } + ], + name: 'getNodesNumber', + outputs: [ + { + internalType: 'uint256', + name: 'number', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'getRewardAmount', + outputs: [ + { + internalType: 'SKL', + name: 'reward', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'validatorId', + type: 'uint256' + } + ], + name: 'getRewardAmountFor', + outputs: [ + { + internalType: 'SKL', + name: 'reward', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'SchainHash', + name: 'schainHash', + type: 'bytes32' + } + ], + name: 'getSchainExpirationTimestamp', + outputs: [ + { + internalType: 'Timestamp', + name: 'expiration', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'getSchainsNames', + outputs: [ + { + internalType: 'string[]', + name: 'names', + type: 'string[]' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'getSchainsNumber', + outputs: [ + { + internalType: 'uint256', + name: 'number', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'getValidatorsNumber', + outputs: [ + { + internalType: 'uint256', + name: 'number', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'initialAuthority', + type: 'address' + } + ], + name: 'initialize', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [], + name: 'isConsumingScheduledOp', + outputs: [ + { + internalType: 'bytes4', + name: '', + type: 'bytes4' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'maxReplenishmentPeriod', + outputs: [ + { + internalType: 'Months', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'oneSklPrice', + outputs: [ + { + internalType: 'USD', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'effectiveTimestamp', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'SchainHash', + name: 'schainHash', + type: 'bytes32' + }, + { + internalType: 'Months', + name: 'duration', + type: 'uint256' + } + ], + name: 'pay', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'SchainHash', + name: 'schainHash', + type: 'bytes32' + } + ], + name: 'removeSchain', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'id', + type: 'uint256' + } + ], + name: 'removeValidator', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [], + name: 'schainPricePerMonth', + outputs: [ + { + internalType: 'USD', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'SchainHash', + name: '', + type: 'bytes32' + } + ], + name: 'schains', + outputs: [ + { + internalType: 'SchainHash', + name: 'hash', + type: 'bytes32' + }, + { + internalType: 'string', + name: 'name', + type: 'string' + }, + { + internalType: 'Timestamp', + name: 'paidUntil', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'validatorId', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + name: 'setActiveNodes', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'Seconds', + name: 'lagSeconds', + type: 'uint256' + } + ], + name: 'setAllowedSklPriceLag', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'newAuthority', + type: 'address' + } + ], + name: 'setAuthority', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'Months', + name: 'months', + type: 'uint256' + } + ], + name: 'setMaxReplenishmentPeriod', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'validatorId', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + name: 'setNodesAmount', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'USD', + name: 'price', + type: 'uint256' + } + ], + name: 'setSchainPrice', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'contract IERC20', + name: 'token', + type: 'address' + } + ], + name: 'setSkaleToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'USD', + name: 'price', + type: 'uint256' + } + ], + name: 'setSklPrice', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'string', + name: 'newVersion', + type: 'string' + } + ], + name: 'setVersion', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [], + name: 'skaleToken', + outputs: [ + { + internalType: 'contract IERC20', + name: '', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'sklPriceTimestamp', + outputs: [ + { + internalType: 'Timestamp', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'version', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string' + } + ], + stateMutability: 'view', + type: 'function' + } + ] +} diff --git a/src/components/Terms/terms-of-service.mdx b/src/data/terms-of-service.mdx similarity index 100% rename from src/components/Terms/terms-of-service.mdx rename to src/data/terms-of-service.mdx diff --git a/src/metadata/.keep b/src/metadata/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx new file mode 100644 index 0000000..d263264 --- /dev/null +++ b/src/pages/Admin.tsx @@ -0,0 +1,83 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Admin.tsx + * @copyright SKALE Labs 2023-Present + */ + +import Container from '@mui/material/Container' +import { useParams } from 'react-router-dom' +import Button from '@mui/material/Button' + +import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' +import AdminPanelSettingsRoundedIcon from '@mui/icons-material/AdminPanelSettingsRounded' + +import { Link } from 'react-router-dom' +import { cmn, cls, type MetaportCore, getChainAlias, SkPaper } from '@skalenetwork/metaport' + +import Paymaster from '../components/Paymaster' + +export default function Admin(props: { mpc: MetaportCore }) { + let { name } = useParams() + name = name ?? '' + + const network = props.mpc.config.skaleNetwork + const alias = getChainAlias(network, name) + + return ( + + +
+
+
+ + + +
+

|

+
+ + + +
+

|

+
+
+ +

Manage

+
+
+
+
+
+

Manage {alias}

+

+ This is {alias} admin area - you can manage your chain here. +

+
+ +
+
+ ) +} diff --git a/src/components/Apps.tsx b/src/pages/Apps.tsx similarity index 100% rename from src/components/Apps.tsx rename to src/pages/Apps.tsx diff --git a/src/components/Bridge.tsx b/src/pages/Bridge.tsx similarity index 98% rename from src/components/Bridge.tsx rename to src/pages/Bridge.tsx index f4bf4ee..60425a8 100644 --- a/src/components/Bridge.tsx +++ b/src/pages/Bridge.tsx @@ -41,8 +41,8 @@ import { TransactionData } from '@skalenetwork/metaport' -import Message from './Message' -import BridgeBody from './BridgeBody' +import Message from '../components/Message' +import BridgeBody from '../components/BridgeBody' import { META_TAGS } from '../core/meta' diff --git a/src/components/Network.tsx b/src/pages/Chains.tsx similarity index 96% rename from src/components/Network.tsx rename to src/pages/Chains.tsx index 26b599a..74a0b66 100644 --- a/src/components/Network.tsx +++ b/src/pages/Chains.tsx @@ -28,8 +28,8 @@ import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import CircularProgress from '@mui/material/CircularProgress' -import CategorySection from './CategorySection' -import { getPrimaryCategory } from './CategoryBadge' +import CategorySection from '../components/CategorySection' +import { getPrimaryCategory } from '../components/CategoryBadge' import { cmn, cls, type MetaportCore, CHAINS_META, type interfaces } from '@skalenetwork/metaport' diff --git a/src/components/Faq/Faq.tsx b/src/pages/Faq.tsx similarity index 94% rename from src/components/Faq/Faq.tsx rename to src/pages/Faq.tsx index 42553e9..629ad18 100644 --- a/src/components/Faq/Faq.tsx +++ b/src/pages/Faq.tsx @@ -28,8 +28,8 @@ import Stack from '@mui/material/Stack' import { cmn, cls } from '@skalenetwork/metaport' -import FaqAccordion from '../FaqAccordion' -import { META_TAGS } from '../../core/meta' +import FaqAccordion from '../components/FaqAccordion' +import { META_TAGS } from '../core/meta' export default function Faq() { return ( diff --git a/src/components/History.tsx b/src/pages/History.tsx similarity index 100% rename from src/components/History.tsx rename to src/pages/History.tsx diff --git a/src/components/Portfolio.tsx b/src/pages/Portfolio.tsx similarity index 50% rename from src/components/Portfolio.tsx rename to src/pages/Portfolio.tsx index 2bfe329..fb3ffe4 100644 --- a/src/components/Portfolio.tsx +++ b/src/pages/Portfolio.tsx @@ -24,13 +24,14 @@ import { useState, useEffect } from 'react' import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' +import Grid from '@mui/material/Grid' import { cmn, cls, + styles, type MetaportCore, getChainAlias, - ChainIcon, TokenIcon, SkPaper, dataclasses, @@ -39,6 +40,9 @@ import { fromWei } from '@skalenetwork/metaport' +import TokenSurface from '../components/TokenSurface' +import ConnectWallet from '../components/ConnectWallet' + export default function Portfolio(props: { mpc: MetaportCore }) { const { address } = useWagmiAccount() @@ -86,6 +90,16 @@ export default function Portfolio(props: { mpc: MetaportCore }) { return tokenMetadata.decimals } + const isTokenInChain = (chain: string, token: string) => { + const connection = props.mpc.config.connections[chain] + if (!connection) return false + + const hasErc20Token = connection.erc20 && connection.erc20[token] + const hasEthToken = connection.eth && connection.eth[token] + + return hasErc20Token || hasEthToken + } + return ( @@ -94,65 +108,64 @@ export default function Portfolio(props: { mpc: MetaportCore }) {

Your assets across all SKALE Chains

+ {!address ? : null} {Object.keys(props.mpc.config.tokens)?.map((token: string, index: number) => ( -
-
- -
-

- {props.mpc.config.tokens[token].symbol} -

-

- {props.mpc.config.tokens[token].name} -

-
-
-

- {fromWei(getTotalBalance(token).toString(), getTokenDecimals(token))}{' '} - {props.mpc.config.tokens[token].symbol} -

-

On 2 chains

-
-
- +
- {props.mpc.config.chains.map((chain: string, index: number) => ( -
-
- -
-

- {getChainAlias(props.mpc.config.skaleNetwork, chain)} -

-
-
-

- {balances[index] && balances[index][token] - ? fromWei(balances[index][token].toString(), getTokenDecimals(token)) // todo: use correct decimals - : '0'}{' '} - {props.mpc.config.tokens[token].symbol} -

-
-
+
+ +
+

+ {props.mpc.config.tokens[token].symbol} +

+

+ {props.mpc.config.tokens[token].name ?? (token === 'eth' ? 'Ethereum' : '')} +

- ))} +
+

+ {fromWei(getTotalBalance(token).toString(), getTokenDecimals(token))}{' '} + {props.mpc.config.tokens[token].symbol} +

+

On 2 chains

+
+
+ + {props.mpc.config.chains + .filter((chain: string) => { + return isTokenInChain(chain, token) + }) + .map((chain: string, index: number) => ( + + + + ))} +
))} diff --git a/src/components/Schain.tsx b/src/pages/Schain.tsx similarity index 87% rename from src/components/Schain.tsx rename to src/pages/Schain.tsx index b38bd4f..1c1d576 100644 --- a/src/components/Schain.tsx +++ b/src/pages/Schain.tsx @@ -25,8 +25,7 @@ import { useEffect } from 'react' import { useParams } from 'react-router-dom' import Container from '@mui/material/Container' -import Stack from '@mui/material/Stack' -import SchainDetails from './SchainDetails' +import SchainDetails from '../components/SchainDetails' import CircularProgress from '@mui/material/CircularProgress' import { cmn, cls, type MetaportCore, CHAINS_META, type interfaces } from '@skalenetwork/metaport' @@ -58,7 +57,7 @@ export default function Schain(props: { loadSchains: any; schains: any[]; mpc: M return (
-
+
@@ -75,14 +74,7 @@ export default function Schain(props: { loadSchains: any; schains: any[]; mpc: M return ( - - - + ) } diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx new file mode 100644 index 0000000..f38afed --- /dev/null +++ b/src/pages/Start.tsx @@ -0,0 +1,68 @@ +import Container from '@mui/material/Container' +import Stack from '@mui/material/Stack' +import Box from '@mui/material/Box' +import Grid from '@mui/material/Grid' + +import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontalCircleOutlined' +import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' +import InsertChartOutlinedIcon from '@mui/icons-material/InsertChartOutlined' +// import AppsOutlinedIcon from '@mui/icons-material/AppsOutlined' +// import WalletOutlinedIcon from '@mui/icons-material/WalletOutlined' + +import PageCard from '../components/PageCard' + +import { cmn, cls } from '@skalenetwork/metaport' + +export default function Start() { + return ( + + +
+

SKALE Portal

+
+

+ Gateway to the SKALE Ecosystem +

+ + + + } + /> + + + } + /> + + {/* + } + /> + */} + + } + /> + + {/* + } + /> + */} + + +
+
+ ) +} diff --git a/src/components/Stats.tsx b/src/pages/Stats.tsx similarity index 100% rename from src/components/Stats.tsx rename to src/pages/Stats.tsx diff --git a/src/components/Terms/Terms.tsx b/src/pages/Terms.tsx similarity index 90% rename from src/components/Terms/Terms.tsx rename to src/pages/Terms.tsx index 08bb09e..4afcd12 100644 --- a/src/components/Terms/Terms.tsx +++ b/src/pages/Terms.tsx @@ -3,7 +3,7 @@ import Stack from '@mui/material/Stack' import { cmn, cls } from '@skalenetwork/metaport' -import TermsOfService from './terms-of-service.mdx' +import TermsOfService from '../data/terms-of-service.mdx' export default function Terms() { return ( diff --git a/tests/time.test.ts b/tests/time.test.ts new file mode 100644 index 0000000..c56aad8 --- /dev/null +++ b/tests/time.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from 'bun:test' +import { daysBetweenTimestamps, monthsBetweenTimestamps, formatTimePeriod } from '../src/core/timeHelper' + +const BASE_TS = 1710262826n + +describe('test timeHelper functions', () => { + test('daysBetweenNowAndTimestamp test', () => { + let days: number + days = daysBetweenTimestamps(BASE_TS, 1712937626n) // +31 days + expect(days).toBe(31) + days = daysBetweenTimestamps(BASE_TS, 1710273626n) // +1 hour + expect(days).toBe(0) + days = daysBetweenTimestamps(BASE_TS, 1710180026n) // -2 hours + expect(days).toBe(-1) + days = daysBetweenTimestamps(BASE_TS, 1710097226n) // -2 days + expect(days).toBe(-2) + }) + + test('daysBetweenNowAndTimestamp test', () => { + let months: number + months = monthsBetweenTimestamps(BASE_TS, 1712937626n) // +31 days + expect(months).toBe(1) + months = monthsBetweenTimestamps(BASE_TS, 1710273626n) // +1 hour + expect(months).toBe(0) + months = monthsBetweenTimestamps(BASE_TS, 1710097226n) // -2 days + expect(months).toBe(-0) + months = monthsBetweenTimestamps(BASE_TS, 1741719626n) // +1 year + expect(months).toBe(12) + months = monthsBetweenTimestamps(BASE_TS, 2025716426n) // +10 years + expect(months).toBe(120) + }) + + test('formatTimePeriod test', () => { + let text: string + text = formatTimePeriod(2, 'month') + expect(text).toBe('2 months') + text = formatTimePeriod(-1, 'month') + expect(text).toBe('-1 month') + text = formatTimePeriod(-0, 'month') + expect(text).toBe('>1 month') + text = formatTimePeriod(0, 'month') + expect(text).toBe('>1 month') + }) +})