From f025f2fb68df9d11e3c7046e8c471cd9859d914d Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 9 Nov 2023 17:38:06 +0000 Subject: [PATCH 01/31] portal#143 Add start page - wip --- src/App.scss | 13 ++++++ src/Router.tsx | 4 +- src/SkDrawer.tsx | 22 ++++++++-- src/components/ChainCategories.tsx | 6 +-- src/components/MoreMenu/MoreMenu.tsx | 12 ++--- src/components/PageCard.tsx | 65 ++++++++++++++++++++++++++++ src/components/Start.tsx | 60 +++++++++++++++++++++++++ src/components/TermsModal.tsx | 6 +-- src/core/constants.ts | 4 +- 9 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 src/components/PageCard.tsx create mode 100644 src/components/Start.tsx diff --git a/src/App.scss b/src/App.scss index 53c2a7c..5a2d5b9 100644 --- a/src/App.scss +++ b/src/App.scss @@ -235,6 +235,19 @@ body::-webkit-scrollbar { border: 1px rgb(44 44 44) solid !important; } +.pageCard { + height: 100% !important; + border-radius: 25px !important; + border: 1px rgb(44 44 44) solid !important; + + svg { + width: 24px; + height: 24px; + // margin-top: 60px; + color: white; + } +} + .br__tileLogo { height: 95px; diff --git a/src/Router.tsx b/src/Router.tsx index 81f122e..ebe013e 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -15,6 +15,7 @@ import App from './components/App' import History from './components/History' import Portfolio from './components/Portfolio' import Admin from './components/Admin' +import Start from './components/Start' import { getHistoryFromStorage, setHistoryToStorage } from './core/transferHistory' // import chainsJson from './chainsJson.json'; @@ -50,7 +51,8 @@ export default function Router() { return ( - } /> + } /> + } /> } /> diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index 83315a1..bb30b5c 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -21,9 +21,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 +44,27 @@ export default function SkDrawer() { > -

Bridge

+ + + + + + + + + +

Bridge

+ + + diff --git a/src/components/ChainCategories.tsx b/src/components/ChainCategories.tsx index 1a56071..eb2c1aa 100644 --- a/src/components/ChainCategories.tsx +++ b/src/components/ChainCategories.tsx @@ -22,10 +22,10 @@ */ import Button from '@mui/material/Button' -import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded'; +import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' import { cmn, cls } from '@skalenetwork/metaport' import CategoryBadge, { isString } from './CategoryBadge' -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom' export default function ChainCategories(props: { category: string | string[] | undefined @@ -35,7 +35,7 @@ export default function ChainCategories(props: { return (
- diff --git a/src/components/MoreMenu/MoreMenu.tsx b/src/components/MoreMenu/MoreMenu.tsx index 85ca719..2c459cf 100644 --- a/src/components/MoreMenu/MoreMenu.tsx +++ b/src/components/MoreMenu/MoreMenu.tsx @@ -41,7 +41,6 @@ import { cls, styles, cmn } from '@skalenetwork/metaport' import { DISCORD_INVITE_URL } from '../../core/constants' import discordLogo from '../../assets/discord-mark-white.svg' - export default function MoreMenu() { const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) @@ -136,18 +135,15 @@ export default function MoreMenu() {
- +
discord logo + style={{ width: '22px', height: '22px' }} + alt="discord logo" + />
Discord
diff --git a/src/components/PageCard.tsx b/src/components/PageCard.tsx new file mode 100644 index 0000000..ae9f908 --- /dev/null +++ b/src/components/PageCard.tsx @@ -0,0 +1,65 @@ +/** + * @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 PageCard.tsx + * @copyright SKALE Labs 2022-Present + */ + +import { Link } from 'react-router-dom' +import { cmn, cls } from '@skalenetwork/metaport' + +export default function PageCard(props: { name: string; icon: any; description: string }) { + return ( +
+
+
+ +
+
+
{props.icon}
+

+ {props.name} +

+
+

{props.description}

+
+ +
+
+
+ ) +} diff --git a/src/components/Start.tsx b/src/components/Start.tsx new file mode 100644 index 0000000..81b520c --- /dev/null +++ b/src/components/Start.tsx @@ -0,0 +1,60 @@ +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 PageCard from './PageCard' + +import { cmn, cls } from '@skalenetwork/metaport' + +export default function Start() { + return ( + + +
+

SKALE Portal

+
+

+ Gateway to the SKALE Ecosystem +

+ + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + +
+
+ ) +} diff --git a/src/components/TermsModal.tsx b/src/components/TermsModal.tsx index e7380dd..aebdfb5 100644 --- a/src/components/TermsModal.tsx +++ b/src/components/TermsModal.tsx @@ -65,10 +65,8 @@ export default function TermsModal(props: { } function isBridgePage(): boolean { - return ( - BRIDGE_PAGES.some( - (pathname) => location.pathname === pathname || location.pathname.includes(pathname) - ) || location.pathname === '/' + return BRIDGE_PAGES.some( + (pathname) => location.pathname === pathname || location.pathname.includes(pathname) ) } diff --git a/src/core/constants.ts b/src/core/constants.ts index 45bb0e7..60a1996 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -28,7 +28,7 @@ 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' @@ -41,4 +41,4 @@ import * as MAINNET_CHAIN_LOGOS from '../meta/logos' export { FAQ, MAINNET_CHAIN_LOGOS } -export const DISCORD_INVITE_URL = 'https://discord.com/invite/gM5XBy6' \ No newline at end of file +export const DISCORD_INVITE_URL = 'https://discord.com/invite/gM5XBy6' From 7c34752355f5ac3bdf17f0ffc0fcfcb42737c5fb Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 13 Nov 2023 13:47:20 +0000 Subject: [PATCH 02/31] Update Portfolio page --- config/mainnet.ts | 6 -- src/App.scss | 12 +-- src/SkDrawer.tsx | 12 +-- src/components/AccordionSection.tsx | 2 + src/components/Portfolio.tsx | 125 +++++++++++++++------------- src/components/TokenSurface.tsx | 107 ++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 74 deletions(-) create mode 100644 src/components/TokenSurface.tsx 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/src/App.scss b/src/App.scss index 5a2d5b9..3bad317 100644 --- a/src/App.scss +++ b/src/App.scss @@ -455,11 +455,7 @@ code { } } - .titleSection { - background: rgba(0, 0, 0, 0.6) !important; - border-radius: 25px; - padding: 20px; - } + .titleBadge { background: rgba(0, 0, 0, 0.6) !important; @@ -473,6 +469,12 @@ code { } } +.titleSection { + background: rgba(0, 0, 0, 0.6) !important; + border-radius: 25px; + padding: 20px; +} + button:focus { outline: none !important; } diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index bb30b5c..925d9d5 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -17,8 +17,8 @@ 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' -// import AppsOutlinedIcon from '@mui/icons-material/AppsOutlined' -// import WalletOutlinedIcon from '@mui/icons-material/WalletOutlined' +import AppsOutlinedIcon from '@mui/icons-material/AppsOutlined' +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' @@ -86,7 +86,7 @@ export default function SkDrawer() { - {/* + - */} + - {/* + - */} + ) : null}

{props.title}

+

{props.subtitle}

{props.expanded === props.panel ? ( ) : ( diff --git a/src/components/Portfolio.tsx b/src/components/Portfolio.tsx index 2bfe329..3965d26 100644 --- a/src/components/Portfolio.tsx +++ b/src/components/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,8 @@ import { fromWei } from '@skalenetwork/metaport' +import TokenSurface from './TokenSurface' + export default function Portfolio(props: { mpc: MetaportCore }) { const { address } = useWagmiAccount() @@ -86,6 +89,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 ( @@ -95,64 +108,62 @@ export default function Portfolio(props: { mpc: MetaportCore }) {

Your assets across all SKALE Chains

{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/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 ? ( + + ) : ( + + )} +
+
+
+
+ ) +} From 46ac8a2f8c484feb3d7b47f6967f901728c6a3f2 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 14 Nov 2023 19:46:27 +0000 Subject: [PATCH 03/31] Add Connect wallet button --- src/App.scss | 2 ++ src/components/AccountMenu.tsx | 51 +++++++++++++++++++++------- src/components/HelpZen/HelpZen.tsx | 18 ++++++---- src/components/MoreMenu/MoreMenu.tsx | 2 +- src/components/NetworkSwitch.tsx | 2 +- 5 files changed, 53 insertions(+), 22 deletions(-) diff --git a/src/App.scss b/src/App.scss index 4fcdfa9..e3a5b37 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; 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)} - - + + ) : ( + + + + )}
- + diff --git a/src/components/MoreMenu/MoreMenu.tsx b/src/components/MoreMenu/MoreMenu.tsx index 2c459cf..f759419 100644 --- a/src/components/MoreMenu/MoreMenu.tsx +++ b/src/components/MoreMenu/MoreMenu.tsx @@ -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/NetworkSwitch.tsx b/src/components/NetworkSwitch.tsx index 68fdb1c..52810b5 100644 --- a/src/components/NetworkSwitch.tsx +++ b/src/components/NetworkSwitch.tsx @@ -49,7 +49,7 @@ export default function NetworkSwitch(props: { mpc: MetaportCore }) {
- - -
- +
+
+ + + +
) } From 710488a862e343efc5c0b58c60d3276332bded12 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Fri, 24 Nov 2023 17:05:22 +0000 Subject: [PATCH 10/31] portal#143 Update css scrollbar --- src/App.scss | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/App.scss b/src/App.scss index d51343c..f1327e8 100644 --- a/src/App.scss +++ b/src/App.scss @@ -183,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 { From 46ddaa16ebb65b465e28707951224930d8a590c0 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 28 Nov 2023 11:45:10 +0000 Subject: [PATCH 11/31] Update chains page, add admin area --- src/App.scss | 24 ++++------ src/Router.tsx | 3 +- src/SkDrawer.tsx | 6 ++- src/components/AccordionLink.tsx | 55 +++++++++++++++++++++ src/components/Admin.tsx | 77 +++++++++++++++++++++++++----- src/components/CategoryBadge.tsx | 5 +- src/components/ChainAccordion.tsx | 7 +++ src/components/ChainCard.tsx | 14 ++++-- src/components/ChainCategories.tsx | 28 +++++++---- src/components/SchainDetails.tsx | 4 +- 10 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 src/components/AccordionLink.tsx diff --git a/src/App.scss b/src/App.scss index f1327e8..f3ca2be 100644 --- a/src/App.scss +++ b/src/App.scss @@ -236,14 +236,13 @@ body::-webkit-scrollbar { } .br__tile:hover { - box-shadow: 0px -4px 6px rgba(255, 255, 255, 0.1); transform: scale(1.05); } .br__tile { height: 140px; border-radius: 25px !important; - border: 1px rgb(44 44 44) solid !important; + border: 1px solid #171616 !important } .pageCard { @@ -348,21 +347,20 @@ body::-webkit-scrollbar { .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; + padding: 6px 10px 6px 12px !important; + font-size: 0.6rem !important; + min-width: 55px; 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; } } @@ -461,12 +459,10 @@ code { } } - - .titleBadge { background: rgba(0, 0, 0, 0.6) !important; border-radius: 25px; - padding: 10px 15px; + padding: 5px; svg { width: 12px; diff --git a/src/Router.tsx b/src/Router.tsx index 6c7f7b7..02d289c 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -2,8 +2,7 @@ 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' diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index d27c45f..9e6811b 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' @@ -118,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/Admin.tsx b/src/components/Admin.tsx index 45f9364..59a5bf5 100644 --- a/src/components/Admin.tsx +++ b/src/components/Admin.tsx @@ -21,29 +21,82 @@ * @copyright SKALE Labs 2023-Present */ -import { useEffect } from 'react' +import { useState } from 'react' import Container from '@mui/material/Container' -import Stack from '@mui/material/Stack' import { useParams } from 'react-router-dom' +import Button from '@mui/material/Button' -import { cmn, cls, type MetaportCore, getChainAlias } from '@skalenetwork/metaport' +import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' +import PaymentsRoundedIcon from '@mui/icons-material/PaymentsRounded' +import AdminPanelSettingsRoundedIcon from '@mui/icons-material/AdminPanelSettingsRounded' + +import { Link } from 'react-router-dom' +import { cmn, cls, type MetaportCore, getChainAlias, SkPaper } from '@skalenetwork/metaport' + +import AccordionSection from './AccordionSection' export default function Admin(props: { mpc: MetaportCore }) { let { name } = useParams() name = name ?? '' - const alias = getChainAlias(props.mpc.config.skaleNetwork, name) - useEffect(() => {}, []) + const [expanded, setExpanded] = useState('panel1') + + function handleChange(panel: string | false) { + setExpanded(expanded && panel === expanded ? false : panel) + } + + const network = props.mpc.config.skaleNetwork + const alias = getChainAlias(network, name) return ( - - -
-

Manage {alias}

+ + {/* */} + +
+
+
+ + + +
+

|

+
+ + + +
+

|

+
+
+ +

Admin Area

+
+
+
+
+ +
+

Manage {alias}

+

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

-

Manage your SKALE Chain

-
- + } + > +
!!!!
+
+
) } diff --git a/src/components/CategoryBadge.tsx b/src/components/CategoryBadge.tsx index c8f6f01..b9c35e6 100644 --- a/src/components/CategoryBadge.tsx +++ b/src/components/CategoryBadge.tsx @@ -66,7 +66,10 @@ export default function CategoryBadge(props: { category: string; className?: str } return ( -
+
{getCategoryIcon(props.category)}

{props.category}

diff --git a/src/components/ChainAccordion.tsx b/src/components/ChainAccordion.tsx index 037bd64..34eed96 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, @@ -188,6 +190,11 @@ export default function ChainAccordion(props: { explorerUrl={explorerUrl} /> + } + url={`/admin/${props.schainName}`} + /> ) } diff --git a/src/components/ChainCard.tsx b/src/components/ChainCard.tsx index 0811b1d..bc326eb 100644 --- a/src/components/ChainCard.tsx +++ b/src/components/ChainCard.tsx @@ -33,8 +33,8 @@ import { } from '@skalenetwork/metaport' import Button from '@mui/material/Button' -import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward' -import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded' +import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined' +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' import ChainLogo from './ChainLogo' import { getExplorerUrl } from '../core/chain' @@ -78,12 +78,16 @@ export default function ChainCard(props: { skaleNetwork: interfaces.SkaleNetwork >
- - - 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/SchainDetails.tsx b/src/components/SchainDetails.tsx index c42d301..37f6e94 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -105,7 +105,7 @@ export default function SchainDetails(props: { - +
@@ -117,7 +117,7 @@ export default function SchainDetails(props: {
-
+
- -
-
- -
- {props.chainMeta?.url ? ( -
- + + +
+

{chainAlias}

+

{chainDescription}

+
+ +
+ + - ) : null} +
+ +
+ {props.chainMeta?.url ? ( + + ) : null} +
-
- -
+ + +
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 44a26e2..50b545f 100644 --- a/src/components/TermsModal.tsx +++ b/src/components/TermsModal.tsx @@ -35,7 +35,7 @@ import GradingRoundedIcon from '@mui/icons-material/GradingRounded' import { type MetaportCore, SkPaper, cls, cmn, styles } from '@skalenetwork/metaport' import { PORTAL_URLS } from '../core/constants' -import TermsOfService from './Terms/terms-of-service.mdx' +import TermsOfService from '../terms-of-service.mdx' export default function TermsModal(props: { mpc: MetaportCore diff --git a/src/components/Tile.tsx b/src/components/Tile.tsx new file mode 100644 index 0000000..bf5281d --- /dev/null +++ b/src/components/Tile.tsx @@ -0,0 +1,87 @@ +/** + * @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' + +export default function Tile(props: { + text: string + value: string + textRi?: string + icon?: ReactElement + className?: string + grow?: boolean + color?: 'primary' | 'warning' | 'error' + 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.icon ? ( +
{props.icon}
+ ) : null} +

{props.text}

+

{props.textRi}

+
+
+

+ {props.value} +

+ {props.progress ? ( + + ) : null} +
+ {props.children} +
+ ) +} diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx new file mode 100644 index 0000000..4f40a55 --- /dev/null +++ b/src/components/Toggle.tsx @@ -0,0 +1,58 @@ +import * as React from 'react' +import ToggleButton from '@mui/material/ToggleButton' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' + +export default function ToggleButtons() { + const [alignment, setAlignment] = React.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/Topup.tsx b/src/components/Topup.tsx new file mode 100644 index 0000000..48b12e8 --- /dev/null +++ b/src/components/Topup.tsx @@ -0,0 +1,156 @@ +/** + * @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 TollIcon from '@mui/icons-material/Toll' +import AvTimerRoundedIcon from '@mui/icons-material/AvTimerRounded' + +import { + cmn, + cls, + type MetaportCore, + TokenIcon +} from '@skalenetwork/metaport' + +import Stack from '@mui/material/Stack' +import Tile from './Tile' + +export default function Topup(_: { mpc: MetaportCore; name: string }) { + // const network = props.mpc.config.skaleNetwork + // const alias = getChainAlias(network, props.name) + + return ( +
+

+ Payment info +

+ + } + /> + } + /> + } grow /> + + + } + /> + + + +

+ Top-up chain +

+ + } + grow + // children={} + /> + + + {/* +
+
+
+

+ Paid until +

+

+ Max topup period: 12 month +

+
+

+ 02.04.2024 +

+ +
+
+
+
+
+

+ Payment overdue +

+
+

+ 45 days +

+
+
+
*/} + + {/* +
+
+
+

+ Paid until +

+

+ Max top-up period: 12 month +

+
+

+ 02.04.2024 +

+ +
+
+
+
+
+

+ Until due date +

+
+

+ 125 days +

+
+
+
*/} +
+ ) +} diff --git a/src/components/Admin.tsx b/src/pages/Admin.tsx similarity index 69% rename from src/components/Admin.tsx rename to src/pages/Admin.tsx index 59a5bf5..2450da2 100644 --- a/src/components/Admin.tsx +++ b/src/pages/Admin.tsx @@ -21,7 +21,6 @@ * @copyright SKALE Labs 2023-Present */ -import { useState } from 'react' import Container from '@mui/material/Container' import { useParams } from 'react-router-dom' import Button from '@mui/material/Button' @@ -31,19 +30,19 @@ import PaymentsRoundedIcon from '@mui/icons-material/PaymentsRounded' import AdminPanelSettingsRoundedIcon from '@mui/icons-material/AdminPanelSettingsRounded' import { Link } from 'react-router-dom' -import { cmn, cls, type MetaportCore, getChainAlias, SkPaper } from '@skalenetwork/metaport' +import { cmn, cls, styles, type MetaportCore, getChainAlias, SkPaper } from '@skalenetwork/metaport' -import AccordionSection from './AccordionSection' +import Topup from '../components/Topup' export default function Admin(props: { mpc: MetaportCore }) { let { name } = useParams() name = name ?? '' - const [expanded, setExpanded] = useState('panel1') + // const [expanded, setExpanded] = useState('panel1') - function handleChange(panel: string | false) { - setExpanded(expanded && panel === expanded ? false : panel) - } + // function handleChange(panel: string | false) { + // setExpanded(expanded && panel === expanded ? false : panel) + // } const network = props.mpc.config.skaleNetwork const alias = getChainAlias(network, name) @@ -74,28 +73,45 @@ export default function Admin(props: { mpc: MetaportCore }) {
-

Admin Area

+

Manage

- -
-

Manage {alias}

+ +

Manage {alias}

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

+
+ +
+
+ +
+

Chain top-up

- + + {/* } > -
!!!!
-
+ +
*/} + {/* } + > +

Will be available soon

+
*/} ) 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/Chains.tsx b/src/pages/Chains.tsx similarity index 96% rename from src/components/Chains.tsx rename to src/pages/Chains.tsx index 26b599a..74a0b66 100644 --- a/src/components/Chains.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 99% rename from src/components/Portfolio.tsx rename to src/pages/Portfolio.tsx index 3965d26..31160c4 100644 --- a/src/components/Portfolio.tsx +++ b/src/pages/Portfolio.tsx @@ -40,7 +40,7 @@ import { fromWei } from '@skalenetwork/metaport' -import TokenSurface from './TokenSurface' +import TokenSurface from '../components/TokenSurface' export default function Portfolio(props: { mpc: MetaportCore }) { const { address } = useWagmiAccount() diff --git a/src/components/Start.tsx b/src/pages/Start.tsx similarity index 98% rename from src/components/Start.tsx rename to src/pages/Start.tsx index 6a989d2..2e78ec4 100644 --- a/src/components/Start.tsx +++ b/src/pages/Start.tsx @@ -9,7 +9,7 @@ import InsertChartOutlinedIcon from '@mui/icons-material/InsertChartOutlined' import AppsOutlinedIcon from '@mui/icons-material/AppsOutlined' import WalletOutlinedIcon from '@mui/icons-material/WalletOutlined' -import PageCard from './PageCard' +import PageCard from '../components/PageCard' import { cmn, cls } from '@skalenetwork/metaport' 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 91% rename from src/components/Terms/Terms.tsx rename to src/pages/Terms.tsx index 08bb09e..9dd82c2 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 '../terms-of-service.mdx' export default function Terms() { return ( diff --git a/src/components/Terms/terms-of-service.mdx b/src/terms-of-service.mdx similarity index 100% rename from src/components/Terms/terms-of-service.mdx rename to src/terms-of-service.mdx From e62d373c79c3679a17ec79920ed724b294e3b0c7 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 7 Dec 2023 15:58:24 +0000 Subject: [PATCH 13/31] portal#118 Add schain top page, update internal structure --- .gitignore | 2 +- build.sh | 2 +- bun.lockb | Bin 319760 -> 318745 bytes package.json | 4 +- src/App.scss | 22 +- src/App.tsx | 2 +- src/Router.tsx | 2 +- src/components/ConnectWallet.tsx | 60 +++ src/components/Loader.tsx | 40 ++ src/components/MonthSelector.tsx | 60 +++ src/components/Paymaster.tsx | 162 ++++++ src/components/PricingInfo.tsx | 105 ++++ src/components/SkStack.tsx | 42 ++ src/components/TermsModal.tsx | 2 +- src/components/Tile.tsx | 33 +- src/components/Topup.tsx | 210 ++++---- src/core/constants.ts | 5 +- src/core/paymaster.ts | 102 ++++ src/core/timeHelper.ts | 60 +++ src/{ => data}/faq.json | 0 src/data/paymaster.ts | 708 +++++++++++++++++++++++++++ src/{ => data}/terms-of-service.mdx | 0 src/metadata/.keep | 0 src/pages/Admin.tsx | 43 +- src/pages/Portfolio.tsx | 2 + src/{components => pages}/Schain.tsx | 4 +- src/pages/Terms.tsx | 2 +- 27 files changed, 1502 insertions(+), 172 deletions(-) create mode 100644 src/components/ConnectWallet.tsx create mode 100644 src/components/Loader.tsx create mode 100644 src/components/MonthSelector.tsx create mode 100644 src/components/Paymaster.tsx create mode 100644 src/components/PricingInfo.tsx create mode 100644 src/components/SkStack.tsx create mode 100644 src/core/paymaster.ts create mode 100644 src/core/timeHelper.ts rename src/{ => data}/faq.json (100%) create mode 100644 src/data/paymaster.ts rename src/{ => data}/terms-of-service.mdx (100%) delete mode 100644 src/metadata/.keep rename src/{components => pages}/Schain.tsx (95%) 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 a2d21f416c70cc8f7f23e6c9f84aa94d0c75f98b..1ed01d256530299d230b36ae09ba12aa4236a1d2 100755 GIT binary patch delta 22169 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 RSBFKV8bUJ%&M+*E{XZ)|FOmQN delta 23172 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 diff --git a/package.json b/package.json index c7f5f03..1bdb09a 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.0", "@types/react-copy-to-clipboard": "^5.0.4", "@vercel/analytics": "^1.0.2", "eslint-config-prettier": "^9.0.0", diff --git a/src/App.scss b/src/App.scss index 7ac456f..466653f 100644 --- a/src/App.scss +++ b/src/App.scss @@ -345,6 +345,26 @@ 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%, .1) !important; @@ -535,7 +555,7 @@ button:focus { .blackP { - color: #000000 + color: #000000; } .divider { 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/Router.tsx b/src/Router.tsx index 6f971e1..2e1f5e6 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -9,7 +9,7 @@ import Bridge from './pages/Bridge' import Faq from './pages/Faq' import Terms from './pages/Terms' import Network from './pages/Chains' -import Schain from './components/Schain' +import Schain from './pages/Schain' import Stats from './pages/Stats' import Apps from './pages/Apps' import App from './components/App' 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/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..071c644 --- /dev/null +++ b/src/components/MonthSelector.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 MonthSelector.tsx + * @copyright SKALE Labs 2023-Present + */ + +import Button from '@mui/material/Button' +import { cmn, cls } from '@skalenetwork/metaport' + +const MONTH_RECOMENDATIONS = [1, 2, 3, 6, 12, 18, 24] + +export default function MonthSelector(props: { + max: number + topupPeriod: number + setTopupPeriod: any + className?: string +}) { + return ( +
+ {MONTH_RECOMENDATIONS.filter((x) => x <= props.max).map((month: any, i: number) => ( + + ))} + +
+ ) +} diff --git a/src/components/Paymaster.tsx b/src/components/Paymaster.tsx new file mode 100644 index 0000000..62521af --- /dev/null +++ b/src/components/Paymaster.tsx @@ -0,0 +1,162 @@ +/** + * @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 Paymaster.tsx + * @copyright SKALE Labs 2023-Present + */ + +import { Contract, id } from 'ethers' +import { useState, useEffect } from 'react' + +import { + cmn, + cls, + type MetaportCore, + useWagmiAccount, + ERC_ABIS, + enforceNetwork, + useWagmiWalletClient, + useWagmiSwitchNetwork, + walletClientToSigner, + sendTransaction, + getChainAlias, + toWei +} from '@skalenetwork/metaport' + +import ConnectWallet from './ConnectWallet' +import PricingInfo from './PricingInfo' +import Topup from './Topup' +import Loader from './Loader' + +import { DEFAULT_UPDATE_INTERVAL_MS } from '../core/constants' +import { + initPaymaster, + getPaymasterChain, + getPaymasterInfo, + PaymasterInfo, + DEFAULT_PAYMASTER_INFO, + getPaymasterAddress, + getPaymasterAbi +} from '../core/paymaster' + +const DEFAULT_TOPUP_PERIOD = 3 + +export default function Paymaster(props: { mpc: MetaportCore; name: string }) { + const { address } = useWagmiAccount() + const paymaster = initPaymaster(props.mpc) + const network = props.mpc.config.skaleNetwork + const paymasterChain = getPaymasterChain(network) + + const [btnText, setBtnText] = useState() + const [errorMsg, setErrorMsg] = useState() + const [loading, setLoading] = useState(false) + + const [sklToken, setSklToken] = useState() + const [tokenBalance, setTokenBalance] = useState() + const [topupPeriod, setTopupPeriod] = useState(DEFAULT_TOPUP_PERIOD) + const [info, setInfo] = useState(DEFAULT_PAYMASTER_INFO) + + const { data: walletClient } = useWagmiWalletClient() + const { switchNetworkAsync } = useWagmiSwitchNetwork() + + useEffect(() => { + if (paymaster === undefined) return + loadPaymasterInfo() + const intervalId = setInterval(loadPaymasterInfo, DEFAULT_UPDATE_INTERVAL_MS) + return () => clearInterval(intervalId) + }, [sklToken, paymaster, address]) + + async function loadPaymasterInfo() { + if (paymaster === undefined) return + const info = await getPaymasterInfo(paymaster, props.name) + let skl = sklToken + if (skl === undefined) { + skl = new Contract(info.skaleToken, ERC_ABIS.erc20.abi, paymaster.runner) + setSklToken(skl) + } else { + setTokenBalance(await skl.balanceOf(address)) + } + setInfo(info) + } + + async function topupChain() { + if (!paymaster.runner?.provider || !walletClient || !switchNetworkAsync) { + setErrorMsg('Something is wrong with your wallet, try again') + return + } + setLoading(true) + setBtnText(`Switch network to ${getChainAlias(network, paymasterChain)}`) + setErrorMsg(undefined) + try { + const { chainId } = await paymaster.runner.provider.getNetwork() + await enforceNetwork(chainId, walletClient, switchNetworkAsync, network, paymasterChain) + setBtnText('Sending transaction...') + const signer = walletClientToSigner(walletClient) + const connectedPaymaster = new Contract( + getPaymasterAddress(network), + getPaymasterAbi(), + signer + ) + // const res = await sendTransaction(connectedPaymaster.pay, [id(props.name), topupPeriod]) + const res = await sendTransaction(connectedPaymaster.setSchainPrice, [toWei('3600', '18')]) + if (!res.status) { + setErrorMsg(res.err?.name) + return + } + await loadPaymasterInfo() + } catch (e: any) { + console.error(e) + setErrorMsg(e.toString()) + } finally { + setLoading(false) + setBtnText(undefined) + } + } + + if (info.oneSklPrice === 0n) return + + return ( +
+

+ Pricing info +

+ +

+ Top-up chain +

+ {!address ? ( + + ) : ( + + )} +
+ ) +} diff --git a/src/components/PricingInfo.tsx b/src/components/PricingInfo.tsx new file mode 100644 index 0000000..b18674e --- /dev/null +++ b/src/components/PricingInfo.tsx @@ -0,0 +1,105 @@ +/** + * @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 PricingInfo.tsx + * @copyright SKALE Labs 2023-Present + */ + +import TollIcon from '@mui/icons-material/Toll' +import AvTimerRoundedIcon from '@mui/icons-material/AvTimerRounded' + +import { cmn, TokenIcon, fromWei } from '@skalenetwork/metaport' + +import { truncateDecimals, PaymasterInfo, DueDateStatus, divideBigInts } from '../core/paymaster' +import { + daysBetweenNowAndTimestamp, + calculateElapsedPercentage, + formatBigIntTimestampSeconds +} from '../core/timeHelper' +import { DEFAULT_ERC20_DECIMALS } from '../core/constants' + +import SkStack from './SkStack' +import Tile from './Tile' + +export default function PricingInfo(props: { info: PaymasterInfo }) { + const sklPrice = fromWei(props.info.oneSklPrice, DEFAULT_ERC20_DECIMALS) + const chainPriceUsd = fromWei(props.info.schainPricePerMonth, DEFAULT_ERC20_DECIMALS) + const chainPriceSkl = divideBigInts(props.info.schainPricePerMonth, props.info.oneSklPrice) + + const untilDueDateDays = daysBetweenNowAndTimestamp(props.info.schain.paidUntil) + const dueDateStatus = getDueDateStatus(untilDueDateDays) + const dueDateText = untilDueDateDays < 0 ? 'Payment overdue' : 'Until due date' + + const elapsedPercentage = calculateElapsedPercentage( + props.info.schain.paidUntil, + props.info.maxReplenishmentPeriod + ) + + function getDueDateStatus(days: number): DueDateStatus { + if (days > 31) { + return 'success' + } else if (days > 0 && days <= 31) { + return 'warning' + } else { + // days <= 0 + return 'error' + } + } + + return ( +
+ + } + /> + } + /> + } + grow + /> + + + } + /> + + +
+ ) +} diff --git a/src/components/SkStack.tsx b/src/components/SkStack.tsx new file mode 100644 index 0000000..e862cf8 --- /dev/null +++ b/src/components/SkStack.tsx @@ -0,0 +1,42 @@ +/** + * @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 SkStack.tsx + * @copyright SKALE Labs 2023-Present + */ + +import { ReactElement } from 'react' +import Stack from '@mui/material/Stack' + +export default function SkStack(props: { + className?: string + children?: ReactElement | ReactElement[] +}) { + return ( + + {props.children} + + ) +} diff --git a/src/components/TermsModal.tsx b/src/components/TermsModal.tsx index 50b545f..e6d5084 100644 --- a/src/components/TermsModal.tsx +++ b/src/components/TermsModal.tsx @@ -35,7 +35,7 @@ import GradingRoundedIcon from '@mui/icons-material/GradingRounded' import { type MetaportCore, SkPaper, cls, cmn, styles } from '@skalenetwork/metaport' import { PORTAL_URLS } from '../core/constants' -import TermsOfService from '../terms-of-service.mdx' +import TermsOfService from '../data/terms-of-service.mdx' export default function TermsModal(props: { mpc: MetaportCore diff --git a/src/components/Tile.tsx b/src/components/Tile.tsx index bf5281d..3723393 100644 --- a/src/components/Tile.tsx +++ b/src/components/Tile.tsx @@ -26,14 +26,17 @@ 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 + value?: string textRi?: string icon?: ReactElement className?: string grow?: boolean - color?: 'primary' | 'warning' | 'error' + color?: DueDateStatus + progressColor?: DueDateStatus progress?: number children?: ReactElement | ReactElement[] }) { @@ -60,22 +63,24 @@ export default function Tile(props: {

{props.textRi}

-

- {props.value} -

+ {props.value ? ( +

+ {props.value} +

+ ) : null} {props.progress ? ( diff --git a/src/components/Topup.tsx b/src/components/Topup.tsx index 48b12e8..044d586 100644 --- a/src/components/Topup.tsx +++ b/src/components/Topup.tsx @@ -21,136 +21,132 @@ * @copyright SKALE Labs 2023-Present */ -import TollIcon from '@mui/icons-material/Toll' -import AvTimerRoundedIcon from '@mui/icons-material/AvTimerRounded' +import { Link } from 'react-router-dom' +import Button from '@mui/material/Button' +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, - TokenIcon + fromWei, + toWei, + walletClientToSigner, + enforceNetwork, + useWagmiWalletClient, + useWagmiSwitchNetwork } from '@skalenetwork/metaport' -import Stack from '@mui/material/Stack' 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 { Collapse } from '@mui/material' + +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) -export default function Topup(_: { mpc: MetaportCore; name: string }) { - // const network = props.mpc.config.skaleNetwork - // const alias = getChainAlias(network, props.name) + const topupPeriodText = `${props.topupPeriod} ${props.topupPeriod === 1 ? 'month' : 'months'}` + const helperText = `${truncateDecimals(chainPriceSkl.toString(), 6)} SKL x ${topupPeriodText}` + + const balanceOk = props.tokenBalance >= totalPriceWei + const topupBtnText = balanceOk ? 'Top-up chain' : 'Insufficient funds' return (
-

- Payment info -

- + } + children={ + + } grow - icon={} /> + + } grow - icon={} /> - } grow /> - - } - /> - - - -

- Top-up chain -

- - } - grow - // children={} + color={balanceOk ? undefined : 'error'} /> - - - {/* -
-
-
-

- Paid until -

-

- Max topup period: 12 month -

-
-

- 02.04.2024 -

- -
-
-
-
-
-

- Payment overdue -

-
-

- 45 days -

-
-
-
*/} - - {/* -
-
-
-

- Paid until -

-

- Max top-up period: 12 month -

-
-

- 02.04.2024 -

- -
-
-
-
-
-

- Until due date -

-
-

- 125 days -

+ + + + } + color="error" + grow + children={ + + } + /> + + +
+
+ + {!balanceOk ? ( + + + + ) : null}
- */}
) } diff --git a/src/core/constants.ts b/src/core/constants.ts index 60a1996..c3368a8 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2022-Present */ -import FAQ from '../faq.json' +import FAQ from '../data/faq.json' export const MAINNET_CHAIN_NAME = 'mainnet' @@ -42,3 +42,6 @@ import * as MAINNET_CHAIN_LOGOS from '../meta/logos' export { FAQ, MAINNET_CHAIN_LOGOS } export const DISCORD_INVITE_URL = 'https://discord.com/invite/gM5XBy6' + +const _DEFAULT_UPDATE_INTERVAL_SECONDS = 10 +export const DEFAULT_UPDATE_INTERVAL_MS = _DEFAULT_UPDATE_INTERVAL_SECONDS * 1000 diff --git a/src/core/paymaster.ts b/src/core/paymaster.ts new file mode 100644 index 0000000..c614a86 --- /dev/null +++ b/src/core/paymaster.ts @@ -0,0 +1,102 @@ +/** + * @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' + +export interface PaymasterInfo { + maxReplenishmentPeriod: bigint + oneSklPrice: bigint + schainPricePerMonth: bigint + skaleToken: string + schain: { + name: string + paidUntil: 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 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 async function getPaymasterInfo( + paymaster: Contract, + targetChainName: string +): Promise { + const rawData = await Promise.all([ + paymaster.maxReplenishmentPeriod(), + paymaster.oneSklPrice(), + paymaster.schainPricePerMonth(), + paymaster.skaleToken(), + paymaster.schains(id(targetChainName)) + ]) + return { + maxReplenishmentPeriod: rawData[0], + oneSklPrice: rawData[1], + schainPricePerMonth: rawData[2], + skaleToken: rawData[3], + schain: { + name: rawData[4][1], + paidUntil: rawData[4][2] + } + } +} + +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..7843c32 --- /dev/null +++ b/src/core/timeHelper.ts @@ -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 timeHelper.ts + * @copyright SKALE Labs 2022-Present + */ + +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 daysBetweenNowAndTimestamp(timestamp: bigint): number { + const timestampDate = new Date(Number(timestamp * 1000n)) + const currentDate = new Date() + const diffInMs = currentDate.getTime() - timestampDate.getTime() + const diffInDays = diffInMs / (1000 * 60 * 60 * 24) + return Math.round(diffInDays) * -1 +} + +export function calculateElapsedPercentage( + tsInSeconds: bigint, + maxReplenishmentPeriod: bigint +): number { + const tsDate = new Date(Number(tsInSeconds * 1000n)) + const futureTime = new Date() + futureTime.setMonth(futureTime.getMonth() + Number(maxReplenishmentPeriod)) + + const currentTime = new Date() + + const tsTimeMs = tsDate.getTime() + const currentTimeMs = currentTime.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 +} 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..ea40c25 --- /dev/null +++ b/src/data/paymaster.ts @@ -0,0 +1,708 @@ +export default { + networks: { + mainnet: { + chain: 'elated-tan-skat', + address: '0x' + }, + staging: { + chain: 'staging-perfect-parallel-gacrux', + address: '0xeF18D694e7659C1Ed5dE7e83E72e871b32f3fE69' + }, + legacy: { + chain: '', + address: '0x' + }, + regression: { + chain: '', + address: '0x' + } + }, + 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: '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' + } + ], + 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: '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: '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: [ + { + 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: 'id', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + name: 'setActiveNodes', + 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: 'id', + 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: [], + 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' + } + ] +} diff --git a/src/terms-of-service.mdx b/src/data/terms-of-service.mdx similarity index 100% rename from src/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 index 2450da2..4cd322f 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -26,30 +26,22 @@ import { useParams } from 'react-router-dom' import Button from '@mui/material/Button' import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' -import PaymentsRoundedIcon from '@mui/icons-material/PaymentsRounded' import AdminPanelSettingsRoundedIcon from '@mui/icons-material/AdminPanelSettingsRounded' import { Link } from 'react-router-dom' -import { cmn, cls, styles, type MetaportCore, getChainAlias, SkPaper } from '@skalenetwork/metaport' +import { cmn, cls, type MetaportCore, getChainAlias, SkPaper } from '@skalenetwork/metaport' -import Topup from '../components/Topup' +import Paymaster from '../components/Paymaster' export default function Admin(props: { mpc: MetaportCore }) { let { name } = useParams() name = name ?? '' - // const [expanded, setExpanded] = useState('panel1') - - // function handleChange(panel: string | false) { - // setExpanded(expanded && panel === expanded ? false : panel) - // } - const network = props.mpc.config.skaleNetwork const alias = getChainAlias(network, name) return ( - {/* */}
@@ -78,40 +70,13 @@ export default function Admin(props: { mpc: MetaportCore }) {
- +

Manage {alias}

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

- - -
-
- -
-

Chain top-up

- - - {/* } - > - - */} - {/* } - > -

Will be available soon

-
*/} + ) diff --git a/src/pages/Portfolio.tsx b/src/pages/Portfolio.tsx index 31160c4..fb3ffe4 100644 --- a/src/pages/Portfolio.tsx +++ b/src/pages/Portfolio.tsx @@ -41,6 +41,7 @@ import { } from '@skalenetwork/metaport' import TokenSurface from '../components/TokenSurface' +import ConnectWallet from '../components/ConnectWallet' export default function Portfolio(props: { mpc: MetaportCore }) { const { address } = useWagmiAccount() @@ -107,6 +108,7 @@ 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) => (
diff --git a/src/components/Schain.tsx b/src/pages/Schain.tsx similarity index 95% rename from src/components/Schain.tsx rename to src/pages/Schain.tsx index b38bd4f..d52b4ab 100644 --- a/src/components/Schain.tsx +++ b/src/pages/Schain.tsx @@ -26,7 +26,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 +58,7 @@ export default function Schain(props: { loadSchains: any; schains: any[]; mpc: M return (
-
+
diff --git a/src/pages/Terms.tsx b/src/pages/Terms.tsx index 9dd82c2..4afcd12 100644 --- a/src/pages/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 ( From 543fb658fad7f67ae0691e3f1ab94ea9bf66fd63 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 11 Dec 2023 15:33:48 +0000 Subject: [PATCH 14/31] Add approve to pay function --- bun.lockb | Bin 318745 -> 318745 bytes package.json | 2 +- src/components/Paymaster.tsx | 35 ++++++++++++++++++++++++++--------- src/components/Topup.tsx | 6 +----- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/bun.lockb b/bun.lockb index 1ed01d256530299d230b36ae09ba12aa4236a1d2..5c6f05a670499805957911446eab014e570a6f78 100755 GIT binary patch delta 179 zcmV;k08Iaxx)YhY6Ob+-y5V#F-5v+iEpKVeGmLYM@kX{B zcV5&JlNOF)QIrYpJmgs~y}1L+`J@(M2l3n?uRPTK*v_qoWm^G-V97=t)iw>VeLzNUvky1sDXVbJP_&uY>Uw{RtWe zeI+cYCZj#%+b3cW05nEiJ0?I_(}MSDiFaZeGMPds9{LD}Wm^G return ( diff --git a/src/components/Topup.tsx b/src/components/Topup.tsx index 044d586..408f671 100644 --- a/src/components/Topup.tsx +++ b/src/components/Topup.tsx @@ -32,11 +32,7 @@ import { cls, type MetaportCore, fromWei, - toWei, - walletClientToSigner, - enforceNetwork, - useWagmiWalletClient, - useWagmiSwitchNetwork + toWei } from '@skalenetwork/metaport' import Tile from './Tile' From 79122b77162986e1c7b5f17dfb18aa9b4b52eeac Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 11 Dec 2023 16:44:41 +0000 Subject: [PATCH 15/31] Add legacy paymaster, update metaport dependency --- bun.lockb | Bin 318745 -> 318745 bytes package.json | 2 +- src/data/paymaster.ts | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lockb b/bun.lockb index 5c6f05a670499805957911446eab014e570a6f78..b2bfff352ff1e8fd72603886ca62112469a073b2 100755 GIT binary patch delta 179 zcmV;k08Iaxx)YhY6Ob+-c{=M{wyvN*3K2}%bX&$m7D-{`1wxQX$p#HS;#c$cu};cY z2xK8q+G@|fd6S?q7=t)iw>VeVeGmLYM@kX{B zcV5&JlNOF)QIrYpJmgs~y}1L+`J@(M2l3n?uRPTK*v_qoWm^G Date: Tue, 12 Dec 2023 18:21:22 +0000 Subject: [PATCH 16/31] Update debt logic for chain pricing --- bun.lockb | Bin 318745 -> 319109 bytes package.json | 1 + src/App.scss | 4 ++ src/components/MonthSelector.tsx | 90 +++++++++++++++++++++++++------ src/components/Paymaster.tsx | 8 ++- src/components/PricingInfo.tsx | 14 +++-- src/components/Topup.tsx | 24 ++++----- src/core/constants.ts | 5 +- src/core/timeHelper.ts | 41 ++++++++++++-- tests/time.test.ts | 44 +++++++++++++++ 10 files changed, 191 insertions(+), 40 deletions(-) create mode 100644 tests/time.test.ts diff --git a/bun.lockb b/bun.lockb index b2bfff352ff1e8fd72603886ca62112469a073b2..1b49de2a22ef11b6980ef247bf19ad175e2e5f3b 100755 GIT binary patch delta 48706 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 A>Hq)$ delta 47843 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 diff --git a/package.json b/package.json index 38f3012..8255040 100644 --- a/package.json +++ b/package.json @@ -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/src/App.scss b/src/App.scss index 466653f..a79678d 100644 --- a/src/App.scss +++ b/src/App.scss @@ -561,4 +561,8 @@ button:focus { .divider { border-bottom: 1px $border-color solid; padding-bottom: 10px; +} + +.flexi{ + display: inline-flex; } \ No newline at end of file diff --git a/src/components/MonthSelector.tsx b/src/components/MonthSelector.tsx index 071c644..edf2b62 100644 --- a/src/components/MonthSelector.tsx +++ b/src/components/MonthSelector.tsx @@ -21,40 +21,96 @@ * @copyright SKALE Labs 2023-Present */ +import { useState } from 'react' import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' + import { cmn, cls } from '@skalenetwork/metaport' -const MONTH_RECOMENDATIONS = [1, 2, 3, 6, 12, 18, 24] +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 (
- {MONTH_RECOMENDATIONS.filter((x) => x <= props.max).map((month: any, i: number) => ( + {monthRecommendations + .filter((x) => x <= props.max) + .map((month: any, i: number) => ( + + ))} + {openCustom ? ( +
+ ) => { + setTextPeriod(Number(event.target.value)) + }} + className={cls(cmn.mri10)} + /> + +
+ ) : ( - ))} - + )}
) } diff --git a/src/components/Paymaster.tsx b/src/components/Paymaster.tsx index dbcd1aa..ed56742 100644 --- a/src/components/Paymaster.tsx +++ b/src/components/Paymaster.tsx @@ -118,12 +118,16 @@ export default function Paymaster(props: { mpc: MetaportCore; name: string }) { const allowance = await connectedToken.allowance(address, paymasterAddress) const totalPriceWei = getTotalPriceWei() if (allowance <= totalPriceWei) { - const approveRes = await sendTransaction( - connectedToken.approve, [paymasterAddress, totalPriceWei * APPROVE_MULTIPLIER]) + setBtnText('Waiting for approval...') + const approveRes = await sendTransaction(connectedToken.approve, [ + paymasterAddress, + totalPriceWei * APPROVE_MULTIPLIER + ]) if (!approveRes.status) { setErrorMsg(approveRes.err?.name) return } + setBtnText('Sending transaction...') } const res = await sendTransaction(connectedPaymaster.pay, [id(props.name), topupPeriod]) if (!res.status) { diff --git a/src/components/PricingInfo.tsx b/src/components/PricingInfo.tsx index b18674e..8f4d70a 100644 --- a/src/components/PricingInfo.tsx +++ b/src/components/PricingInfo.tsx @@ -29,8 +29,10 @@ import { cmn, TokenIcon, fromWei } from '@skalenetwork/metaport' import { truncateDecimals, PaymasterInfo, DueDateStatus, divideBigInts } from '../core/paymaster' import { daysBetweenNowAndTimestamp, + monthsBetweenNowAndTimestamp, calculateElapsedPercentage, - formatBigIntTimestampSeconds + formatBigIntTimestampSeconds, + formatTimePeriod } from '../core/timeHelper' import { DEFAULT_ERC20_DECIMALS } from '../core/constants' @@ -43,8 +45,9 @@ export default function PricingInfo(props: { info: PaymasterInfo }) { const chainPriceSkl = divideBigInts(props.info.schainPricePerMonth, props.info.oneSklPrice) const untilDueDateDays = daysBetweenNowAndTimestamp(props.info.schain.paidUntil) + const untilDueDateMonths = monthsBetweenNowAndTimestamp(props.info.schain.paidUntil) const dueDateStatus = getDueDateStatus(untilDueDateDays) - const dueDateText = untilDueDateDays < 0 ? 'Payment overdue' : 'Until due date' + const dueDateText = untilDueDateDays < 0 ? 'Payment overdue' : 'Paid for' const elapsedPercentage = calculateElapsedPercentage( props.info.schain.paidUntil, @@ -88,14 +91,17 @@ export default function PricingInfo(props: { info: PaymasterInfo }) { } /> diff --git a/src/components/Topup.tsx b/src/components/Topup.tsx index 408f671..b4ae6f4 100644 --- a/src/components/Topup.tsx +++ b/src/components/Topup.tsx @@ -24,16 +24,12 @@ 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 { cmn, cls, type MetaportCore, fromWei, toWei } from '@skalenetwork/metaport' import Tile from './Tile' import SkStack from './SkStack' @@ -42,7 +38,7 @@ import Loader from './Loader' import { PaymasterInfo, divideBigInts, truncateDecimals } from '../core/paymaster' import { DEFAULT_ERC20_DECIMALS } from '../core/constants' -import { Collapse } from '@mui/material' +import { formatTimePeriod, monthsBetweenNowAndTimestamp } from '../core/timeHelper' export default function Topup(props: { mpc: MetaportCore @@ -65,12 +61,15 @@ export default function Topup(props: { const tokenBalanceSkl = fromWei(props.tokenBalance, DEFAULT_ERC20_DECIMALS) - const topupPeriodText = `${props.topupPeriod} ${props.topupPeriod === 1 ? 'month' : 'months'}` + 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) + const maxTopupPeriod = Number(props.info.maxReplenishmentPeriod) - untilDueDateMonths + return (
@@ -79,10 +78,11 @@ export default function Topup(props: { icon={} children={ } grow @@ -129,7 +129,7 @@ export default function Topup(props: { - - - - -
+ {getChainAlias(props.skaleNetwork, props.schain[0], undefined, true)} + + +
-

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

) From 84d06723ead81fbd24487342692714423f8e0a9d Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 20 Dec 2023 18:12:41 +0000 Subject: [PATCH 24/31] Add effectiveTimestamp support for legacy network --- src/App.scss | 4 +- src/components/Paymaster.tsx | 2 +- src/components/PricingInfo.tsx | 10 +- src/components/Topup.tsx | 5 +- src/core/paymaster.ts | 13 +- src/core/timeHelper.ts | 11 +- src/data/paymaster.ts | 217 ++++++++++++++++++++++++++++++++- 7 files changed, 247 insertions(+), 15 deletions(-) diff --git a/src/App.scss b/src/App.scss index 2cae7e6..d92d4c5 100644 --- a/src/App.scss +++ b/src/App.scss @@ -242,7 +242,7 @@ body::-webkit-scrollbar { .br__tile { height: 150px; border-radius: 25px !important; - border: 1px solid #171616 !important + border: 1px solid #171616 !important; } .pageCard { @@ -369,7 +369,7 @@ body::-webkit-scrollbar { text-transform: none !important; color: #ffffffe6 !important; padding: 6px 10px 6px 12px !important; - font-size: 0.75rem !important; + font-size: 0.7rem !important; min-width: 55px; width: 100%; diff --git a/src/components/Paymaster.tsx b/src/components/Paymaster.tsx index aad602a..acefb60 100644 --- a/src/components/Paymaster.tsx +++ b/src/components/Paymaster.tsx @@ -87,7 +87,7 @@ export default function Paymaster(props: { mpc: MetaportCore; name: string }) { }, [sklToken, address]) async function loadPaymasterInfo() { - const info = await getPaymasterInfo(paymaster, props.name) + const info = await getPaymasterInfo(paymaster, props.name, network) let skl = sklToken if (skl === undefined) { skl = new Contract(info.skaleToken, ERC_ABIS.erc20.abi, paymaster.runner) diff --git a/src/components/PricingInfo.tsx b/src/components/PricingInfo.tsx index 8f4d70a..83ee25e 100644 --- a/src/components/PricingInfo.tsx +++ b/src/components/PricingInfo.tsx @@ -44,8 +44,14 @@ export default function PricingInfo(props: { info: PaymasterInfo }) { const chainPriceUsd = fromWei(props.info.schainPricePerMonth, DEFAULT_ERC20_DECIMALS) const chainPriceSkl = divideBigInts(props.info.schainPricePerMonth, props.info.oneSklPrice) - const untilDueDateDays = daysBetweenNowAndTimestamp(props.info.schain.paidUntil) - const untilDueDateMonths = monthsBetweenNowAndTimestamp(props.info.schain.paidUntil) + const untilDueDateDays = daysBetweenNowAndTimestamp( + props.info.schain.paidUntil, + props.info.effectiveTimestamp + ) + const untilDueDateMonths = monthsBetweenNowAndTimestamp( + props.info.schain.paidUntil, + props.info.effectiveTimestamp + ) const dueDateStatus = getDueDateStatus(untilDueDateDays) const dueDateText = untilDueDateDays < 0 ? 'Payment overdue' : 'Paid for' diff --git a/src/components/Topup.tsx b/src/components/Topup.tsx index b4ae6f4..5ada203 100644 --- a/src/components/Topup.tsx +++ b/src/components/Topup.tsx @@ -67,7 +67,10 @@ export default function Topup(props: { const balanceOk = props.tokenBalance >= totalPriceWei const topupBtnText = balanceOk ? 'Top-up chain' : 'Insufficient funds' - const untilDueDateMonths = monthsBetweenNowAndTimestamp(props.info.schain.paidUntil) + const untilDueDateMonths = monthsBetweenNowAndTimestamp( + props.info.schain.paidUntil, + props.info.effectiveTimestamp + ) const maxTopupPeriod = Number(props.info.maxReplenishmentPeriod) - untilDueDateMonths return ( diff --git a/src/core/paymaster.ts b/src/core/paymaster.ts index c614a86..e916a1b 100644 --- a/src/core/paymaster.ts +++ b/src/core/paymaster.ts @@ -33,6 +33,7 @@ export interface PaymasterInfo { name: string paidUntil: bigint } + effectiveTimestamp?: bigint } export type DueDateStatus = 'primary' | 'warning' | 'error' | 'success' @@ -74,7 +75,8 @@ export function initPaymaster(mpc: MetaportCore): Contract { export async function getPaymasterInfo( paymaster: Contract, - targetChainName: string + targetChainName: string, + skaleNetwork: interfaces.SkaleNetwork ): Promise { const rawData = await Promise.all([ paymaster.maxReplenishmentPeriod(), @@ -83,6 +85,12 @@ export async function getPaymasterInfo( paymaster.skaleToken(), paymaster.schains(id(targetChainName)) ]) + + let effectiveTimestamp + if (skaleNetwork === 'legacy') { + effectiveTimestamp = await paymaster.effectiveTimestamp() + } + return { maxReplenishmentPeriod: rawData[0], oneSklPrice: rawData[1], @@ -91,7 +99,8 @@ export async function getPaymasterInfo( schain: { name: rawData[4][1], paidUntil: rawData[4][2] - } + }, + effectiveTimestamp } } diff --git a/src/core/timeHelper.ts b/src/core/timeHelper.ts index 47de3e1..52359e6 100644 --- a/src/core/timeHelper.ts +++ b/src/core/timeHelper.ts @@ -43,16 +43,17 @@ export function monthsBetweenTimestamps(from: bigint, to: bigint): number { return Math.round(diffInDays / AVG_MONTH_LENGTH) } -export function getCurrentTsBigInt(): bigint { +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): number { - return daysBetweenTimestamps(getCurrentTsBigInt(), timestamp) +export function daysBetweenNowAndTimestamp(timestamp: bigint, customCurrentTs?: bigint): number { + return daysBetweenTimestamps(getCurrentTsBigInt(customCurrentTs), timestamp) } -export function monthsBetweenNowAndTimestamp(timestamp: bigint): number { - return monthsBetweenTimestamps(getCurrentTsBigInt(), timestamp) +export function monthsBetweenNowAndTimestamp(timestamp: bigint, customCurrentTs?: bigint): number { + return monthsBetweenTimestamps(getCurrentTsBigInt(customCurrentTs), timestamp) } export function calculateElapsedPercentage( diff --git a/src/data/paymaster.ts b/src/data/paymaster.ts index 01e7413..2a55115 100644 --- a/src/data/paymaster.ts +++ b/src/data/paymaster.ts @@ -174,6 +174,11 @@ export default { name: 'SklPriceIsNotSet', type: 'error' }, + { + inputs: [], + name: 'SklPriceIsOutdated', + type: 'error' + }, { inputs: [], name: 'TimeIntervalIsAlreadyProcessed', @@ -259,6 +264,22 @@ export default { name: 'ValidatorDeletionError', type: 'error' }, + { + inputs: [ + { + internalType: 'ValidatorId', + name: 'id', + type: 'uint256' + }, + { + internalType: 'Timestamp', + name: 'when', + type: 'uint256' + } + ], + name: 'ValidatorHasBeenRemoved', + type: 'error' + }, { inputs: [ { @@ -327,6 +348,19 @@ export default { stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'allowedSklPriceLag', + outputs: [ + { + internalType: 'Seconds', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, { inputs: [], name: 'authority', @@ -439,6 +473,134 @@ export default { 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: [ { @@ -491,6 +653,18 @@ export default { stateMutability: 'view', type: 'function' }, + { + inputs: [], + name: 'effectiveTimestamp', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, { inputs: [ { @@ -581,7 +755,7 @@ export default { inputs: [ { internalType: 'ValidatorId', - name: 'id', + name: 'validatorId', type: 'uint256' }, { @@ -595,6 +769,19 @@ export default { stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [ + { + internalType: 'Seconds', + name: 'lagSeconds', + type: 'uint256' + } + ], + name: 'setAllowedSklPriceLag', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, { inputs: [ { @@ -625,7 +812,7 @@ export default { inputs: [ { internalType: 'ValidatorId', - name: 'id', + name: 'validatorId', type: 'uint256' }, { @@ -678,6 +865,19 @@ export default { stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [ + { + internalType: 'string', + name: 'newVersion', + type: 'string' + } + ], + name: 'setVersion', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, { inputs: [], name: 'skaleToken', @@ -703,6 +903,19 @@ export default { ], stateMutability: 'view', type: 'function' + }, + { + inputs: [], + name: 'version', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string' + } + ], + stateMutability: 'view', + type: 'function' } ] } From 30f97c940d0d287bc5363f134063803842d33d23 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 20 Dec 2023 18:56:26 +0000 Subject: [PATCH 25/31] Minor updates to code style --- src/SkBottomNavigation.tsx | 4 ++-- src/components/ChainCard.tsx | 2 -- src/components/HelpZen.tsx | 8 ++++---- src/components/MonthSelector.tsx | 4 ++-- src/components/TermsModal.tsx | 4 ++-- src/components/Toggle.tsx | 4 ++-- 6 files changed, 12 insertions(+), 14 deletions(-) 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/components/ChainCard.tsx b/src/components/ChainCard.tsx index 4e73752..7378243 100644 --- a/src/components/ChainCard.tsx +++ b/src/components/ChainCard.tsx @@ -37,8 +37,6 @@ import ChainLogo from './ChainLogo' 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 } diff --git a/src/components/HelpZen.tsx b/src/components/HelpZen.tsx index 1e009f9..3db55ac 100644 --- a/src/components/HelpZen.tsx +++ b/src/components/HelpZen.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom' -import React, { useEffect, useState, MouseEvent } from 'react' +import { useEffect, useState, MouseEvent } from 'react' import Box from '@mui/material/Box' import Tooltip from '@mui/material/Tooltip' import IconButton from '@mui/material/IconButton' @@ -13,7 +13,7 @@ import { cls, styles, cmn } from '@skalenetwork/metaport' export default function HelpZen() { const [anchorEl, setAnchorEl] = useState(null) - const [openZen, setOpenZen] = React.useState(false) + const [openZen, setOpenZen] = useState(false) const open = Boolean(anchorEl) @@ -39,7 +39,7 @@ export default function HelpZen() { }, []) return ( - +
- +
) } diff --git a/src/components/MonthSelector.tsx b/src/components/MonthSelector.tsx index a6ef837..d05f35c 100644 --- a/src/components/MonthSelector.tsx +++ b/src/components/MonthSelector.tsx @@ -29,7 +29,7 @@ import { cmn, cls } from '@skalenetwork/metaport' import { formatTimePeriod } from '../core/timeHelper' -const _MONTH_RECOMMENDATIONS = [1, 2, 3, 6, 12, 18, 24] +const MONTH_RECOMMENDATIONS = [1, 2, 3, 6, 12, 18, 24] export default function MonthSelector(props: { max: number @@ -38,7 +38,7 @@ export default function MonthSelector(props: { setErrorMsg: (errorMsg: string | undefined) => void className?: string }) { - const [monthRecommendations, setMonthRecommendations] = useState(_MONTH_RECOMMENDATIONS) + const [monthRecommendations, setMonthRecommendations] = useState(MONTH_RECOMMENDATIONS) const [openCustom, setOpenCustom] = useState(false) const [customPeriod, setCustomPeriod] = useState() const [textPeriod, setTextPeriod] = useState() diff --git a/src/components/TermsModal.tsx b/src/components/TermsModal.tsx index e6d5084..e018465 100644 --- a/src/components/TermsModal.tsx +++ b/src/components/TermsModal.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2022-Present */ -import React, { type Dispatch, type SetStateAction } from 'react' +import { useState, type Dispatch, type SetStateAction } from 'react' import Container from '@mui/material/Container' import Button from '@mui/material/Button' @@ -42,7 +42,7 @@ export default function TermsModal(props: { termsAccepted: boolean setTermsAccepted: Dispatch> }) { - const [scrolled, setScrolled] = React.useState(false) + const [scrolled, setScrolled] = useState(false) const portalUrl = PORTAL_URLS[props.mpc.config.skaleNetwork] ?? PORTAL_URLS.mainnet diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index 4f40a55..6a5b0f8 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -1,9 +1,9 @@ -import * as React from 'react' +import { useState } from 'react' import ToggleButton from '@mui/material/ToggleButton' import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' export default function ToggleButtons() { - const [alignment, setAlignment] = React.useState('left') + const [alignment, setAlignment] = useState('left') const handleAlignment = (_: React.MouseEvent, newAlignment: string | null) => { setAlignment(newAlignment) From 86d5d9eeabf14ad47b2d5ab700459a5adcc26eba Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 21 Dec 2023 13:27:27 +0000 Subject: [PATCH 26/31] Add pricing launch timestamp --- src/Router.tsx | 9 ++++++--- src/components/ChainAccordion.tsx | 20 +++++++++++++------- src/core/paymaster.ts | 9 +++++++++ src/data/paymaster.ts | 12 ++++++++---- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/Router.tsx b/src/Router.tsx index 2e1f5e6..bc0f985 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -21,6 +21,7 @@ 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'; @@ -100,9 +101,11 @@ export default function Router() { } /> } /> - - } /> - + {pricingLaunchTsReached(mpc.config.skaleNetwork) ? ( + + } /> + + ) : null}
) diff --git a/src/components/ChainAccordion.tsx b/src/components/ChainAccordion.tsx index 09446f7..3ce0804 100644 --- a/src/components/ChainAccordion.tsx +++ b/src/components/ChainAccordion.tsx @@ -54,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) @@ -190,11 +192,15 @@ export default function ChainAccordion(props: { explorerUrl={explorerUrl} /> - } - url={`/admin/${props.schainName}`} - /> + {pricingLaunchTsReached(network) ? ( + } + url={`/admin/${props.schainName}`} + /> + ) : ( +
+ )} ) } diff --git a/src/core/paymaster.ts b/src/core/paymaster.ts index e916a1b..c2b22f8 100644 --- a/src/core/paymaster.ts +++ b/src/core/paymaster.ts @@ -23,6 +23,7 @@ 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 @@ -61,6 +62,10 @@ export function getPaymasterAddress(skaleNetwork: interfaces.SkaleNetwork): stri 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 } @@ -73,6 +78,10 @@ export function initPaymaster(mpc: MetaportCore): Contract { 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, diff --git a/src/data/paymaster.ts b/src/data/paymaster.ts index 2a55115..0c675af 100644 --- a/src/data/paymaster.ts +++ b/src/data/paymaster.ts @@ -2,19 +2,23 @@ export default { networks: { mainnet: { chain: 'elated-tan-skat', - address: '0x' + address: '0x', + launchTs: '0' }, staging: { chain: 'staging-perfect-parallel-gacrux', - address: '0xeF18D694e7659C1Ed5dE7e83E72e871b32f3fE69' + address: '0xeF18D694e7659C1Ed5dE7e83E72e871b32f3fE69', + launchTs: '0' }, legacy: { chain: 'skale-innocent-nasty', - address: '0xCa1B0A6236BBA2b30F7260863b56209a97351853' + address: '0xCa1B0A6236BBA2b30F7260863b56209a97351853', + launchTs: '1708521648' }, regression: { chain: '', - address: '0x' + address: '0x', + launchTs: '0' } }, abi: [ From 0180714991ad1a5f743123b7336288144a2318be Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 28 Dec 2023 18:26:58 +0000 Subject: [PATCH 27/31] Update schain overview page css, fix elapsed time func --- src/components/CopySurface.tsx | 2 +- src/components/PricingInfo.tsx | 8 +++++- src/components/SchainDetails.tsx | 46 +++++++++++++++----------------- src/components/SkStack.tsx | 1 + src/components/Tile.tsx | 36 +++++++++++++------------ src/core/timeHelper.ts | 11 ++++---- src/data/paymaster.ts | 6 ++--- src/pages/Schain.tsx | 10 +------ 8 files changed, 59 insertions(+), 61 deletions(-) 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/PricingInfo.tsx b/src/components/PricingInfo.tsx index 83ee25e..8c430cb 100644 --- a/src/components/PricingInfo.tsx +++ b/src/components/PricingInfo.tsx @@ -57,7 +57,8 @@ export default function PricingInfo(props: { info: PaymasterInfo }) { const elapsedPercentage = calculateElapsedPercentage( props.info.schain.paidUntil, - props.info.maxReplenishmentPeriod + props.info.maxReplenishmentPeriod, + props.info.effectiveTimestamp ) function getDueDateStatus(days: number): DueDateStatus { @@ -110,6 +111,11 @@ export default function PricingInfo(props: { info: PaymasterInfo }) { value={`${formatTimePeriod(Math.abs(untilDueDateMonths), 'month')} `} text={dueDateText} color={dueDateStatus} + textRi={ + props.info.effectiveTimestamp + ? '// Current ts: ' + formatBigIntTimestampSeconds(props.info.effectiveTimestamp) + : '' + } />
diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index 30ac9b8..398ca9c 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -24,7 +24,6 @@ import { Helmet } from 'react-helmet' import Button from '@mui/material/Button' -import Stack from '@mui/material/Stack' import AddCircleRoundedIcon from '@mui/icons-material/AddCircleRounded' import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' @@ -43,10 +42,12 @@ import { interfaces } from '@skalenetwork/metaport' +import SkStack from './SkStack' import ChainLogo from './ChainLogo' import CopySurface from './CopySurface' import ChainAccordion from './ChainAccordion' import ChainCategories from './ChainCategories' +import Tile from './Tile' import { MAINNET_CHAIN_LOGOS } from '../core/constants' import { getRpcUrl, getExplorerUrl, getChainId, HTTPS_PREFIX } from '../core/chain' @@ -113,19 +114,20 @@ export default function SchainDetails(props: {
- - +

{chainAlias}

{chainDescription}

- -
- -
+ + + + -
+
- {props.chainMeta?.url ? ( -
+
+ {props.chainMeta?.url ? ( -
- ) : null} - -
- - - + ) : null} +
+
+ } + /> + +
diff --git a/src/components/SkStack.tsx b/src/components/SkStack.tsx index e862cf8..5ff9d0c 100644 --- a/src/components/SkStack.tsx +++ b/src/components/SkStack.tsx @@ -30,6 +30,7 @@ export default function SkStack(props: { }) { return ( -
- {props.icon ? ( -
{props.icon}
- ) : null} -

{props.text}

-

{props.textRi}

-
+ {props.text ? ( +
+ {props.icon ? ( +
{props.icon}
+ ) : null} +

{props.text}

+

{props.textRi}

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

- - - + ) } From 71c1b44def2224d70374f33f4265bd4ab2a55cab Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 2 Jan 2024 17:33:16 +0000 Subject: [PATCH 28/31] Minor CSS changes --- src/App.scss | 2 ++ src/components/SchainDetails.tsx | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/App.scss b/src/App.scss index d92d4c5..f6b2e0a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -468,6 +468,8 @@ code { } .chainDetails { + max-width: calc(100vw - 32px); + .logo { height: 60pt; margin: 30px 0; diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index 398ca9c..0000722 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -115,10 +115,13 @@ export default function SchainDetails(props: {

-
-

{chainAlias}

-

{chainDescription}

-
+ +

{chainAlias}

+

{chainDescription}

+
} + />
Date: Tue, 2 Jan 2024 19:39:10 +0000 Subject: [PATCH 29/31] Update month selector in sChain pricing --- src/App.scss | 31 +++++++++++ src/components/MonthSelector.tsx | 90 ++++++++++++++++++-------------- src/components/SchainDetails.tsx | 10 ++-- src/pages/Admin.tsx | 4 +- 4 files changed, 91 insertions(+), 44 deletions(-) diff --git a/src/App.scss b/src/App.scss index f6b2e0a..dc9ca4a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -493,6 +493,37 @@ code { 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 { diff --git a/src/components/MonthSelector.tsx b/src/components/MonthSelector.tsx index d05f35c..b2990d2 100644 --- a/src/components/MonthSelector.tsx +++ b/src/components/MonthSelector.tsx @@ -24,7 +24,8 @@ 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' @@ -52,7 +53,7 @@ export default function MonthSelector(props: { } return ( -
+
{monthRecommendations .filter((x) => x <= props.max) .map((month: any, i: number) => ( @@ -69,48 +70,61 @@ export default function MonthSelector(props: { ))} {openCustom ? (
- ) => { - if ( - parseFloat(event.target.value) < 0 || - !Number.isInteger(Number(event.target.value)) - ) { - setTextPeriod('') - return - } - setTextPeriod(event.target.value) - }} - className={cls(cmn.mri10)} - /> +
+ ) => { + 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/SchainDetails.tsx b/src/components/SchainDetails.tsx index 0000722..3fd7e58 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -117,10 +117,12 @@ export default function SchainDetails(props: { -

{chainAlias}

-

{chainDescription}

-
} + children={ +
+

{chainAlias}

+

{chainDescription}

+
+ } /> diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 4cd322f..d263264 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -41,8 +41,8 @@ export default function Admin(props: { mpc: MetaportCore }) { const alias = getChainAlias(network, name) return ( - - + +
From 31e96f7b53145a627e22ae5f7e8457e97ecace27 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 3 Jan 2024 13:35:46 +0000 Subject: [PATCH 30/31] Minor UI changes - month selector error handling --- src/App.scss | 4 +- src/components/MonthSelector.tsx | 1 + src/components/MonthSlider.tsx | 68 -------------------------------- src/components/SchainDetails.tsx | 10 ++--- 4 files changed, 8 insertions(+), 75 deletions(-) delete mode 100644 src/components/MonthSlider.tsx diff --git a/src/App.scss b/src/App.scss index dc9ca4a..bf72dfd 100644 --- a/src/App.scss +++ b/src/App.scss @@ -471,8 +471,8 @@ code { max-width: calc(100vw - 32px); .logo { - height: 60pt; - margin: 30px 0; + height: 65pt; + margin: 35px 0; width: 100%; text-align: center; diff --git a/src/components/MonthSelector.tsx b/src/components/MonthSelector.tsx index b2990d2..db58ae8 100644 --- a/src/components/MonthSelector.tsx +++ b/src/components/MonthSelector.tsx @@ -111,6 +111,7 @@ export default function MonthSelector(props: { setCustomPeriod(Number(textPeriod)) } props.setTopupPeriod(Number(textPeriod)) + props.setErrorMsg(undefined) }} >

Apply

diff --git a/src/components/MonthSlider.tsx b/src/components/MonthSlider.tsx deleted file mode 100644 index 547d961..0000000 --- a/src/components/MonthSlider.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @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 Box from '@mui/material/Box' -import Slider from '@mui/material/Slider' - -const marks = [ - { - value: 1, - label: '1m' - }, - { - value: 3, - label: '3m' - }, - { - value: 6, - label: '6m' - }, - { - value: 9, - label: '9m' - }, - { - value: 12, - label: '12m' - } -] - -function valuetext(value: number) { - return `${value}°C` -} - -export default function DiscreteSliderMarks() { - return ( - - - - ) -} diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index 3fd7e58..2f0a810 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -133,7 +133,7 @@ export default function SchainDetails(props: {
@@ -160,10 +160,10 @@ export default function SchainDetails(props: { > ) : null} From fa6a1b9e51a7146311ada175052b5b8f1dab3593 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 4 Jan 2024 17:16:16 +0000 Subject: [PATCH 31/31] Update paymaster.ts - add staging contracts --- src/data/paymaster.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/paymaster.ts b/src/data/paymaster.ts index 77f2763..a4e2c38 100644 --- a/src/data/paymaster.ts +++ b/src/data/paymaster.ts @@ -6,8 +6,8 @@ export default { launchTs: '0' }, staging: { - chain: 'staging-perfect-parallel-gacrux', - address: '0xeF18D694e7659C1Ed5dE7e83E72e871b32f3fE69', + chain: 'staging-legal-crazy-castor', + address: '0x9E444978d11E7e753017ce3329B01663D5D78240', launchTs: '0' }, legacy: {