diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9af9e37..652a6a6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,10 +6,14 @@ module.exports = { extends: [ 'plugin:react/recommended', 'standard-with-typescript', - 'prettier', - 'plugin:storybook/recommended' + 'prettier' ], - overrides: [], + overrides: [{ + files: ['*.jsx', '*.tsx'], + rules: { + '@typescript-eslint/explicit-function-return-type': ['off'], + }, + }], parserOptions: { ecmaVersion: 'latest', sourceType: 'module', @@ -18,8 +22,7 @@ module.exports = { rules: { 'react/jsx-key': 'off', 'react/react-in-jsx-scope': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'warn', 'no-console': 'warn', + '@typescript-eslint/triple-slash-reference': 'off' }, } diff --git a/.gitignore b/.gitignore index 730e58d..446c694 100644 --- a/.gitignore +++ b/.gitignore @@ -26,12 +26,11 @@ yarn-error.log* .env src/data/metaportConfig*.ts src/meta/ +src/assets/validators/index.ts src/metadata/chainsData.json src/metadata/faucet.json .vercel -chainsJson.json - public/sitemap.xml public/robots.txt \ No newline at end of file diff --git a/build.sh b/build.sh index 9ea5bdb..675a818 100644 --- a/build.sh +++ b/build.sh @@ -31,8 +31,9 @@ else fi node generate-imports.cjs ./src/meta/logos +node generate-imports.cjs ./src/assets/validators bash generate_sitemap.sh echo "Building..." -yarn build +bun run build diff --git a/bun.lockb b/bun.lockb index 7f592e7..d2499cd 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/config/legacy.ts b/config/legacy.ts index 6bc85e5..0d61d76 100644 --- a/config/legacy.ts +++ b/config/legacy.ts @@ -7,9 +7,9 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { debug: false, chains: [ 'mainnet', - 'skale-innocent-nasty', // europa - 'international-villainous-zaurak', // calypso - 'big-majestic-oval-SKALE' // qa chain + 'these-long-sadalsuud', // europa + 'adorable-quaint-bellatrix', // nebula + 'spanish-smug-auva' // calypso ], tokens: { eth: { @@ -19,17 +19,6 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { decimals: '18', name: 'SKALE', symbol: 'SKL' - }, - usdc: { - decimals: '6', - symbol: 'USDC', - name: 'USD Coin' - }, - trt: { - decimals: '18', - symbol: 'TRT', - name: 'Turtle Coin', - iconUrl: 'https://github.com/microsoft/fluentui-emoji/blob/main/assets/Turtle/3D/turtle_3d.png?raw=true' } }, connections: { @@ -37,58 +26,41 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { eth: { eth: { chains: { - 'skale-innocent-nasty': {}, - 'international-villainous-zaurak': { - hub: 'skale-innocent-nasty' + 'these-long-sadalsuud': {}, + 'adorable-quaint-bellatrix': { + hub: 'these-long-sadalsuud' } } } }, erc20: { skl: { - address: '0x17A7Cf31a11554e75246973663262dA56F84F89b', + address: '0x0E53fDa415cc6b2a7D9495D4a1F0659F0Ee45e0d', chains: { - 'skale-innocent-nasty': {}, - 'international-villainous-zaurak': { - hub: 'skale-innocent-nasty' + 'these-long-sadalsuud': {}, + 'adorable-quaint-bellatrix': { + hub: 'these-long-sadalsuud' } } - }, - // usdc: { - // address: '0x85dedAA65D33210E15911Da5E9dc29F5C93a50A9', - // chains: { - // 'skale-innocent-nasty': {}, - // 'international-villainous-zaurak': { - // hub: 'skale-innocent-nasty' - // } - // } - // } + } } }, - 'big-majestic-oval-SKALE': { + 'spanish-smug-auva': { erc20: { - trt: { - address: '0xbb2c9411079c6ddcd19c74e8442f77b70ae74267', - chains: { - 'international-villainous-zaurak': { - clone: true - } - } - } } }, - 'international-villainous-zaurak': { - // Calypso connections + 'adorable-quaint-bellatrix': { + // Nebula connections eth: { eth: { address: '0x9C0e8bC2B2D403299214c80081F93fAB5e10b593', chains: { - 'skale-innocent-nasty': { + 'these-long-sadalsuud': { clone: true }, mainnet: { clone: true, - hub: 'skale-innocent-nasty' + hub: 'these-long-sadalsuud' } } } @@ -97,37 +69,18 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { skl: { address: '0xFbbDF9aC97093b1E88aB79F7D0c296d9cc5eD0d0', chains: { - 'skale-innocent-nasty': { + 'these-long-sadalsuud': { clone: true }, mainnet: { - hub: 'skale-innocent-nasty', + hub: 'these-long-sadalsuud', clone: true } } - }, - trt: { - address: '0x45f7ca2ace063867e8e1378f0f2cfa86d8f591de', - chains: { - 'big-majestic-oval-SKALE': {} - } } - - // usdc: { - // address: '0x49c37d0Bb6238933eEe2157e9Df417fd62723fF6', - // chains: { - // 'skale-innocent-nasty': { - // clone: true - // }, - // mainnet: { - // hub: 'skale-innocent-nasty', - // clone: true - // } - // } - // } } }, - 'skale-innocent-nasty': { + 'these-long-sadalsuud': { // Europa connections eth: { eth: { @@ -136,35 +89,24 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { mainnet: { clone: true }, - 'international-villainous-zaurak': { - wrapper: '0x321e1aa81B4c6CC3B8EFe3D9c0AD67E6eC949c2c' + 'adorable-quaint-bellatrix': { + wrapper: '0x3a830008c24300Dd8F469EBFEd13E4854409440D' } } } }, erc20: { skl: { - address: '0xa101902B3119f4830292bb79ebAB56967229207B', + address: '0xDeCcD09457Bd23c4CDD3C6E07a00053Ff54869dd', chains: { mainnet: { clone: true }, - 'international-villainous-zaurak': { - wrapper: '0x51A1eD016633Afb00C25Eb404745C61D8c16BBd4' + 'adorable-quaint-bellatrix': { + wrapper: '0xEc656cc30205479C5DAa3aDac7b4D9d0fe0FDc51' } } - }, - // usdc: { - // address: '0x5d42495D417fcd9ECf42F3EA8a55FcEf44eD9B33', - // chains: { - // mainnet: { - // clone: true - // }, - // 'international-villainous-zaurak': { - // wrapper: '0x4f250cCE5b8B39caA96D1144b9A32E1c6a9f97b0' - // } - // } - // } + } } } }, diff --git a/generate-imports.cjs b/generate-imports.cjs index a9fd82e..6936797 100644 --- a/generate-imports.cjs +++ b/generate-imports.cjs @@ -11,7 +11,7 @@ if (!rootDir) { const getSvgFilesInDir = (dir) => { return fs.readdirSync(dir).filter(file => { const ext = path.extname(file); - return ['.png', '.svg', '.gif'].includes(ext); + return ['.png', '.svg', '.gif', '.webp', '.jpeg', '.jpg'].includes(ext); }).map(file => path.join(dir, file)) }; diff --git a/package.json b/package.json index 3b3dee0..2599b44 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,35 @@ { "name": "portal", "private": true, - "version": "2.1.1", + "version": "2.2.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint ./src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "prettier": "prettier --write \"src/**/*.{ts,tsx,js,mdx}\"", "version": "node -e \"console.log(require('./package.json').version);\"" }, "dependencies": { "@mdx-js/rollup": "^2.3.0", - "@mui/icons-material": "^5.14.8", - "@mui/material": "^5.14.5", - "@skalenetwork/metaport": "2.0.3-beta.2", + "@mui/icons-material": "^5.15.14", + "@mui/material": "^5.15.14", + "@skalenetwork/skale-contracts-ethers-v6": "1.0.1-develop.2", + "@skalenetwork/metaport": "2.1.0-develop.2", "@types/react-copy-to-clipboard": "^5.0.4", "@vercel/analytics": "^1.0.2", "eslint-config-prettier": "^9.0.0", "eslint-config-standard-with-typescript": "^39.1.0", + "ethers-multicall-provider": "^6.2.0", "prettier": "^3.0.3", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-helmet": "^6.1.0", "react-jazzicon": "^1.0.4", - "react-router-dom": "^6.15.0" + "react-router-dom": "^6.15.0", + "react-transition-group": "^4.4.5" }, "devDependencies": { "@types/react": "^18.2.15", @@ -37,6 +40,10 @@ "@vitejs/plugin-react": "^4.0.4", "bun-types": "^1.0.17", "eslint": "^8.45.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "sass": "^1.65.1", diff --git a/public/apps.png b/public/apps.png index a50f6ce..471d239 100644 Binary files a/public/apps.png and b/public/apps.png differ diff --git a/public/bridge.png b/public/bridge.png index 28d038f..19cdeef 100644 Binary files a/public/bridge.png and b/public/bridge.png differ diff --git a/public/chains.png b/public/chains.png index 892b8c9..98296cd 100644 Binary files a/public/chains.png and b/public/chains.png differ diff --git a/public/portfolio.png b/public/portfolio.png index 7039970..8aa0e48 100644 Binary files a/public/portfolio.png and b/public/portfolio.png differ diff --git a/public/staking.png b/public/staking.png new file mode 100644 index 0000000..9394105 Binary files /dev/null and b/public/staking.png differ diff --git a/public/stats.png b/public/stats.png index 8ab3336..56ab1cc 100644 Binary files a/public/stats.png and b/public/stats.png differ diff --git a/skale-network b/skale-network index c7d1848..7c643af 160000 --- a/skale-network +++ b/skale-network @@ -1 +1 @@ -Subproject commit c7d184841f2bde3469c6b102156181bd003a52d8 +Subproject commit 7c643afa0f54f839c04334c0ee837b7ba4b913f6 diff --git a/src/App.scss b/src/App.scss index bf72dfd..abfa6c1 100644 --- a/src/App.scss +++ b/src/App.scss @@ -236,7 +236,7 @@ body::-webkit-scrollbar { } .br__tile:hover { - transform: scale(1.05); + transform: scale(1.03); } .br__tile { @@ -245,14 +245,18 @@ body::-webkit-scrollbar { border: 1px solid #171616 !important; } +.border { + border: 1px solid #171616 !important; +} + .pageCard { height: 100% !important; border-radius: 25px !important; border: 1px rgb(44 44 44) solid !important; svg { - width: 20px; - height: 20px; + width: 15px; + height: 15px; } } @@ -354,6 +358,42 @@ body::-webkit-scrollbar { box-shadow: none !important; } + +.btnSm { + text-transform: none !important; + font-size: 0.8025rem !important; + line-height: 1.5 !important; + letter-spacing: 0.02857em !important; + font-weight: 600 !important; + padding: 0.5em 1.5em !important; + border-radius: $sk-border-radius !important; + box-shadow: none !important; +} + +.btnerror { + background: #f4433621; +} + +.btnwarning { + background: rgb(244 139 54 / 13%); +} + +.btnSmLoading { + padding: 0.5em 1.5em 0.5em 3em !important; +} + +.btnMd { + text-transform: none !important; + font-size: 0.8025rem !important; + line-height: 1.5 !important; + letter-spacing: 0.02857em !important; + font-weight: 600 !important; + padding: 0.6em 2em !important; + border-radius: $sk-border-radius !important; + box-shadow: none !important; +} + + .outlined { background: rgba(41, 255, 148, 0.08) } @@ -410,6 +450,10 @@ body::-webkit-scrollbar { font-size: 0.9rem !important; } +.pointer { + cursor: pointer; +} + code { overflow: hidden; white-space: nowrap; @@ -527,7 +571,7 @@ code { } .titleSection { - background: rgba(0, 0, 0, 0.6); + background: linear-gradient(180deg, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.4)); border-radius: 25px; padding: 20px; } @@ -552,11 +596,11 @@ button:focus { } .startCardText { - margin: 120px 10px 10px 10px; + margin: 125px 10px 10px 10px; background: #00000066; backdrop-filter: blur(10px) brightness(0.8); border-radius: 25px; - padding: 10px 15px; + padding: 15px 20px; } .startCardBg { @@ -579,6 +623,9 @@ button:focus { background-image: url(/apps.png); } +.startCardstaking { + background-image: url(/staking.png); +} .startCardstats { background-image: url(/stats.png); @@ -598,6 +645,10 @@ button:focus { padding-bottom: 10px; } +.whiteLink { + border-bottom: 1px #ffffff solid; +} + .flexi { display: inline-flex; } @@ -613,4 +664,397 @@ input::-webkit-inner-spin-button { input[type=number] { -moz-appearance: textfield; /* Firefox */ +} + +.shipNew { + margin-right: 5px; + background: linear-gradient(180deg, #4e0000, #400000); + border-radius: 20px; + padding: 3px 6px; + + p { + color: #fc8181 !important + } +} + + +.ship { + border-radius: 20px; + padding: 6px 12px; + width: max-content; + + p { + + font-weight: 600 !important; + } +} + +.ship_DELEGATED { + background: linear-gradient(180deg, #0f3d29, #0a2a1c); + color: #3cda94; +} + +.ship_ACCEPTED { + background: linear-gradient(180deg, #233d0f, #0a1b07); + color: #3cda4e; +} + +.ship_REJECTED { + background: linear-gradient(180deg, #4e0000, #330000); + color: #fc8181; +} + +.ship_COMPLETED { + background: linear-gradient(180deg, #4e3300, #372400); + color: #fcbb81; +} + +.ship_UNDELEGATION_REQUESTED { + color: rgb(252 248 129); + background: linear-gradient(rgb(78 71 0), rgb(36 39 0)); +} + +.ship_CANCELED { + color: rgb(219 219 219); + background: linear-gradient(rgb(59 59 59), rgb(36 36 36)); +} + +.ship_PROPOSED { + color: rgb(57 218 248); + background: linear-gradient(rgb(20 66 59), rgb(11 36 33)); +} + +.ship_DELEGATION_UI { + background: linear-gradient(180deg, #1e1b37, #131123); + color: #8c81fc; + +} + +.ship_MEW_WALLET { + background: linear-gradient(180deg, #144348, #0c2326); + color: #4bd9e9; +} + +.ship_ACTIVATE { + background: linear-gradient(180deg, #101635, #0a0e23); + color: #6f82f4; + +} + +.ship_PORTAL { + background: linear-gradient(180deg, #221d3c, #151225); + color: #9681fc; + // background: linear-gradient(180deg, #103324, #091d14); + // color: #81fcd1; +} + +.ship_SELF { + background: linear-gradient(180deg, #4e3300, #372400); + color: #fcbb81; +} + +.ship_OTHER { + background: linear-gradient(180deg, #3e1f3f, #2b152b); + color: #f681fc; +} + +.ship_ETHERSCAN { + background: linear-gradient(180deg, #1f203f, #15172b); + color: #8199fc; +} + +.shipFee { + margin-right: 5px; + background: linear-gradient(180deg, #1e4e00, #163800); + + border-radius: 20px; + padding: 3px 6px; + + p { + color: #cef79b !important; + } +} + +.shipId { + margin-right: 5px; + background: linear-gradient(180deg, #333333, #212121); + border-radius: 20px; + padding: 3px 6px; + + p { + color: #cecece !important; + } +} + + + +.shipNodes { + margin-right: 5px; + background: linear-gradient(180deg, #301f35, #1f1322); + border-radius: 20px; + padding: 3px 6px; + + p { + color: #e887ff !important + } +} + +.shipAddress { + margin-right: 5px; + background: #1f3533; + border-radius: 20px; + padding: 3px 12px; + + p { + color: #87e5ff !important; + } +} + +// md styling + +a, +.a { + color: #71ffb8 !important; +} + +.markdown { + hr { + border-color: $border-color; + width: 100%; + margin-block-start: 2em; + margin-block-end: 1em; + } + + ul, + p { + margin-block-start: 0.5em; + margin-block-end: 0.5em; + font-size: 0.9rem; + font-weight: 500; + } + + h2 { + margin-block-start: 0.7em; + margin-block-end: 0.7em; + } + + h3 { + margin-block-start: 1em; + margin-block-end: 0.7em; + } + + ul { + padding-inline-start: 20px; + } + + li { + list-style: none; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif !important; + -webkit-font-smoothing: antialiased; + font-size: 1rem; + letter-spacing: 0.02857em; + } + + li::marker { + width: 0; + display: none !important; + } + + code { + color: #fc8180; + } + + blockquote { + background: #2d2d2d; + border-radius: 25px; + padding: 10px 20px; + margin: 0; + } +} + + + + + +.page { + position: absolute; + left: 15px; + right: 15px; +} + +.page-enter { + opacity: 0; + transform: scale(1.05); +} + +.page-enter-active { + opacity: 1; + transform: scale(1); + transition: opacity 100ms, transform 100ms; +} + +.page-exit { + opacity: 1; + transform: scale(1); +} + +.page-exit-active { + opacity: 0; + transform: scale(0.95); + transition: opacity 100ms, transform 100ms; +} + + +.fade-enter { + opacity: 0; + transform: translate(0, 25px); + z-index: 1; +} + +.fade-enter.fade-enter-active { + opacity: 1; + transform: translate(0, 0); + + transition: opacity 150ms ease-out, transform 200ms ease; +} + +.fade-exit { + opacity: 1; + transform: translate(0, 0); +} + +.fade-exit.fade-exit-active { + opacity: 0; + transform: translate(0, 30px); + + transition: opacity 150ms ease-out, transform 200ms ease; +} + + +.sk-select { + .MuiSelect-select { + border: none !important; + // padding: 6px 20px 6px 6px !important; + } + + .MuiInputBase-root:after, + .MuiInputBase-root:before { + display: none !important; + } + + .MuiNativeSelect-standard { + background: none !important; + padding: 7px 25px 7px 15px !important; + } + + svg { + margin-right: 5px !important; + padding-right: 5px !important; + width: 1em !important; + height: 1em !important; + } +} + + +.trustedBadge { + color: #0095f6; +} + +.untrustedBadge { + color: #ffb817; +} + +.validatorCard { + height: 100% !important; + cursor: pointer; +} + + +.pOneLine { + overflow: hidden; + white-space: nowrap; + display: block; + text-overflow: ellipsis; +} + +.disabledCard { + filter: saturate(0) !important; + cursor: not-allowed !important; +} + +.selectedValidator { + background: rgb(12 41 24) !important; +} + +.pbott5 { + padding-bottom: 5px; +} + +.validatorIcon { + border-radius: 50%; + width: 70px; + height: 70px; +} + +.nestedSection { + border-left: 4px $border-color solid; + padding-left: 20px; + margin-left: 41px !important; +} + +.nestedSectionXs { + border-left: 3px $border-color solid; + padding-left: 20px; + margin-left: 20px !important; +} + +.borderVert { + border-left: 2px $border-color solid; +} + +.validatorIconDelegation { + margin-left: -15px; + margin-top: -15px; +} + + +.rotate-90 { + transition: transform 0.5s ease; + /* Adjust duration and easing as needed */ +} + +.rotate-90.active { + transform: rotate(90deg); +} + +.MuiSkeleton-root { + border-radius: 25px !important; + width: 100% !important; +} + + +.amountInput { + input { + padding: 3px 10px 0 5px !important + } +} + + +.warningMsg { + background-color: #43391d !important; + + p, + svg { + color: #f9e09c !important; + } + + a, + .a { + color: #ffc832 !important; + + p, + svg { + color: #ffc832 !important; + } + } } \ No newline at end of file diff --git a/src/Header.tsx b/src/Header.tsx index ffa931d..745d2ee 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -29,7 +29,7 @@ import { Link } from 'react-router-dom' import logo from './assets/skale_lg.svg' -import { cmn, cls, MetaportCore } from '@skalenetwork/metaport' +import { cmn, cls, type MetaportCore } from '@skalenetwork/metaport' import HelpZen from './components/HelpZen' import MoreMenu from './components/MoreMenu' diff --git a/src/Portal.tsx b/src/Portal.tsx index 207dd9f..2ea9b09 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -27,6 +27,7 @@ import CssBaseline from '@mui/material/CssBaseline' import Header from './Header' import SkDrawer from './SkDrawer' import Router from './Router' +import ScrollToTop from './components/ScrollToTop' import { useMetaportStore, useWagmiAccount, Debug, cls, cmn } from '@skalenetwork/metaport' @@ -37,9 +38,10 @@ export default function Portal() { return ( +
-
+
diff --git a/src/Router.tsx b/src/Router.tsx index bc0f985..17b566f 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -2,8 +2,23 @@ import './App.scss' import { useState, useEffect } from 'react' import { Helmet } from 'react-helmet' -import { useLocation, Routes, Route } from 'react-router-dom' -import { useMetaportStore, PROXY_ENDPOINTS, type MetaportState } from '@skalenetwork/metaport' +import { useLocation, Routes, Route, useSearchParams } from 'react-router-dom' +import { TransitionGroup, CSSTransition } from 'react-transition-group' + +import { useTheme } from '@mui/material/styles' +import useMediaQuery from '@mui/material/useMediaQuery' + +import { + useMetaportStore, + PROXY_ENDPOINTS, + type MetaportState, + useWagmiAccount, + useWagmiWalletClient, + useWagmiSwitchNetwork, + walletClientToSigner, + enforceNetwork, + type interfaces +} from '@skalenetwork/metaport' import Bridge from './pages/Bridge' import Faq from './pages/Faq' @@ -17,37 +32,75 @@ import History from './pages/History' import Portfolio from './pages/Portfolio' import Admin from './pages/Admin' import Start from './pages/Start' +import Staking from './pages/Staking' +import StakeValidator from './pages/StakeValidator' +import StakeAmount from './pages/StakeAmount' +import Validators from './pages/Validators' 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'; +import { BRIDGE_PAGES, MAINNET_CHAIN_NAME } from './core/constants' +import { type IValidator, type ISkaleContractsMap, type StakingInfoMap } from './core/interfaces' +import { getValidators } from './core/delegation/validators' +import Changelog from './pages/Changelog' +import { initContracts } from './core/contracts' +import { getStakingInfoMap } from './core/delegation/staking' export default function Router() { const location = useLocation() const currentUrl = `${window.location.origin}${location.pathname}${location.search}` + const theme = useTheme() + const isXs = useMediaQuery(theme.breakpoints.down('sm')) + const [schains, setSchains] = useState([]) const [termsAccepted, setTermsAccepted] = useState(false) + const [loadCalled, setLoadCalled] = useState(false) + const [sc, setSc] = useState(null) + const [validators, setValidators] = useState([]) + const [si, setSi] = useState({ 0: null, 1: null, 2: null }) + + const [customAddress, setCustomAddress] = useState(undefined) + const mpc = useMetaportStore((state: MetaportState) => state.mpc) const transfersHistory = useMetaportStore((state) => state.transfersHistory) const setTransfersHistory = useMetaportStore((state) => state.setTransfersHistory) + const { address } = useWagmiAccount() + const { data: walletClient } = useWagmiWalletClient() + const { switchNetworkAsync } = useWagmiSwitchNetwork() + + const [searchParams, _] = useSearchParams() const endpoint = PROXY_ENDPOINTS[mpc.config.skaleNetwork] useEffect(() => { setTransfersHistory(getHistoryFromStorage(mpc.config.skaleNetwork)) + initSkaleContracts() }, []) + useEffect(() => { + setCustomAddress((searchParams.get('_customAddress') as interfaces.AddressType) ?? undefined) + }, [location]) + useEffect(() => { if (transfersHistory.length !== 0) { setHistoryToStorage(transfersHistory, mpc.config.skaleNetwork) } }, [transfersHistory]) + async function getMainnetSigner() { + const { chainId } = await mpc.mainnet().provider.getNetwork() + await enforceNetwork( + chainId, + walletClient, + switchNetworkAsync!, + mpc.config.skaleNetwork, + MAINNET_CHAIN_NAME + ) + return walletClientToSigner(walletClient!) + } + async function loadSchains() { const response = await fetch(`https://${endpoint}/files/chains.json`) const chainsJson = await response.json() @@ -58,6 +111,23 @@ export default function Router() { setSchains(schains) } + async function initSkaleContracts() { + setLoadCalled(true) + if (loadCalled) return + setSc(await initContracts(mpc)) + } + + async function loadValidators() { + if (!sc) return + const validatorsData = await getValidators(sc.validatorService) + setValidators(validatorsData) + } + + async function loadStakingInfo() { + if (!sc) return + setSi(await getStakingInfoMap(sc, customAddress ?? address)) + } + function isBridgePage(): boolean { return BRIDGE_PAGES.some( (pathname) => location.pathname === pathname || location.pathname.includes(pathname) @@ -75,38 +145,100 @@ export default function Router() { - - } /> - } /> - - } /> - - } /> - } - /> - - } - /> - - } /> - - } /> - - } /> - - } /> - } /> - - {pricingLaunchTsReached(mpc.config.skaleNetwork) ? ( - - } /> - - ) : null} - + + + + } /> + } /> + + } /> + + } /> + } + /> + + } + /> + + } /> + + } /> + + } /> + + } /> + } /> + } /> + + + } /> + + + + } + /> + + } + /> + + + } + /> + + } + /> + + + +
) } diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index 91d84cd..e1b080c 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -16,11 +16,11 @@ import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontal 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 PieChartOutlineOutlinedIcon from '@mui/icons-material/PieChartOutlineOutlined' import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' import DonutLargeRoundedIcon from '@mui/icons-material/DonutLargeRounded' import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined' +import GroupOutlinedIcon from '@mui/icons-material/GroupOutlined' import { DUNE_SKALE_URL } from './core/constants' @@ -85,32 +85,6 @@ export default function SkDrawer() { - {/* - - - - - - - - - */} - {/* - - - - - - - - - */}

Network

@@ -129,19 +103,35 @@ export default function SkDrawer() { - {/* - + + + + + + + +
+

NEW

+
+
+ +
+ + - + - + - */} +
- + diff --git a/src/_variables.scss b/src/_variables.scss index 7586990..305ea28 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -6,4 +6,4 @@ $sk-gray-background-color: rgba(161, 161, 161, 0.2); $sk-btn-height: 47px; -$border-color: rgba(131, 131, 131, 0.35); \ No newline at end of file +$border-color: rgba(131, 131, 131, 0.20); \ No newline at end of file diff --git a/src/assets/validators/v1.png b/src/assets/validators/v1.png new file mode 100644 index 0000000..f17a426 Binary files /dev/null and b/src/assets/validators/v1.png differ diff --git a/src/assets/validators/v10.webp b/src/assets/validators/v10.webp new file mode 100644 index 0000000..9ea42a6 Binary files /dev/null and b/src/assets/validators/v10.webp differ diff --git a/src/assets/validators/v11.svg b/src/assets/validators/v11.svg new file mode 100644 index 0000000..102c5c9 --- /dev/null +++ b/src/assets/validators/v11.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/validators/v12.jpg b/src/assets/validators/v12.jpg new file mode 100644 index 0000000..6f6eedc Binary files /dev/null and b/src/assets/validators/v12.jpg differ diff --git a/src/assets/validators/v13.jpg b/src/assets/validators/v13.jpg new file mode 100644 index 0000000..a9e5448 Binary files /dev/null and b/src/assets/validators/v13.jpg differ diff --git a/src/assets/validators/v14.jpeg b/src/assets/validators/v14.jpeg new file mode 100644 index 0000000..7195c01 Binary files /dev/null and b/src/assets/validators/v14.jpeg differ diff --git a/src/assets/validators/v15.jpg b/src/assets/validators/v15.jpg new file mode 100644 index 0000000..d2b4323 Binary files /dev/null and b/src/assets/validators/v15.jpg differ diff --git a/src/assets/validators/v16.jpeg b/src/assets/validators/v16.jpeg new file mode 100644 index 0000000..1f7cf29 Binary files /dev/null and b/src/assets/validators/v16.jpeg differ diff --git a/src/assets/validators/v17.jpg b/src/assets/validators/v17.jpg new file mode 100644 index 0000000..00aba98 Binary files /dev/null and b/src/assets/validators/v17.jpg differ diff --git a/src/assets/validators/v18.jpeg b/src/assets/validators/v18.jpeg new file mode 100644 index 0000000..0287729 Binary files /dev/null and b/src/assets/validators/v18.jpeg differ diff --git a/src/assets/validators/v19.webp b/src/assets/validators/v19.webp new file mode 100644 index 0000000..d0a7eeb Binary files /dev/null and b/src/assets/validators/v19.webp differ diff --git a/src/assets/validators/v2.png b/src/assets/validators/v2.png new file mode 100644 index 0000000..6ee95fb Binary files /dev/null and b/src/assets/validators/v2.png differ diff --git a/src/assets/validators/v20.jpeg b/src/assets/validators/v20.jpeg new file mode 100644 index 0000000..b2d4457 Binary files /dev/null and b/src/assets/validators/v20.jpeg differ diff --git a/src/assets/validators/v21.png b/src/assets/validators/v21.png new file mode 100644 index 0000000..6acab63 Binary files /dev/null and b/src/assets/validators/v21.png differ diff --git a/src/assets/validators/v23.png b/src/assets/validators/v23.png new file mode 100644 index 0000000..bc3dd7a Binary files /dev/null and b/src/assets/validators/v23.png differ diff --git a/src/assets/validators/v3.webp b/src/assets/validators/v3.webp new file mode 100644 index 0000000..7da39c2 Binary files /dev/null and b/src/assets/validators/v3.webp differ diff --git a/src/assets/validators/v30.jpg b/src/assets/validators/v30.jpg new file mode 100644 index 0000000..39fd84b Binary files /dev/null and b/src/assets/validators/v30.jpg differ diff --git a/src/assets/validators/v31.jpg b/src/assets/validators/v31.jpg new file mode 100644 index 0000000..cbe1b33 Binary files /dev/null and b/src/assets/validators/v31.jpg differ diff --git a/src/assets/validators/v32.jpeg b/src/assets/validators/v32.jpeg new file mode 100644 index 0000000..c779b23 Binary files /dev/null and b/src/assets/validators/v32.jpeg differ diff --git a/src/assets/validators/v33.jpg b/src/assets/validators/v33.jpg new file mode 100644 index 0000000..59ec34e Binary files /dev/null and b/src/assets/validators/v33.jpg differ diff --git a/src/assets/validators/v34.jpeg b/src/assets/validators/v34.jpeg new file mode 100644 index 0000000..7195c01 Binary files /dev/null and b/src/assets/validators/v34.jpeg differ diff --git a/src/assets/validators/v35.jpeg b/src/assets/validators/v35.jpeg new file mode 100644 index 0000000..7195c01 Binary files /dev/null and b/src/assets/validators/v35.jpeg differ diff --git a/src/assets/validators/v36.webp b/src/assets/validators/v36.webp new file mode 100644 index 0000000..672dcd7 Binary files /dev/null and b/src/assets/validators/v36.webp differ diff --git a/src/assets/validators/v37.jpg b/src/assets/validators/v37.jpg new file mode 100644 index 0000000..a9e5448 Binary files /dev/null and b/src/assets/validators/v37.jpg differ diff --git a/src/assets/validators/v39.webp b/src/assets/validators/v39.webp new file mode 100644 index 0000000..8816326 Binary files /dev/null and b/src/assets/validators/v39.webp differ diff --git a/src/assets/validators/v4.webp b/src/assets/validators/v4.webp new file mode 100644 index 0000000..672dcd7 Binary files /dev/null and b/src/assets/validators/v4.webp differ diff --git a/src/assets/validators/v40.jpeg b/src/assets/validators/v40.jpeg new file mode 100644 index 0000000..c86a7af Binary files /dev/null and b/src/assets/validators/v40.jpeg differ diff --git a/src/assets/validators/v41.png b/src/assets/validators/v41.png new file mode 100644 index 0000000..6b83ed1 Binary files /dev/null and b/src/assets/validators/v41.png differ diff --git a/src/assets/validators/v42.webp b/src/assets/validators/v42.webp new file mode 100644 index 0000000..7da39c2 Binary files /dev/null and b/src/assets/validators/v42.webp differ diff --git a/src/assets/validators/v43.webp b/src/assets/validators/v43.webp new file mode 100644 index 0000000..9ea42a6 Binary files /dev/null and b/src/assets/validators/v43.webp differ diff --git a/src/assets/validators/v45.png b/src/assets/validators/v45.png new file mode 100644 index 0000000..6acab63 Binary files /dev/null and b/src/assets/validators/v45.png differ diff --git a/src/assets/validators/v46.jpeg b/src/assets/validators/v46.jpeg new file mode 100644 index 0000000..1f7cf29 Binary files /dev/null and b/src/assets/validators/v46.jpeg differ diff --git a/src/assets/validators/v47.jpg b/src/assets/validators/v47.jpg new file mode 100644 index 0000000..d2b4323 Binary files /dev/null and b/src/assets/validators/v47.jpg differ diff --git a/src/assets/validators/v48.png b/src/assets/validators/v48.png new file mode 100644 index 0000000..bc3dd7a Binary files /dev/null and b/src/assets/validators/v48.png differ diff --git a/src/assets/validators/v49.jpg b/src/assets/validators/v49.jpg new file mode 100644 index 0000000..00aba98 Binary files /dev/null and b/src/assets/validators/v49.jpg differ diff --git a/src/assets/validators/v5.png b/src/assets/validators/v5.png new file mode 100644 index 0000000..4ec5523 Binary files /dev/null and b/src/assets/validators/v5.png differ diff --git a/src/assets/validators/v50.svg b/src/assets/validators/v50.svg new file mode 100644 index 0000000..102c5c9 --- /dev/null +++ b/src/assets/validators/v50.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/validators/v51.jpg b/src/assets/validators/v51.jpg new file mode 100644 index 0000000..6f6eedc Binary files /dev/null and b/src/assets/validators/v51.jpg differ diff --git a/src/assets/validators/v52.webp b/src/assets/validators/v52.webp new file mode 100644 index 0000000..d0a7eeb Binary files /dev/null and b/src/assets/validators/v52.webp differ diff --git a/src/assets/validators/v53.webp b/src/assets/validators/v53.webp new file mode 100644 index 0000000..1b2c723 Binary files /dev/null and b/src/assets/validators/v53.webp differ diff --git a/src/assets/validators/v54.webp b/src/assets/validators/v54.webp new file mode 100644 index 0000000..1b2c723 Binary files /dev/null and b/src/assets/validators/v54.webp differ diff --git a/src/assets/validators/v55.jpg b/src/assets/validators/v55.jpg new file mode 100644 index 0000000..a1a029b Binary files /dev/null and b/src/assets/validators/v55.jpg differ diff --git a/src/assets/validators/v56.png b/src/assets/validators/v56.png new file mode 100644 index 0000000..86a0bb0 Binary files /dev/null and b/src/assets/validators/v56.png differ diff --git a/src/assets/validators/v57.jpeg b/src/assets/validators/v57.jpeg new file mode 100644 index 0000000..400dce7 Binary files /dev/null and b/src/assets/validators/v57.jpeg differ diff --git a/src/assets/validators/v58.jpg b/src/assets/validators/v58.jpg new file mode 100644 index 0000000..4f6ea45 Binary files /dev/null and b/src/assets/validators/v58.jpg differ diff --git a/src/assets/validators/v6.jpg b/src/assets/validators/v6.jpg new file mode 100644 index 0000000..c9d0d2b Binary files /dev/null and b/src/assets/validators/v6.jpg differ diff --git a/src/assets/validators/v61.png b/src/assets/validators/v61.png new file mode 100644 index 0000000..bc2fd40 Binary files /dev/null and b/src/assets/validators/v61.png differ diff --git a/src/assets/validators/v63.png b/src/assets/validators/v63.png new file mode 100644 index 0000000..0496335 Binary files /dev/null and b/src/assets/validators/v63.png differ diff --git a/src/assets/validators/v64.jpeg b/src/assets/validators/v64.jpeg new file mode 100644 index 0000000..7195c01 Binary files /dev/null and b/src/assets/validators/v64.jpeg differ diff --git a/src/assets/validators/v66.jpg b/src/assets/validators/v66.jpg new file mode 100644 index 0000000..6a299e9 Binary files /dev/null and b/src/assets/validators/v66.jpg differ diff --git a/src/assets/validators/v67.png b/src/assets/validators/v67.png new file mode 100644 index 0000000..3b86eff Binary files /dev/null and b/src/assets/validators/v67.png differ diff --git a/src/assets/validators/v68.jpeg b/src/assets/validators/v68.jpeg new file mode 100644 index 0000000..c779b23 Binary files /dev/null and b/src/assets/validators/v68.jpeg differ diff --git a/src/assets/validators/v7.png b/src/assets/validators/v7.png new file mode 100644 index 0000000..6b83ed1 Binary files /dev/null and b/src/assets/validators/v7.png differ diff --git a/src/assets/validators/v8.webp b/src/assets/validators/v8.webp new file mode 100644 index 0000000..8816326 Binary files /dev/null and b/src/assets/validators/v8.webp differ diff --git a/src/assets/validators/v9.jpg b/src/assets/validators/v9.jpg new file mode 100644 index 0000000..1ebe316 Binary files /dev/null and b/src/assets/validators/v9.jpg differ diff --git a/src/components/AccordionLink.tsx b/src/components/AccordionLink.tsx index 0afd750..41758e8 100644 --- a/src/components/AccordionLink.tsx +++ b/src/components/AccordionLink.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2023-Present */ -import { ReactElement } from 'react' +import { type ReactElement } from 'react' import ButtonBase from '@mui/material/ButtonBase' import AddCircleRoundedIcon from '@mui/icons-material/AddCircleRounded' @@ -41,7 +41,7 @@ export default function AccordionLink(props: {
{props.icon ? ( -
+
{props.icon}
) : null} diff --git a/src/components/AccordionSection.tsx b/src/components/AccordionSection.tsx index ae05995..8f4b5d0 100644 --- a/src/components/AccordionSection.tsx +++ b/src/components/AccordionSection.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2023-Present */ -import { ReactElement } from 'react' +import { type ReactElement, useState } from 'react' import Collapse from '@mui/material/Collapse' import ButtonBase from '@mui/material/ButtonBase' @@ -31,38 +31,56 @@ import RemoveCircleRoundedIcon from '@mui/icons-material/RemoveCircleRounded' import { cmn, cls, styles } from '@skalenetwork/metaport' export default function AccordionSection(props: { - handleChange: (panel: string | false) => void - expanded: string | false - panel: string title: string + handleChange?: (panel: string | false) => void + expanded?: string | false + panel?: string subtitle?: string - children: ReactElement | ReactElement[] + children?: ReactElement | ReactElement[] | null icon?: ReactElement className?: string + expandedByDefault?: boolean + marg?: boolean }) { + const marg = props.marg ?? true + + const [expandedInternal, setExpandedInternal] = useState( + props.expandedByDefault ? 'panel1' : false + ) + + function handleChangeInternal(panel: string | false) { + setExpandedInternal(expanded && panel === expanded ? false : panel) + } + + const handleChange = props.handleChange ?? handleChangeInternal + const expanded = props.expanded ?? expandedInternal + const panel = props.panel ?? 'panel1' + return (
props.handleChange(props.panel)} + onClick={() => { + handleChange(panel) + }} className={cls(cmn.fullWidth, cmn.flex, cmn.pleft, cmn.bordRad)} >
{props.icon ? ( -
+
{props.icon}
) : null}

{props.title}

-

{props.subtitle}

- {props.expanded === props.panel ? ( +

{props.subtitle}

+ {expanded === panel ? ( ) : ( )}
- -
{props.children}
+ +
{props.children}
) diff --git a/src/components/AccountMenu.tsx b/src/components/AccountMenu.tsx index fd8420a..4d50e57 100644 --- a/src/components/AccountMenu.tsx +++ b/src/components/AccountMenu.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2023-Present */ -import { useState, MouseEvent } from 'react' +import { useState, type MouseEvent } from 'react' import { Link } from 'react-router-dom' import Jazzicon, { jsNumberForAddress } from 'react-jazzicon' @@ -146,11 +146,6 @@ export default function AccountMenu(props: any) { Transfers history - {/* - - Assets overview - - */}
. + */ + +/** + * @file AppCard.tsx + * @copyright SKALE Labs 2022-Present + */ + +import { Link } from 'react-router-dom' +import { + cmn, + cls, + chainBg, + getChainAlias, + CHAINS_META, + ChainIcon, + type interfaces +} from '@skalenetwork/metaport' + +import Button from '@mui/material/Button' + +export default function AppCard(props: { + skaleNetwork: interfaces.SkaleNetwork + schainName: string + appName: string +}) { + function getChainShortAlias(meta: interfaces.ChainsMetadataMap, name: string): string { + return meta[name]?.shortAlias !== undefined ? meta[name].shortAlias! : name + } + + const chainsMeta: interfaces.ChainsMetadataMap = CHAINS_META[props.skaleNetwork] + + const shortAlias = getChainShortAlias(chainsMeta, props.schainName) + + return ( +
+
+
+ +
+
+ +
+
+ +
+ + + +
+
+
+
+ ) +} diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx new file mode 100644 index 0000000..86ba1b0 --- /dev/null +++ b/src/components/Breadcrumbs.tsx @@ -0,0 +1,58 @@ +/** + * @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 Breadcrumbs.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { type ReactElement } from 'react' +import Button from '@mui/material/Button' +import { Link } from 'react-router-dom' +import { cmn, cls } from '@skalenetwork/metaport' + +interface Section { + icon: ReactElement + text: string + url?: string +} + +export default function Breadcrumbs(props: { sections: Section[]; className?: string }) { + return ( +
+ {props.sections.map((section: Section, index) => ( +
+ {section.url ? ( + + + + ) : ( + + )} + {index + 1 !== props.sections.length ?

|

: null} +
+ ))} +
+ ) +} diff --git a/src/components/CategoryBadge.tsx b/src/components/CategoryBadge.tsx index b9c35e6..3580452 100644 --- a/src/components/CategoryBadge.tsx +++ b/src/components/CategoryBadge.tsx @@ -62,7 +62,7 @@ function isStringArray(value: any): value is string[] { export default function CategoryBadge(props: { category: string; className?: string }) { function getCategoryIcon(category: string) { - return CATEGORY_ICON[category] ?? CATEGORY_ICON['other'] + return CATEGORY_ICON[category] ?? CATEGORY_ICON.other } return ( diff --git a/src/components/CategorySection.tsx b/src/components/CategorySection.tsx index f1d68ed..f839650 100644 --- a/src/components/CategorySection.tsx +++ b/src/components/CategorySection.tsx @@ -25,7 +25,7 @@ import Box from '@mui/material/Box' import Grid from '@mui/material/Grid' import ChainCard from './ChainCard' -import { cls, cmn, interfaces, styles, getChainAlias } from '@skalenetwork/metaport' +import { cls, cmn, type interfaces, styles, getChainAlias } from '@skalenetwork/metaport' import { CATEGORY_ICON } from './CategoryBadge' diff --git a/src/components/ChainAccordion.tsx b/src/components/ChainAccordion.tsx index 3ce0804..5c49e82 100644 --- a/src/components/ChainAccordion.tsx +++ b/src/components/ChainAccordion.tsx @@ -37,7 +37,7 @@ import { BASE_EXPLORER_URLS, type MetaportCore, SkPaper, - interfaces + type interfaces } from '@skalenetwork/metaport' import VerifiedContracts from './VerifiedContracts' @@ -54,7 +54,6 @@ import { HTTPS_PREFIX, WSS_PREFIX } from '../core/chain' -import { pricingLaunchTsReached } from '../core/paymaster' export default function ChainAccordion(props: { schainName: string @@ -192,15 +191,11 @@ export default function ChainAccordion(props: { explorerUrl={explorerUrl} /> - {pricingLaunchTsReached(network) ? ( - } - url={`/admin/${props.schainName}`} - /> - ) : ( -
- )} + } + url={`/admin/${props.schainName}`} + /> ) } diff --git a/src/components/ChainCard.tsx b/src/components/ChainCard.tsx index 7378243..125ed41 100644 --- a/src/components/ChainCard.tsx +++ b/src/components/ChainCard.tsx @@ -38,7 +38,7 @@ import { MAINNET_CHAIN_LOGOS } from '../core/constants' export default function ChainCard(props: { skaleNetwork: interfaces.SkaleNetwork; schain: any[] }) { function getChainShortAlias(meta: interfaces.ChainsMetadataMap, name: string): string { - return meta[name] && meta[name].shortAlias !== undefined ? meta[name].shortAlias! : name + return meta[name]?.shortAlias !== undefined ? meta[name].shortAlias! : name } const chainsMeta: interfaces.ChainsMetadataMap = CHAINS_META[props.skaleNetwork] diff --git a/src/components/ChainCategories.tsx b/src/components/ChainCategories.tsx index 4919d6a..b643b88 100644 --- a/src/components/ChainCategories.tsx +++ b/src/components/ChainCategories.tsx @@ -21,11 +21,12 @@ * @copyright SKALE Labs 2023-Present */ -import Button from '@mui/material/Button' import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' import { cmn, cls } from '@skalenetwork/metaport' +import LinkRoundedIcon from '@mui/icons-material/LinkRounded' + import CategoryBadge, { isString } from './CategoryBadge' -import { Link } from 'react-router-dom' +import Breadcrumbs from './Breadcrumbs' export default function ChainCategories(props: { category: string | string[] | undefined @@ -34,29 +35,29 @@ export default function ChainCategories(props: { if (!props.category) return return (
-
-
- - - -
-

|

-
-
-

{props.alias}

-
-
+
+ , + url: '/chains' + }, + { + text: props.alias, + icon: + } + ]} + />
+
{isString(props.category) ? ( ) : ( props.category.map((cat: string) => ( - + )) )}
diff --git a/src/components/ChainLogo.tsx b/src/components/ChainLogo.tsx index a314bc2..ec3b75c 100644 --- a/src/components/ChainLogo.tsx +++ b/src/components/ChainLogo.tsx @@ -25,7 +25,7 @@ import Jazzicon from 'react-jazzicon' function hashCode(str: string) { let hash = 0 - for (var i = 0; i < str.length; i++) { + for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash) } return hash @@ -57,9 +57,9 @@ export default function ChainLogo(props: { iconPath = iconPath.replace(/-([a-z])/g, (_, g) => g.toUpperCase()) - let pngPath = iconPath + '.png' - let gifPath = iconPath + '.gif' - let svgPath = iconPath + '.svg' + const pngPath = iconPath + '.png' + const gifPath = iconPath + '.gif' + const svgPath = iconPath + '.svg' if (props.logos[pngPath]) { iconPath = pngPath } else if (props.logos[gifPath]) { diff --git a/src/components/CopySurface.tsx b/src/components/CopySurface.tsx index 6d80ae1..7974d9f 100644 --- a/src/components/CopySurface.tsx +++ b/src/components/CopySurface.tsx @@ -29,7 +29,7 @@ import ButtonBase from '@mui/material/ButtonBase' import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded' import ContentCopyIcon from '@mui/icons-material/ContentCopy' -import { cmn, cls, styles, TokenIcon, interfaces } from '@skalenetwork/metaport' +import { cmn, cls, styles, TokenIcon, type interfaces } from '@skalenetwork/metaport' import { DEFAULT_ERC20_DECIMALS } from '../core/constants' @@ -50,7 +50,9 @@ export default function CopySurface(props: { const timer = setTimeout(() => { setCopied(false) }, 1000) - return () => clearTimeout(timer) + return () => { + clearTimeout(timer) + } } }, [copied]) diff --git a/src/components/ErrorTile.tsx b/src/components/ErrorTile.tsx new file mode 100644 index 0000000..37b99a8 --- /dev/null +++ b/src/components/ErrorTile.tsx @@ -0,0 +1,61 @@ +/** + * @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 ErrorTile.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cls, cmn } from '@skalenetwork/metaport' + +import Button from '@mui/material/Button' +import Collapse from '@mui/material/Collapse' +import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded' + +import Tile from './Tile' + +export default function ErrorTile(props: { + errorMsg: string | undefined + setErrorMsg: (errorMsg: string | undefined) => void + className?: string | undefined +}) { + return ( + + } + color="error" + grow + children={ + + } + /> + + ) +} diff --git a/src/components/Headline.tsx b/src/components/Headline.tsx new file mode 100644 index 0000000..b434492 --- /dev/null +++ b/src/components/Headline.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 Headline.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cls, cmn, styles } from '@skalenetwork/metaport' +import { type ReactElement } from 'react' + +export default function Headline(props: { + icon: ReactElement | undefined + text: string + className?: string | undefined +}) { + return ( +
+
+ {props.icon} +
+

{props.text}

+
+ ) +} diff --git a/src/components/HelpZen.tsx b/src/components/HelpZen.tsx index 3db55ac..0f8530a 100644 --- a/src/components/HelpZen.tsx +++ b/src/components/HelpZen.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom' -import { useEffect, useState, MouseEvent } from 'react' +import { useEffect, useState, type MouseEvent } from 'react' import Box from '@mui/material/Box' import Tooltip from '@mui/material/Tooltip' import IconButton from '@mui/material/IconButton' diff --git a/src/components/Message.tsx b/src/components/Message.tsx index f030b96..cf958d5 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -25,35 +25,55 @@ import { type ReactElement, useState } from 'react' import IconButton from '@mui/material/IconButton' import CloseRoundedIcon from '@mui/icons-material/CloseRounded' import Collapse from '@mui/material/Collapse' - +import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' import { SkPaper, cls, cmn } from '@skalenetwork/metaport' +import { Link } from 'react-router-dom' export default function Message(props: { - text: string + text: string | null + linkText?: string + link?: string icon: ReactElement className?: string | undefined showOnLoad?: boolean | undefined + type?: 'warning' | 'info' }) { + const type = props.type ?? 'info' const [show, setShow] = useState(true) return ( - +
{props.icon}
-

- {props.text} -

+ {props.text ? ( +

+ {props.text} +

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

{props.linkText}

+ + +
+ ) : null} + +
{ setShow(false) }} - className={cls(cmn.paperGrey, cmn.pPrim, cmn.mleft10)} + className={cls(cmn.paperGrey, cmn.mleft10)} > diff --git a/src/components/MoreMenu.tsx b/src/components/MoreMenu.tsx index cf7c1cf..b5d71b8 100644 --- a/src/components/MoreMenu.tsx +++ b/src/components/MoreMenu.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2023-Present */ -import { useState, MouseEvent } from 'react' +import { useState, type MouseEvent } from 'react' import Box from '@mui/material/Box' import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' @@ -29,6 +29,7 @@ import IconButton from '@mui/material/IconButton' import Tooltip from '@mui/material/Tooltip' import MoreVertIcon from '@mui/icons-material/MoreVert' import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward' +import FormatListBulletedRoundedIcon from '@mui/icons-material/FormatListBulletedRounded' import { Link } from 'react-router-dom' @@ -108,6 +109,11 @@ export default function MoreMenu() { Terms of service + + + Changelog + +
diff --git a/src/components/NetworkSwitch.tsx b/src/components/NetworkSwitch.tsx index 109544c..f746f05 100644 --- a/src/components/NetworkSwitch.tsx +++ b/src/components/NetworkSwitch.tsx @@ -31,7 +31,7 @@ import Button from '@mui/material/Button' import SensorsRoundedIcon from '@mui/icons-material/SensorsRounded' import ChangeCircleRoundedIcon from '@mui/icons-material/ChangeCircleRounded' -import { cls, styles, cmn, MetaportCore } from '@skalenetwork/metaport' +import { cls, styles, cmn, type MetaportCore } from '@skalenetwork/metaport' import { PORTAL_URLS } from '../core/constants' diff --git a/src/components/PageCard.tsx b/src/components/PageCard.tsx index 31dd181..170585a 100644 --- a/src/components/PageCard.tsx +++ b/src/components/PageCard.tsx @@ -36,7 +36,7 @@ export default function PageCard(props: { name: string; icon: any; description:

{props.name}

diff --git a/src/components/Paymaster.tsx b/src/components/Paymaster.tsx index acefb60..34e7a48 100644 --- a/src/components/Paymaster.tsx +++ b/src/components/Paymaster.tsx @@ -24,6 +24,7 @@ import { Contract, id } from 'ethers' import { useState, useEffect } from 'react' import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded' +import MonetizationOnRoundedIcon from '@mui/icons-material/MonetizationOnRounded' import { cmn, @@ -51,12 +52,13 @@ import { initPaymaster, getPaymasterChain, getPaymasterInfo, - PaymasterInfo, + type PaymasterInfo, DEFAULT_PAYMASTER_INFO, getPaymasterAddress, getPaymasterAbi, divideBigInts } from '../core/paymaster' +import Headline from './Headline' const DEFAULT_TOPUP_PERIOD = 3 const APPROVE_MULTIPLIER = 2n @@ -83,7 +85,9 @@ export default function Paymaster(props: { mpc: MetaportCore; name: string }) { useEffect(() => { loadPaymasterInfo() const intervalId = setInterval(loadPaymasterInfo, DEFAULT_UPDATE_INTERVAL_MS) - return () => clearInterval(intervalId) + return () => { + clearInterval(intervalId) + } }, [sklToken, address]) async function loadPaymasterInfo() { @@ -177,13 +181,8 @@ export default function Paymaster(props: { mpc: MetaportCore; name: string }) { return (
-

- Pricing info -

-

- Top-up chain -

+ } /> {!address ? ( ) : ( diff --git a/src/components/PricingInfo.tsx b/src/components/PricingInfo.tsx index 8c430cb..c7c5640 100644 --- a/src/components/PricingInfo.tsx +++ b/src/components/PricingInfo.tsx @@ -26,7 +26,8 @@ import AvTimerRoundedIcon from '@mui/icons-material/AvTimerRounded' import { cmn, TokenIcon, fromWei } from '@skalenetwork/metaport' -import { truncateDecimals, PaymasterInfo, DueDateStatus, divideBigInts } from '../core/paymaster' +import { type PaymasterInfo, type DueDateStatus, divideBigInts } from '../core/paymaster' +import { truncateDecimals } from '../core/helper' import { daysBetweenNowAndTimestamp, monthsBetweenNowAndTimestamp, diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index 2f0a810..e3bbb3e 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -39,7 +39,7 @@ import { SkPaper, getChainAlias, chainBg, - interfaces + type interfaces } from '@skalenetwork/metaport' import SkStack from './SkStack' @@ -68,6 +68,12 @@ export default function SchainDetails(props: { const network = props.mpc.config.skaleNetwork + // const [expanded, setExpanded] = useState('panel1') + + // function handleChange(panel: string | false) { + // setExpanded(expanded && panel === expanded ? false : panel) + // } + const networkParams = { chainId, chainName: '[S]' + getChainAlias(props.mpc.config.skaleNetwork, props.schainName), diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..abd5ec5 --- /dev/null +++ b/src/components/ScrollToTop.tsx @@ -0,0 +1,13 @@ +import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' + +export default function ScrollToTop() { + const { pathname } = useLocation() + useEffect(() => { + const appContentScrollId = document.getElementById('appContentScroll') + if (appContentScrollId) { + appContentScrollId.scroll({ top: 0, behavior: 'auto' }) + } + }, [pathname]) + return null +} diff --git a/src/components/SkBtn.tsx b/src/components/SkBtn.tsx new file mode 100644 index 0000000..6b7f5d6 --- /dev/null +++ b/src/components/SkBtn.tsx @@ -0,0 +1,67 @@ +/** + * @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 SkBtn.tsx + * @copyright SKALE Labs 2024-Present + */ + +import Button from '@mui/material/Button' +import LoadingButton from '@mui/lab/LoadingButton' + +import { cls } from '@skalenetwork/metaport' + +export default function SkBtn(props: { + text: string + disabled?: boolean + onClick?: () => void + loading?: boolean + className?: string | undefined + color?: 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning' | undefined + size?: 'sm' | 'md' + variant?: 'contained' | 'outlined' | 'text' +}) { + const size = props.size ?? 'md' + return props.loading ? ( + + {props.text} + + ) : ( + + ) +} diff --git a/src/components/SkStack.tsx b/src/components/SkStack.tsx index 5ff9d0c..95728be 100644 --- a/src/components/SkStack.tsx +++ b/src/components/SkStack.tsx @@ -21,16 +21,11 @@ * @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[] -}) { +export default function SkStack(props: { className?: string; children?: any }) { return ( { setCopied(false) }, 1000) - return () => clearTimeout(timer) + return () => { + clearTimeout(timer) + } } }, [copied]) diff --git a/src/components/Tile.tsx b/src/components/Tile.tsx index 947cc75..c95735a 100644 --- a/src/components/Tile.tsx +++ b/src/components/Tile.tsx @@ -21,16 +21,22 @@ * @copyright SKALE Labs 2023-Present */ -import { ReactElement } from 'react' +import { useState, useEffect, type ReactElement } from 'react' +import { CopyToClipboard } from 'react-copy-to-clipboard' import { useTheme } from '@mui/material/styles' -import LinearProgress from '@mui/material/LinearProgress' +import useMediaQuery from '@mui/material/useMediaQuery' import { cmn, cls, styles } from '@skalenetwork/metaport' -import { DueDateStatus } from '../core/paymaster' +import LinearProgress from '@mui/material/LinearProgress' +import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded' + +import { type DueDateStatus } from '../core/paymaster' +import { Skeleton, Tooltip } from '@mui/material' +import SkStack from './SkStack' export default function Tile(props: { text?: string - value?: string + value?: string | null textRi?: string icon?: ReactElement className?: string @@ -39,55 +45,133 @@ export default function Tile(props: { progressColor?: DueDateStatus progress?: number children?: ReactElement | ReactElement[] + childrenRi?: ReactElement | ReactElement[] + size?: 'lg' | 'md' + textColor?: string + disabled?: boolean | null + ri?: boolean + copy?: string | undefined + transparent?: boolean }) { const theme = useTheme() - const color = props.color ? theme.palette[props.color].main : 'rgba(0, 0, 0, 0.6)' + let color = props.color ? theme.palette[props.color].main : 'rgba(0, 0, 0, 0.6)' + color = props.transparent ? 'transparent' : color + const size = props.size ?? 'lg' + + const [copied, setCopied] = useState(false) + const isXs = useMediaQuery(theme.breakpoints.down('sm')) + + const handleClick = () => { + setCopied(true) + } + + useEffect(() => { + if (copied) { + const timer = setTimeout(() => { + setCopied(false) + }, 1000) + return () => { + clearTimeout(timer) + } + } + }, [copied]) + + const value = ( +

+ {props.value} +

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

+ {props.text} +

+ {props.textRi ? ( +

+ {props.textRi} +

+ ) : null} +
) : null} -

{props.text}

-

{props.textRi}

+
+ {props.ri ?
: null} + {props.value && props.copy ? ( + +
+ + {value} + +
+
+ ) : null} + {props.value && !props.copy ? value : null} + {!props.value && !props.children ? ( + + ) : null} + {props.progress ? ( + + ) : null} +
- ) : null} -
- {props.value ? ( -

- {props.value} -

- ) : null} - {props.progress ? ( - - ) : null} -
+ {props.childrenRi} +
{props.children}
) diff --git a/src/components/TokenSurface.tsx b/src/components/TokenSurface.tsx index e3e888c..4a2b339 100644 --- a/src/components/TokenSurface.tsx +++ b/src/components/TokenSurface.tsx @@ -28,7 +28,7 @@ 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 { cmn, cls, styles, TokenIcon, ChainIcon, type interfaces } from '@skalenetwork/metaport' import { DEFAULT_ERC20_DECIMALS } from '../core/constants' @@ -51,7 +51,9 @@ export default function TokenSurface(props: { const timer = setTimeout(() => { setCopied(false) }, 1000) - return () => clearTimeout(timer) + return () => { + clearTimeout(timer) + } } }, [copied]) diff --git a/src/components/Topup.tsx b/src/components/Topup.tsx index 5ada203..89d34a6 100644 --- a/src/components/Topup.tsx +++ b/src/components/Topup.tsx @@ -36,7 +36,8 @@ import SkStack from './SkStack' import MonthSelector from './MonthSelector' import Loader from './Loader' -import { PaymasterInfo, divideBigInts, truncateDecimals } from '../core/paymaster' +import { type PaymasterInfo, divideBigInts } from '../core/paymaster' +import { truncateDecimals } from '../core/helper' import { DEFAULT_ERC20_DECIMALS } from '../core/constants' import { formatTimePeriod, monthsBetweenNowAndTimestamp } from '../core/timeHelper' @@ -117,7 +118,9 @@ export default function Topup(props: { children={ +
+ } + /> + + + + + {loading ? ( + + Staking SKL + + ) : ( + + )} +
+ ) +} diff --git a/src/components/delegation/Delegation.tsx b/src/components/delegation/Delegation.tsx new file mode 100644 index 0000000..4de08c4 --- /dev/null +++ b/src/components/delegation/Delegation.tsx @@ -0,0 +1,223 @@ +/** + * @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 Delegation.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useState } from 'react' +import { cmn, cls, styles, type interfaces } from '@skalenetwork/metaport' + +import { Collapse, Grid, Tooltip } from '@mui/material' +import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded' +import AccountBalanceRoundedIcon from '@mui/icons-material/AccountBalanceRounded' +import ApartmentRoundedIcon from '@mui/icons-material/ApartmentRounded' + +import SkBtn from '../SkBtn' +import ValidatorLogo from './ValidatorLogo' + +import { + DelegationType, + type IDelegation, + type IDelegationInfo, + type IRewardInfo, + type IValidator +} from '../../core/interfaces' +import { + DelegationSource, + DelegationState, + getDelegationSource, + getKeyByValue, + getValidatorById +} from '../../core/delegation' +import { formatBigIntTimestampSeconds } from '../../core/timeHelper' + +import { convertMonthIndexToText, formatBalance } from '../../core/helper' + +export default function Delegation(props: { + delegation: IDelegation + validators: IValidator[] + delegationType: DelegationType + unstake: (delegationInfo: IDelegationInfo) => Promise + cancelRequest: (delegationInfo: IDelegationInfo) => Promise + loading: IRewardInfo | IDelegationInfo | false + isXs: boolean + customAddress: interfaces.AddressType | undefined +}) { + const validator = getValidatorById(props.validators, props.delegation.validator_id) + const source = getDelegationSource(props.delegation) + const delegationAmount = formatBalance(props.delegation.amount, 'SKL') + const [open, setOpen] = useState(false) + + const delId = Number(props.delegation.stateId) + const isCompleted = delId === DelegationState.COMPLETED + const isActive = + delId === DelegationState.DELEGATED || + delId === DelegationState.UNDELEGATION_REQUESTED || + delId === DelegationState.PROPOSED || + delId === DelegationState.ACCEPTED + + const delegationInfo: IDelegationInfo = { + delegationId: props.delegation.id, + delegationType: props.delegationType + } + + const loading = + props.loading && + props.loading.delegationType === props.delegationType && + 'delegationId' in props.loading && + props.loading.delegationId === props.delegation.id + + function getStakingText() { + if (delId === DelegationState.PROPOSED || delId === DelegationState.ACCEPTED) { + return 'Will be staked' + } else if (delId === DelegationState.CANCELED || delId === DelegationState.REJECTED) { + return 'Staking amount' + } else if ( + delId === DelegationState.DELEGATED || + delId === DelegationState.UNDELEGATION_REQUESTED + ) { + return 'Staked' + } else { + return 'Was staked' + } + } + + if (!validator) return + return ( +
+ { + setOpen(!open) + }} + > + +
+ + +
+

ID: {Number(props.delegation.id)}

+

+ {formatBigIntTimestampSeconds(props.delegation.created)} +

+
+ {props.delegationType === DelegationType.ESCROW ? ( + + + + ) : null} + {props.delegationType === DelegationType.ESCROW2 ? ( + + + + ) : null} +
+
+ +
+
+
+

+ {props.delegation.state.replace(/_/g, ' ')} +

+
+
+
+
+ +
+
+
+

{source}

+
+
+
+
+ +
+
+

{delegationAmount}

+

{getStakingText()}

+
+ +
+
+
+ +
+ {isCompleted ? ( +
+

Delegation completed

+

+ {convertMonthIndexToText(Number(props.delegation.finished))} +

+
+ ) : null} + {Number(props.delegation.stateId) === DelegationState.DELEGATED ? ( + { + await props.unstake(delegationInfo) + }} + disabled={props.loading !== false || props.customAddress !== undefined} + /> + ) : null} + {Number(props.delegation.stateId) === DelegationState.PROPOSED ? ( + { + await props.cancelRequest(delegationInfo) + }} + disabled={props.loading !== false || props.customAddress !== undefined} + /> + ) : null} + {Number(props.delegation.stateId) !== DelegationState.PROPOSED && + Number(props.delegation.stateId) !== DelegationState.DELEGATED && + !isCompleted ? ( +

+ No actions available +

+ ) : null} +
+
+
+ ) +} diff --git a/src/components/delegation/DelegationTypeSelect.tsx b/src/components/delegation/DelegationTypeSelect.tsx new file mode 100644 index 0000000..d2055b0 --- /dev/null +++ b/src/components/delegation/DelegationTypeSelect.tsx @@ -0,0 +1,58 @@ +/** + * @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 DelegationTypeSelect.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cmn, cls } from '@skalenetwork/metaport' + +import { DelegationType, type StakingInfoMap } from '../../core/interfaces' +import NativeSelect from '@mui/material/NativeSelect' +import { isDelegationTypeAvailable } from '../../core/delegation/staking' + +export default function DelegationTypeSelect(props: { + delegationType: DelegationType + handleChange: (event: any) => void + si: StakingInfoMap +}) { + return ( +
+ + + {isDelegationTypeAvailable(props.si, DelegationType.ESCROW) ? ( + + ) : null} + {isDelegationTypeAvailable(props.si, DelegationType.ESCROW2) ? ( + + ) : null} + +
+ ) +} diff --git a/src/components/delegation/Delegations.tsx b/src/components/delegation/Delegations.tsx new file mode 100644 index 0000000..6fdfab7 --- /dev/null +++ b/src/components/delegation/Delegations.tsx @@ -0,0 +1,134 @@ +/** + * @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 Delegations.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cmn, cls, styles, type interfaces } from '@skalenetwork/metaport' +import Skeleton from '@mui/material/Skeleton' +import AllInboxRoundedIcon from '@mui/icons-material/AllInboxRounded' +import PieChartRoundedIcon from '@mui/icons-material/PieChartRounded' + +import Headline from '../Headline' +import DelegationsToValidator from './DelegationsToValidator' + +import { + DelegationType, + type IDelegationInfo, + type IDelegationsToValidator, + type IRewardInfo, + type IValidator, + type StakingInfoMap +} from '../../core/interfaces' + +export default function Delegations(props: { + si: StakingInfoMap + validators: IValidator[] + retrieveRewards: (rewardInfo: IRewardInfo) => Promise + loading: IRewardInfo | IDelegationInfo | false + setErrorMsg: (errorMsg: string | undefined) => void + errorMsg: string | undefined + unstake: (delegationInfo: IDelegationInfo) => Promise + cancelRequest: (delegationInfo: IDelegationInfo) => Promise + isXs: boolean + customAddress: interfaces.AddressType | undefined +}) { + const loaded = props.si[DelegationType.REGULAR] !== null + const noDelegations = + (!props.si[DelegationType.REGULAR] || + props.si[DelegationType.REGULAR]?.delegations.length === 0) && + (!props.si[DelegationType.ESCROW] || + props.si[DelegationType.ESCROW]?.delegations.length === 0) && + (!props.si[DelegationType.ESCROW2] || + props.si[DelegationType.ESCROW2]?.delegations.length === 0) + return ( +
+ } /> +
+ {!loaded ? ( +
+ +
+ +
+
+ ) : null} + {loaded && noDelegations ? ( +
+ +

+ No tokens staked +

+
+ ) : ( +
+ {props.si[DelegationType.REGULAR]?.delegations.map( + (delegationsToValidator: IDelegationsToValidator, index: number) => ( + + ) + )} + {props.si[DelegationType.ESCROW]?.delegations.map( + (delegationsToValidator: IDelegationsToValidator, index: number) => ( + + ) + )} + {props.si[DelegationType.ESCROW2]?.delegations.map( + (delegationsToValidator: IDelegationsToValidator, index: number) => ( + + ) + )} +
+ )} +
+ ) +} diff --git a/src/components/delegation/DelegationsToValidator.tsx b/src/components/delegation/DelegationsToValidator.tsx new file mode 100644 index 0000000..9e47b81 --- /dev/null +++ b/src/components/delegation/DelegationsToValidator.tsx @@ -0,0 +1,86 @@ +/** + * @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 DelegationsToValidator.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useState } from 'react' +import { cls, type interfaces } from '@skalenetwork/metaport' +import { Collapse } from '@mui/material' +import { + type DelegationType, + type IDelegation, + type IDelegationInfo, + type IDelegationsToValidator, + type IRewardInfo, + type IValidator +} from '../../core/interfaces' + +import Delegation from './Delegation' +import Reward from './Reward' + +export default function DelegationsToValidator(props: { + delegationsToValidator: IDelegationsToValidator + validators: IValidator[] + delegationType: DelegationType + retrieveRewards: (rewardInfo: IRewardInfo) => Promise + loading: IRewardInfo | IDelegationInfo | false + unstake: (delegationInfo: IDelegationInfo) => Promise + cancelRequest: (delegationInfo: IDelegationInfo) => Promise + isXs: boolean + customAddress: interfaces.AddressType | undefined +}) { + const [open, setOpen] = useState(true) + return ( +
+ + + +
+ {props.delegationsToValidator.delegations.map( + (delegation: IDelegation, index: number) => ( + + ) + )} +
+
+
+ ) +} diff --git a/src/components/delegation/Reward.tsx b/src/components/delegation/Reward.tsx new file mode 100644 index 0000000..84c372a --- /dev/null +++ b/src/components/delegation/Reward.tsx @@ -0,0 +1,152 @@ +/** + * @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 Reward.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cmn, cls, styles, type interfaces } from '@skalenetwork/metaport' + +import { Grid } from '@mui/material' +import Button from '@mui/material/Button' +import LoadingButton from '@mui/lab/LoadingButton' + +import AddCircleRoundedIcon from '@mui/icons-material/AddCircleRounded' +import RemoveCircleRoundedIcon from '@mui/icons-material/RemoveCircleRounded' + +import ValidatorLogo from './ValidatorLogo' + +import { + type DelegationType, + type IDelegationInfo, + type IDelegationsToValidator, + type IRewardInfo, + type IValidator +} from '../../core/interfaces' +import { getValidatorById } from '../../core/delegation' +import { formatBalance } from '../../core/helper' + +export default function Reward(props: { + validators: IValidator[] + delegationsToValidator: IDelegationsToValidator + setOpen: (open: boolean) => void + open: boolean + retrieveRewards: (rewardInfo: IRewardInfo) => Promise + loading: IRewardInfo | IDelegationInfo | false + delegationType: DelegationType + isXs: boolean + customAddress: interfaces.AddressType | undefined +}) { + const validator = getValidatorById(props.validators, props.delegationsToValidator.validatorId) + const rewardsAmount = formatBalance(props.delegationsToValidator.rewards, 'SKL') + const totalStakedAmount = formatBalance(props.delegationsToValidator.staked, 'SKL') + if (!validator) return + + const loading = + props.loading && + props.loading.delegationType === props.delegationType && + 'validatorId' in props.loading && + props.loading.validatorId === validator.id + + const minimizeBtn = ( +
{ + props.setOpen(!props.open) + }} + > + {props.open ? ( + + ) : ( + + )} +
+ ) + + return ( +
+ + +
+ +
+

{validator.name}

+

Validator ID: {Number(validator.id)}

+
+ {props.isXs ? minimizeBtn : null} +
+
+ +
+
+ {!props.isXs && !props.open ? ( +
+
+

Total staked

+

{totalStakedAmount}

+
+
+
+ ) : null} +
+

Rewards available

+

{rewardsAmount}

+
+ {loading ? ( + + Retrieving + + ) : ( + + )} + {!props.isXs ? minimizeBtn : null} +
+
+
+
+ ) +} diff --git a/src/components/delegation/Summary.tsx b/src/components/delegation/Summary.tsx new file mode 100644 index 0000000..6d9745a --- /dev/null +++ b/src/components/delegation/Summary.tsx @@ -0,0 +1,222 @@ +/** + * @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 Summary.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cmn, cls, styles, TokenIcon, type interfaces } from '@skalenetwork/metaport' + +import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' +import AccountBalanceRoundedIcon from '@mui/icons-material/AccountBalanceRounded' +import EventAvailableRoundedIcon from '@mui/icons-material/EventAvailableRounded' +import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded' +import ApartmentRoundedIcon from '@mui/icons-material/ApartmentRounded' +import LockOpenRoundedIcon from '@mui/icons-material/LockOpenRounded' +import ControlPointDuplicateRoundedIcon from '@mui/icons-material/ControlPointDuplicateRounded' +import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded' + +import SkStack from '../SkStack' +import Tile from '../Tile' +import AccordionSection from '../AccordionSection' + +import { + DelegationType, + type IDelegationInfo, + type IDelegatorInfo, + type IRewardInfo +} from '../../core/interfaces' +import { formatBalance, shortAddress } from '../../core/helper' +import SkBtn from '../SkBtn' + +const icons: { [key in DelegationType]: any } = { + 0: , + 1: , + 2: +} + +const SUMMARY_VALIDATOR_ID = -1 + +export default function Summary(props: { + type: DelegationType + accountInfo: IDelegatorInfo | undefined + retrieveUnlocked: (rewardInfo: IRewardInfo) => Promise + loading: IRewardInfo | IDelegationInfo | false + customAddress: interfaces.AddressType | undefined + isXs: boolean +}) { + function getTitle() { + if (props.type === DelegationType.ESCROW) return 'Escrow' + if (props.type === DelegationType.ESCROW2) return 'Grant Escrow' + return 'Account' + } + + const rewardInfo: IRewardInfo = { + validatorId: SUMMARY_VALIDATOR_ID, + delegationType: props.type + } + + const loading = + props.loading && + props.loading.delegationType === props.type && + 'validatorId' in props.loading && + props.loading.validatorId === SUMMARY_VALIDATOR_ID + + return ( +
+ + + } + childrenRi={ + + {props.type !== DelegationType.REGULAR ? ( +
+ } + /> +
+
+ ) : ( +
+ )} + } + /> +
+ } + /> +
+ } + /> +
+ {props.accountInfo?.vested !== undefined && props.accountInfo?.unlocked !== undefined ? ( + + } + grow + childrenRi={ + + {props.accountInfo?.fullAmount !== undefined ? ( + } + grow + size="md" + transparent + className={cls(cmn.nop, [cmn.mri20, !props.isXs], [cmn.mleft20, !props.isXs])} + ri={!props.isXs} + /> + ) : ( +
+ )} +
+ } + grow + ri={!props.isXs} + childrenRi={ +
+ { + props.retrieveUnlocked(rewardInfo) + }} + /> +
+ } + /> +
+ } + /> +
+ ) : ( +
+ )} +
+
+ ) +} diff --git a/src/components/delegation/ValidatorBadges.tsx b/src/components/delegation/ValidatorBadges.tsx new file mode 100644 index 0000000..849ab88 --- /dev/null +++ b/src/components/delegation/ValidatorBadges.tsx @@ -0,0 +1,58 @@ +/** + * @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 ValidatorBadges.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cmn, cls } from '@skalenetwork/metaport' + +import Tooltip from '@mui/material/Tooltip' +import VerifiedRoundedIcon from '@mui/icons-material/VerifiedRounded' +import WarningRoundedIcon from '@mui/icons-material/WarningRounded' +import AccountBalanceRoundedIcon from '@mui/icons-material/AccountBalanceRounded' +import { ESCROW_VALIDATORS } from '../../core/delegation/validators' +import { type IValidator } from '../../core/interfaces' + +export function ValidatorBadge(props: { validator: IValidator; className?: string }) { + if (ESCROW_VALIDATORS.includes(props.validator.id)) { + return ( + + + + ) + } + return null +} + +export function TrustBadge(props: { validator: IValidator }) { + if (props.validator.trusted) { + return ( + + + + ) + } + return ( + + + + ) +} diff --git a/src/components/delegation/ValidatorCard.tsx b/src/components/delegation/ValidatorCard.tsx new file mode 100644 index 0000000..13a2e15 --- /dev/null +++ b/src/components/delegation/ValidatorCard.tsx @@ -0,0 +1,145 @@ +/** + * @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 ValidatorCard.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { Link } from 'react-router-dom' +import Grid from '@mui/material/Grid' +import Tooltip from '@mui/material/Tooltip' + +import { cmn, cls, styles, fromWei } from '@skalenetwork/metaport' + +import ValidatorLogo from './ValidatorLogo' +import { TrustBadge, ValidatorBadge } from './ValidatorBadges' + +import { type DelegationType, type IValidator } from '../../core/interfaces' +import { DEFAULT_ERC20_DECIMALS } from '../../core/constants' + +export default function ValidatorCard(props: { + validator: IValidator + validatorId: number | undefined + setValidatorId: any + delegationType: DelegationType + size?: 'md' | 'lg' +}) { + if (!props.validator.trusted) return + + const size = props.size ?? 'md' + + const description = props.validator.description ? props.validator.description : 'No description' + const minDelegation = fromWei(props.validator.minimumDelegationAmount, DEFAULT_ERC20_DECIMALS) + + return ( + + +
{ + props.validator.acceptNewRequests ? props.setValidatorId(props.validator.id) : null + }} + > +
+ +
+ +
+
+
+ +
+
+ +
+
+

+ {props.validator.name} +

+
+
+ +
+
+ +

+ {description} +

+
+
+
+
+
+

+ {Number(props.validator.feeRate) / 10}% fee +

+
+
+

ID: {props.validator.id}

+
+ {size === 'lg' ? ( +
+

+ Nodes: {props.validator.linkedNodes} +

+
+ ) : null} +
+ + {size !== 'lg' ? ( + +
+

+ Min: {minDelegation} SKL +

+
+
+ ) : null} + {size === 'lg' ? ( + +
+

+ Address: {props.validator.validatorAddress} +

+
+
+ ) : null} +
+ +
+ ) +} diff --git a/src/components/delegation/ValidatorInfo.tsx b/src/components/delegation/ValidatorInfo.tsx new file mode 100644 index 0000000..0f5ffb1 --- /dev/null +++ b/src/components/delegation/ValidatorInfo.tsx @@ -0,0 +1,84 @@ +/** + * @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 ValidatorInfo.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cmn, cls, fromWei, TokenIcon } from '@skalenetwork/metaport' + +import PercentRoundedIcon from '@mui/icons-material/PercentRounded' +import PersonRoundedIcon from '@mui/icons-material/PersonRounded' + +import ValidatorLogo from './ValidatorLogo' +import { ValidatorBadge, TrustBadge } from './ValidatorBadges' +import Tile from '../Tile' +import SkStack from '../SkStack' + +import { type IValidator } from '../../core/interfaces' +import { DEFAULT_ERC20_DECIMALS } from '../../core/constants' + +export default function ValidatorInfo(props: { validator: IValidator; className?: string }) { + const description = props.validator.description ? props.validator.description : 'No description' + const minDelegation = fromWei(props.validator.minimumDelegationAmount, DEFAULT_ERC20_DECIMALS) + + return ( +
+
+ +
+
+

{props.validator.name}

+ + +
+

+ {description} +

+
+
+ + } + /> + } + /> + } + /> + +
+ ) +} diff --git a/src/components/delegation/ValidatorLogo.tsx b/src/components/delegation/ValidatorLogo.tsx new file mode 100644 index 0000000..e6dac33 --- /dev/null +++ b/src/components/delegation/ValidatorLogo.tsx @@ -0,0 +1,84 @@ +/** + * @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 ValidatorLogo.tsx + * @copyright SKALE Labs 2024-Present + */ + +import Jazzicon from 'react-jazzicon' +import { VALIDATOR_LOGOS } from '../../core/constants' +import { cls, styles } from '@skalenetwork/metaport' + +function hashCode(str: string) { + let hash = 0 + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash) + } + return hash +} + +function getPseudoRandomNumber( + seed: string, + min: number = 1000000000, + max: number = 100000000000000 +): number { + const seedValue = hashCode(seed) + const range = max - min + const rng = Math.sin(seedValue) * 10000 + const randomInt = min + Math.floor((rng - Math.floor(rng)) * range) + return randomInt +} + +type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +export type SizeMap = { + [key in SizeType]: number +} + +export default function ValidatorLogo(props: { + validatorId: number + className?: string + size?: SizeType +}) { + const iconPath = `v${props.validatorId}` + const iconModule = (VALIDATOR_LOGOS as any)[iconPath] + const size = props.size ?? 'md' + const sizes: SizeMap = { xs: 17, sm: 26, md: 35, lg: 45, xl: 70 } + if (iconModule) { + return ( + + ) + } + return ( +
+ +
+ ) +} diff --git a/src/components/delegation/Validators.tsx b/src/components/delegation/Validators.tsx new file mode 100644 index 0000000..9050c03 --- /dev/null +++ b/src/components/delegation/Validators.tsx @@ -0,0 +1,66 @@ +/** + * @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 Validators.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { type MetaportCore } from '@skalenetwork/metaport' +import Grid from '@mui/material/Grid' + +import Loader from '../Loader' +import ValidatorCard from './ValidatorCard' +import { type DelegationType, type IValidator } from '../../core/interfaces' +import { ESCROW_VALIDATORS, filterValidators } from '../../core/delegation/validators' + +export default function Validators(props: { + mpc: MetaportCore + validators: IValidator[] + validatorId: number | undefined + setValidatorId: any + internal?: boolean + delegationType: DelegationType + size?: 'md' | 'lg' +}) { + const size = props.size ?? 'md' + const internal = props.internal ?? false + + if (!props.validators || props.validators.length === 0) { + return + } + + const validators = internal + ? props.validators + : filterValidators(props.validators, ESCROW_VALIDATORS, internal) + + return ( + + {validators.map((validator: IValidator, index) => ( + + ))} + + ) +} diff --git a/src/core/constants.ts b/src/core/constants.ts index 83c5fec..17e922f 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -23,6 +23,11 @@ import FAQ from '../data/faq.json' +import * as MAINNET_CHAIN_LOGOS from '../meta/logos' +import * as VALIDATOR_LOGOS from '../assets/validators' + +import { CONTRACTS_META } from '../data/contractsMeta.ts' + export const MAINNET_CHAIN_NAME = 'mainnet' export const DASHBOARD_URL = 'https://app.geckoboard.com/v5/dashboards/LISYTRBEVGCVGL57/inception' @@ -32,14 +37,12 @@ export const BRIDGE_PAGES = ['/bridge', '/transfer', '/bridge/history', '/portfo export const DEFAULT_ERC20_DECIMALS = '18' -export const PORTAL_URLS: { [network: string]: string } = { +export const PORTAL_URLS: Record = { mainnet: 'https://portal.skale.space/', staging: 'https://testnet.portal.skale.space/' } -import * as MAINNET_CHAIN_LOGOS from '../meta/logos' - -export { FAQ, MAINNET_CHAIN_LOGOS } +export { FAQ, MAINNET_CHAIN_LOGOS, VALIDATOR_LOGOS, CONTRACTS_META } export const DISCORD_INVITE_URL = 'https://discord.com/invite/gM5XBy6' @@ -48,3 +51,13 @@ export const AVG_MONTH_LENGTH = 30.436875 const _DEFAULT_UPDATE_INTERVAL_SECONDS = 10 export const DEFAULT_UPDATE_INTERVAL_MS = _DEFAULT_UPDATE_INTERVAL_SECONDS * MS_MULTIPLIER + +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +export const DEFAULT_ERROR_MSG = 'Something went wrong' + +export const DEFAULT_DELEGATION_PERIOD = 2n +export const DEFAULT_DELEGATION_INFO = 'portal' + +const _BALANCE_UPDATE_INTERVAL_SECONDS = 25 +export const BALANCE_UPDATE_INTERVAL_MS = _BALANCE_UPDATE_INTERVAL_SECONDS * 1000 diff --git a/src/core/contracts.ts b/src/core/contracts.ts new file mode 100644 index 0000000..b469444 --- /dev/null +++ b/src/core/contracts.ts @@ -0,0 +1,123 @@ +/** + * @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 contracts.ts + * @copyright SKALE Labs 2024-Present + */ + +import debug from 'debug' +import { type Contract, type AbstractProvider, type Signer } from 'ethers' +import { MulticallWrapper } from 'ethers-multicall-provider' +import { skaleContracts, type Instance } from '@skalenetwork/skale-contracts-ethers-v6' +import { type MetaportCore, type interfaces } from '@skalenetwork/metaport' + +import { initSkaleToken } from './delegation' +import { type ContractType, DelegationType, type ISkaleContractsMap } from './interfaces' +import { CONTRACTS_META } from './constants' + +debug.enable('*') +const log = debug('portal:core:contracts') + +type PROJECT_TYPE = 'manager' | 'allocator' | 'grants' + +export async function initContracts(mpc: MetaportCore): Promise { + log('Initializing contracts') + const provider = MulticallWrapper.wrap(mpc.provider('mainnet') as AbstractProvider) + const network = await skaleContracts.getNetworkByProvider(provider) + const managerProject = await network.getProject('skale-manager') + const manager = await getInstance(managerProject, mpc.config.skaleNetwork, 'manager') + const allocatorProject = await network.getProject('skale-allocator') + const allocator = await getInstance(allocatorProject, mpc.config.skaleNetwork, 'allocator') + const grantsAllocator = await getInstance(allocatorProject, mpc.config.skaleNetwork, 'grants') + return { + validatorService: (await manager.getContract('ValidatorService')) as Contract, + distributor: (await manager.getContract('Distributor')) as Contract, + delegationController: (await manager.getContract('DelegationController')) as Contract, + tokenState: (await manager.getContract('TokenState')) as Contract, + skaleToken: await initSkaleToken(provider, manager), + allocator: (await allocator.getContract('Allocator')) as Contract, + grantsAllocator: (await grantsAllocator.getContract('Allocator')) as Contract + } +} + +export async function initActionContract( + signer: Signer, + delegationType: DelegationType, + beneficiary: interfaces.AddressType, + skaleNetwork: interfaces.SkaleNetwork, + contractType: ContractType +): Promise { + log('initActionContract:', skaleNetwork, beneficiary, contractType, delegationType) + const network = await skaleContracts.getNetworkByProvider(signer.provider!) + let contract: Contract + if (delegationType === DelegationType.REGULAR) { + contract = await getManagerContract( + network, + skaleNetwork, + contractType === 'delegation' ? 'DelegationController' : 'Distributor' + ) + } else { + contract = await getEscrowContract(network, skaleNetwork, delegationType, beneficiary) + } + return connectedContract(contract, signer) +} + +function getInstanceTag(skaleNetwork: interfaces.SkaleNetwork, projectName: PROJECT_TYPE): string { + if (CONTRACTS_META[skaleNetwork].auto) { + if (projectName === 'grants') return 'grants' + return 'production' + } + return CONTRACTS_META[skaleNetwork][projectName] +} + +function connectedContract(contract: Contract, signer: Signer): Contract { + return contract.connect(signer) as Contract +} + +async function getEscrowContract( + network: any, + skaleNetwork: interfaces.SkaleNetwork, + delegationType: DelegationType, + beneficiary: interfaces.AddressType +): Promise { + const project = await network.getProject('skale-allocator') + const instance = await getInstance( + project, + skaleNetwork, + delegationType === DelegationType.ESCROW ? 'allocator' : 'grants' + ) + return (await instance.getContract('Escrow', [beneficiary])) as Contract +} + +async function getManagerContract( + network: any, + skaleNetwork: interfaces.SkaleNetwork, + name: string +): Promise { + const managerProject = await network.getProject('skale-manager') + const manager = await getInstance(managerProject, skaleNetwork, 'manager') + return (await manager.getContract(name)) as Contract +} + +async function getInstance( + project: any, + skaleNetwork: interfaces.SkaleNetwork, + tag: PROJECT_TYPE +): Promise { + return project.getInstance(getInstanceTag(skaleNetwork, tag)) +} diff --git a/src/core/delegation/delegations.ts b/src/core/delegation/delegations.ts new file mode 100644 index 0000000..9488549 --- /dev/null +++ b/src/core/delegation/delegations.ts @@ -0,0 +1,243 @@ +/** + * @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 delegations.ts + * @copyright SKALE Labs 2024-Present + */ + +import { Contract, type Provider, getUint } from 'ethers' +import { ERC_ABIS, type interfaces } from '@skalenetwork/metaport' +import { + type IDelegationArray, + type IDelegation, + type IDelegationsToValidator, + type ISkaleContractsMap, + DelegationType, + type IDelegatorInfo +} from '../interfaces' +import { maxBigInt } from '../helper' + +export enum DelegationState { + PROPOSED = 0, + ACCEPTED = 1, + CANCELED = 2, + REJECTED = 3, + DELEGATED = 4, + UNDELEGATION_REQUESTED = 5, + COMPLETED = 6 +} + +export enum DelegationSource { + DELEGATION_UI = 'Delegation UI', + MEW_WALLET = 'MEW Wallet', + ACTIVATE = 'Activate', + PORTAL = 'Portal', + SELF = 'Self-delegation', + ETHERSCAN = 'Etherscan', + OTHER = 'Other' +} + +export async function getDelegationIdsByHolder( + delegationController: Contract, + address: interfaces.AddressType +): Promise { + const delegationIdsLen = await delegationController.getDelegationsByHolderLength(address) + return await Promise.all( + Array.from( + { length: Number(delegationIdsLen) }, + async (_, id) => await delegationController.delegationsByHolder(address, id) + ) + ) +} + +async function getDelegationsRaw( + delegationController: Contract, + delegationIds: bigint[] +): Promise> { + return await Promise.all( + delegationIds + .map((delegationId) => [ + delegationController.getDelegation(delegationId), + delegationController.getState(delegationId) + ]) + .flat() + ) +} + +export async function getDelegations( + delegationController: Contract, + delegationIds: bigint[] +): Promise { + const rawDelegations: Array = await getDelegationsRaw( + delegationController, + delegationIds + ) + const delegations: IDelegation[] = [] + for (let i = 0; i < rawDelegations.length; i += 2) { + const delegationArray: IDelegationArray = rawDelegations[i] as IDelegationArray + const stateId: bigint = rawDelegations[i + 1] as bigint + + delegations.push({ + id: delegationIds[i / 2], + address: delegationArray[0], + validator_id: delegationArray[1], + amount: delegationArray[2], + delegation_period: delegationArray[3], + created: delegationArray[4], + started: delegationArray[5], + finished: delegationArray[6], + info: delegationArray[7], + stateId, + state: DelegationState[Number(stateId)] + }) + } + return delegations +} + +export function getDelegationSource(delegation: IDelegation): DelegationSource { + if (delegation.info.includes('Delegation UI')) return DelegationSource.DELEGATION_UI + if (delegation.info.includes('MEW Wallet')) return DelegationSource.MEW_WALLET + if (delegation.info.includes('Activate')) return DelegationSource.ACTIVATE + if (delegation.info.toLowerCase().includes('portal')) return DelegationSource.PORTAL + if (delegation.info.includes('Self')) return DelegationSource.SELF + if (delegation.info.toLowerCase().includes('etherscan')) return DelegationSource.ETHERSCAN + return DelegationSource.OTHER +} + +export function getKeyByValue(enumType: any, enumValue: string): string | undefined { + return Object.keys(enumType).find((key) => enumType[key] === enumValue) +} + +export async function groupDelegationsByValidator( + delegations: IDelegation[], + distributor: Contract, + address: interfaces.AddressType +): Promise { + const groupedDelegations = new Map() + delegations.forEach((delegation) => { + const { validator_id } = delegation + const existingDelegations = groupedDelegations.get(validator_id) || [] + groupedDelegations.set(validator_id, [...existingDelegations, delegation]) + }) + + const delegationsArray = Array.from(groupedDelegations.entries()).map( + ([validatorId, delegations]) => ({ + validatorId, + delegations, + rewards: 0n, + staked: 0n + }) + ) + + const res = await Promise.all( + delegationsArray.map( + async (delegationsToValidator: IDelegationsToValidator) => + await distributor.getAndUpdateEarnedBountyAmountOf.staticCallResult( + address, + delegationsToValidator.validatorId + ) + ) + ) + delegationsArray.forEach((delegationsToValidator, index) => { + delegationsToValidator.rewards = res[index][0] + delegationsToValidator.staked = delegationsToValidator.delegations.reduce( + (total, delegation) => { + if (Number(delegation.stateId) === DelegationState.DELEGATED) { + return total + delegation.amount + } else { + return total + } + }, + 0n + ) + }) + + return delegationsArray +} + +export const sumRewards = (delegations: IDelegationsToValidator[]): bigint => + delegations.reduce((total, del) => total + del.rewards, BigInt(0)) + +export async function initSkaleToken(provider: Provider, instance: any): Promise { + const address = await instance.getContractAddress('SkaleToken') + return new Contract(address, ERC_ABIS.erc20.abi, provider) +} + +export async function getDelegatorInfo( + sc: ISkaleContractsMap, + rewards: bigint, + address: interfaces.AddressType, + beneficiary?: interfaces.AddressType, + type?: DelegationType +): Promise { + const info: IDelegatorInfo = { + balance: await sc.skaleToken.balanceOf(address), + staked: ( + await sc.delegationController.getAndUpdateDelegatedAmount.staticCallResult(address) + )[0], + forbiddenToDelegate: ( + await sc.tokenState.getAndUpdateForbiddenForDelegationAmount.staticCallResult(address) + )[0], + rewards, + address + } + + info.allowedToDelegate = maxBigInt(info.balance - info.forbiddenToDelegate, 0n) + + if (beneficiary) { + if (type === DelegationType.ESCROW) { + info.vested = await getVestedAmount(sc.allocator, address, beneficiary) + info.fullAmount = await sc.allocator.getFullAmount(beneficiary) + } + if (type === DelegationType.ESCROW2) { + info.vested = await getVestedAmount(sc.grantsAllocator, address, beneficiary) + info.fullAmount = await sc.grantsAllocator.getFullAmount(beneficiary) + } + + const locked = maxBigInt(info.fullAmount! - info.vested!, info.forbiddenToDelegate) + info.unlocked = maxBigInt(info.balance - locked, 0n) + } + return info +} + +export async function getVestedAmount( + allocator: Contract, + escrowAddress: interfaces.AddressType, + address: interfaces.AddressType +): Promise { + let vestedAmount: bigint + if (await allocator.isVestingActive(address)) { + vestedAmount = await allocator.calculateVestedAmount(address) + } else { + const provider = allocator.runner?.provider + if (provider) { + const valueHex = await provider.getStorage(escrowAddress, '0x99') + vestedAmount = getUint(valueHex) + } else { + vestedAmount = 0n + } + } + return vestedAmount +} + +export function getDelegationTypeAlias(type: DelegationType): string { + if (type === DelegationType.ESCROW) return 'Escrow' + if (type === DelegationType.ESCROW2) return 'Grant' + return 'Regular' +} diff --git a/src/core/delegation/index.ts b/src/core/delegation/index.ts new file mode 100644 index 0000000..030cdd6 --- /dev/null +++ b/src/core/delegation/index.ts @@ -0,0 +1,25 @@ +/** + * @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 index.ts + * @copyright SKALE Labs 2024-Present + */ + +export * from './validators' +export * from './delegations' diff --git a/src/core/delegation/staking.ts b/src/core/delegation/staking.ts new file mode 100644 index 0000000..4083604 --- /dev/null +++ b/src/core/delegation/staking.ts @@ -0,0 +1,84 @@ +/** + * @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 staking.ts + * @copyright SKALE Labs 2024-Present + */ + +import { + DelegationType, + type ISkaleContractsMap, + type StakingInfo, + type StakingInfoMap +} from '../interfaces' +import { type interfaces } from '@skalenetwork/metaport' +import { isZeroAddr } from '../helper' +import { + getDelegationIdsByHolder, + getDelegations, + groupDelegationsByValidator, + sumRewards, + getDelegatorInfo +} from '.' + +export async function getStakingInfoMap( + sc: ISkaleContractsMap, + address: interfaces.AddressType | undefined +): Promise { + if (!address) return { 0: null, 1: null, 2: null } + const escrowAddress = await sc.allocator.getEscrowAddress(address) + const escrowGrantsAddress = await sc.grantsAllocator.getEscrowAddress(address) + return { + 0: await getStakingInfo(sc, address), + 1: isZeroAddr(escrowAddress) + ? null + : await getStakingInfo(sc, escrowAddress, address, DelegationType.ESCROW), + 2: isZeroAddr(escrowGrantsAddress) + ? null + : await getStakingInfo(sc, escrowGrantsAddress, address, DelegationType.ESCROW2) + } +} + +export async function getStakingInfo( + sc: ISkaleContractsMap, + address: interfaces.AddressType, + beneficiary?: interfaces.AddressType, + type?: DelegationType +): Promise { + const delegationIds = await getDelegationIdsByHolder(sc.delegationController, address) + const delegationsArray = await getDelegations(sc.delegationController, delegationIds) + const groupedDelegations = await groupDelegationsByValidator( + delegationsArray, + sc.distributor, + address + ) + const totalRewards = sumRewards(groupedDelegations) + return { + info: await getDelegatorInfo(sc, totalRewards, address, beneficiary, type), + delegations: groupedDelegations + } +} + +export function isDelegationTypeAvailable(si: StakingInfoMap, type: DelegationType): boolean { + return si[DelegationType.REGULAR] !== null && si[type] !== undefined && si[type] !== null +} + +export function isLoaded(si: StakingInfoMap): boolean { + return si[DelegationType.REGULAR] !== null +} diff --git a/src/core/delegation/validators.ts b/src/core/delegation/validators.ts new file mode 100644 index 0000000..bef12d6 --- /dev/null +++ b/src/core/delegation/validators.ts @@ -0,0 +1,114 @@ +/** + * @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 validators.ts + * @copyright SKALE Labs 2024-Present + */ + +import debug from 'debug' +import { type Contract } from 'ethers' + +import { type IValidatorArray, type IValidator } from '../interfaces' + +debug.enable('*') +const log = debug('portal:core:validators') + +export const ESCROW_VALIDATORS = [ + 43, 46, 54, 37, 48, 49, 42, 41, 47, 40, 52, 35, 36, 39, 50, 45, 51 +] + +async function getValidatorsRaw( + validatorService: Contract, + numberOfValidators: bigint[] +): Promise> { + const validatorIds = Array.from(Array(Number(numberOfValidators)).keys()) + return await Promise.all( + validatorIds + .map((validatorId) => [ + validatorService.validators(validatorId + 1), + validatorService.isAuthorizedValidator(validatorId + 1), + validatorService.getNodeAddresses(validatorId + 1) + ]) + .flat() + ) +} + +export async function getValidators( + validatorService: Contract, + sorted: boolean = true +): Promise { + const numberOfValidators = await validatorService.numberOfValidators() + log('getValidators: ', numberOfValidators) + const rawValidators: Array = await getValidatorsRaw( + validatorService, + numberOfValidators + ) + const validatorsData: IValidator[] = [] + for (let i = 0; i < rawValidators.length; i += 3) { + const IValidatorArray: IValidatorArray = rawValidators[i] as IValidatorArray + const isTrusted: boolean = rawValidators[i + 1] as boolean + const linkedNodeAddresses = rawValidators[i + 2] as string[] + validatorsData.push({ + name: IValidatorArray[0], + validatorAddress: IValidatorArray[1], + requestedAddress: IValidatorArray[2], + description: IValidatorArray[3], + feeRate: IValidatorArray[4], + registrationTime: IValidatorArray[5], + minimumDelegationAmount: IValidatorArray[6], + acceptNewRequests: IValidatorArray[7], + trusted: isTrusted, + id: i / 3 + 1, + linkedNodes: linkedNodeAddresses.length + }) + } + return sorted ? sortValidators(validatorsData) : validatorsData +} + +function sortValidators(validatorsData: IValidator[]): IValidator[] { + validatorsData.sort((a, b) => { + if (a.trusted !== b.trusted) { + return a.trusted ? -1 : 1 + } + if (a.acceptNewRequests !== b.acceptNewRequests) { + return a.acceptNewRequests ? -1 : 1 + } + + const nameComparison = a.name.localeCompare(b.name) + if (nameComparison !== 0) { + return nameComparison + } + return 0 + }) + return validatorsData +} + +export function filterValidators( + validators: IValidator[], + ids: number[], + internal: boolean +): IValidator[] { + return validators.filter( + (val) => (ids.includes(val.id) && internal) || (!ids.includes(val.id) && !internal) + ) +} + +export function getValidatorById(validators: IValidator[], id: bigint): IValidator | undefined { + return validators.find((val) => Number(val.id) === Number(id)) +} diff --git a/src/core/helper.ts b/src/core/helper.ts new file mode 100644 index 0000000..e7042f2 --- /dev/null +++ b/src/core/helper.ts @@ -0,0 +1,80 @@ +/** + * @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 helper.ts + * @copyright SKALE Labs 2024-Present + */ + +import { fromWei, type interfaces } from '@skalenetwork/metaport' +import { DEFAULT_ERC20_DECIMALS, ZERO_ADDRESS } from './constants' + +export function isZeroAddr(address: interfaces.AddressType): boolean { + return address === ZERO_ADDRESS +} + +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)}` +} + +export function formatBalance(value: bigint, tokenSymbol?: string): string { + const res = Number(truncateDecimals(fromWei(value, DEFAULT_ERC20_DECIMALS), 5)).toLocaleString() + return res + (tokenSymbol ? ` ${tokenSymbol}` : '') +} + +export function compareEnum(enumValue1: any, enumValue2: any): boolean { + return Number(enumValue1) === Number(enumValue2) +} + +export function convertMonthIndexToText(index: number): string { + const baseYear = 2020 + const monthsPerYear = 12 + + const year = baseYear + Math.floor(index / monthsPerYear) + const month = index % monthsPerYear + + const monthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' + ] + return `${monthNames[month]} ${year}` +} + +export function maxBigInt(a: bigint, b: bigint): bigint { + return a > b ? a : b +} + +export function minBigInt(a: bigint, b: bigint): bigint { + return a < b ? a : b +} + +export function shortAddress(address: interfaces.AddressType | undefined): string { + if (!address) return '' + return `${address.slice(0, 4)}...${address.slice(-2)}` +} diff --git a/src/core/interfaces/Beneficiary.ts b/src/core/interfaces/Beneficiary.ts new file mode 100644 index 0000000..66d9969 --- /dev/null +++ b/src/core/interfaces/Beneficiary.ts @@ -0,0 +1,34 @@ +/** + * @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 Beneficiary.ts + * @copyright SKALE Labs 2023-Present + */ + +import { type interfaces } from '@skalenetwork/metaport' + +export type IBeneficiaryArray = [bigint, bigint, bigint, bigint, bigint, interfaces.AddressType] + +export interface IBeneficiary { + status: bigint + planId: bigint + startMonth: bigint + fullAmount: bigint + amountAfterLockup: bigint + requestedAddress: interfaces.AddressType +} diff --git a/src/core/interfaces/Delegation.ts b/src/core/interfaces/Delegation.ts new file mode 100644 index 0000000..85b180b --- /dev/null +++ b/src/core/interfaces/Delegation.ts @@ -0,0 +1,72 @@ +/** + * @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 Delegation.ts + * @copyright SKALE Labs 2023-Present + */ + +import { type interfaces } from '@skalenetwork/metaport' + +export type IDelegationArray = [ + interfaces.AddressType, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + string +] + +export interface IDelegation { + id: bigint + address: interfaces.AddressType + validator_id: bigint + amount: bigint + delegation_period: bigint + created: bigint + started: bigint + finished: bigint + info: string + stateId: bigint + state: string +} + +export interface IDelegationsToValidator { + validatorId: bigint + delegations: IDelegation[] + rewards: bigint + staked: bigint +} + +export enum DelegationType { + REGULAR = 0, + ESCROW = 1, + ESCROW2 = 2 +} + +export interface IRewardInfo { + validatorId: number + delegationType: DelegationType +} + +export interface IDelegationInfo { + delegationId: bigint + delegationType: DelegationType +} diff --git a/src/core/interfaces/Delegator.ts b/src/core/interfaces/Delegator.ts new file mode 100644 index 0000000..3c511e7 --- /dev/null +++ b/src/core/interfaces/Delegator.ts @@ -0,0 +1,36 @@ +/** + * @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 Delegator.ts + * @copyright SKALE Labs 2024-Present + */ + +import { type interfaces } from '@skalenetwork/metaport' + +export interface IDelegatorInfo { + balance: bigint + staked: bigint + rewards: bigint + forbiddenToDelegate: bigint + allowedToDelegate?: bigint + vested?: bigint + fullAmount?: bigint + unlocked?: bigint + address: interfaces.AddressType +} diff --git a/src/core/interfaces/SkaleContract.ts b/src/core/interfaces/SkaleContract.ts new file mode 100644 index 0000000..2d4267b --- /dev/null +++ b/src/core/interfaces/SkaleContract.ts @@ -0,0 +1,39 @@ +/** + * @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 SkaleContract.ts + * @copyright SKALE Labs 2024-Present + */ + +import { type Contract } from 'ethers' + +export type SkaleContractName = + | 'delegationController' + | 'skaleToken' + | 'allocator' + | 'distributor' + | 'validatorService' + | 'grantsAllocator' + | 'tokenState' + +export type ISkaleContractsMap = { + [key in SkaleContractName]: Contract +} + +export type ContractType = 'delegation' | 'distributor' diff --git a/src/core/interfaces/Staking.ts b/src/core/interfaces/Staking.ts new file mode 100644 index 0000000..5521306 --- /dev/null +++ b/src/core/interfaces/Staking.ts @@ -0,0 +1,31 @@ +/** + * @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 Staking.ts + * @copyright SKALE Labs 2023-Present + */ + +import { type DelegationType, type IDelegationsToValidator, type IDelegatorInfo } from '.' + +export type StakingInfoMap = { [key in DelegationType]: StakingInfo | null } + +export interface StakingInfo { + delegations: IDelegationsToValidator[] + info: IDelegatorInfo +} diff --git a/src/core/interfaces/Validator.ts b/src/core/interfaces/Validator.ts new file mode 100644 index 0000000..d0c2e0b --- /dev/null +++ b/src/core/interfaces/Validator.ts @@ -0,0 +1,49 @@ +/** + * @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 Validator.ts + * @copyright SKALE Labs 2024-Present + */ + +import { type interfaces } from '@skalenetwork/metaport' + +export type IValidatorArray = [ + string, + interfaces.AddressType, + interfaces.AddressType, + string, + bigint, + bigint, + bigint, + boolean +] + +export interface IValidator { + name: string + validatorAddress: interfaces.AddressType + requestedAddress: interfaces.AddressType + description: string + feeRate: bigint + registrationTime: bigint + minimumDelegationAmount: bigint + acceptNewRequests: boolean + trusted: boolean + id: number + linkedNodes: number +} diff --git a/src/core/interfaces/index.ts b/src/core/interfaces/index.ts new file mode 100644 index 0000000..93cd7a3 --- /dev/null +++ b/src/core/interfaces/index.ts @@ -0,0 +1,28 @@ +/** + * @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 validator.ts + * @copyright SKALE Labs 2024-Present + */ + +export * from './Validator' +export * from './Delegation' +export * from './Delegator' +export * from './SkaleContract' +export * from './Staking' diff --git a/src/core/meta.ts b/src/core/meta.ts index b78b009..903b4bc 100644 --- a/src/core/meta.ts +++ b/src/core/meta.ts @@ -38,7 +38,7 @@ export const META_TAGS = { history: { title: 'SKALE Portal - Bridge History', description: - 'SKALE Bridge transfers history - detailed overview of past transactions and links to block expolorers.' + 'SKALE Bridge transfers history - detailed overview of past transactions and links to block explorers.' }, portfolio: { title: 'SKALE Portal - Portfolio', @@ -49,6 +49,10 @@ export const META_TAGS = { description: 'Connect to SKALE Chains, get block explorer links, endpoints, linked tokens and verified contracts info.' }, + apps: { + title: 'SKALE Portal - Apps', + description: 'Apps on SKALE Network. Explore and interact with dApps on SKALE Network.' + }, faq: { title: 'SKALE Portal - Bridge FAQ', description: 'Common questions about SKALE Bridge and transfers on SKALE.' diff --git a/src/core/paymaster.ts b/src/core/paymaster.ts index c2b22f8..e44fb5f 100644 --- a/src/core/paymaster.ts +++ b/src/core/paymaster.ts @@ -20,10 +20,9 @@ * @copyright SKALE Labs 2022-Present */ -import { Contract, id, InterfaceAbi } from 'ethers' -import { MetaportCore, interfaces } from '@skalenetwork/metaport' +import { Contract, id, type InterfaceAbi } from 'ethers' +import { type MetaportCore, type interfaces } from '@skalenetwork/metaport' import PAYMASTER_INFO from '../data/paymaster' -import { getCurrentTsBigInt } from './timeHelper' export interface PaymasterInfo { maxReplenishmentPeriod: bigint @@ -78,10 +77,6 @@ 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, @@ -112,9 +107,3 @@ export async function getPaymasterInfo( effectiveTimestamp } } - -export function truncateDecimals(input: string, numDecimals: number): string { - const delimiter = input.includes(',') ? ',' : '.' - const [integerPart, decimalPart = ''] = input.split(delimiter) - return `${integerPart}${delimiter}${decimalPart.slice(0, numDecimals)}` -} diff --git a/src/data/changelog.mdx b/src/data/changelog.mdx new file mode 100644 index 0000000..1feb176 --- /dev/null +++ b/src/data/changelog.mdx @@ -0,0 +1,73 @@ +## ๐Ÿš€ Portal 2.2 + +> This release adds a brand new section dedicated to Staking, allowing users to delegate and check validatos. + +### Features + +#### ๐ŸŽฏ Staking + +Staking was added, [check it out here](https://portal.skale.space/staking). + +todo todo todo + +#### ๐ŸŒˆ New wallets + +test test test + +--- + +## ๐Ÿ’ฐ Portal 2.1 + +> Portal 2.1 adds the new Home page and chains payments management functionality. + +### Features + +#### ๐Ÿ  Brand new Home page + +This release adds a brand new Home page with an instant access to main Portal pages + +#### ๐Ÿ”ฎ Chain management functionality + +todo todo todo + +#### ๐Ÿ“ฑ Better mobile optimization, several UI improvements + +todo todo todo + +### Bugfixes + +#### โš’๏ธ Minor internal bugfixes in the `metaport` library + +`@skalenetwork/metaport` dependency has been updated to the latest version, which includes several minor bugfixes. + +--- + +## ๐Ÿงญ Portal 2.0 + +> Portal 2.0 is a major release that introduces a new unified experience, improved bridging, and a number of new features. + +### Features + +#### โœจ Unified Experience + +A consolidated hub for all things SKALE. Whether you're bridging tokens using the improved SKALE Metaport or checking endpoints for SKALE Chains, it's all here! + +#### ๐ŸŒ‰ Revamped Bridging + +Dive into a reimagined bridging experience, complete with a top-notch UI/UX. SKALE Portal now natively uses the very latest version of SKALE Metaport v2, so all new features and enhancements will be available from the start. + +#### ๐ŸŒˆ RainbowKit Integration + +Seamlessly connect your wallets using RainbowKit. Weโ€™re launching with Metamask, more wallets are coming soon! + +#### ๐ŸŒ Network UI + +The Network UI has been integrated into the SKALE Portal for a smoother experience - now you can get chain endpoints, block explorer links, and other developer info in one place. + +#### ๐Ÿ”— Chains metadata + +Browse info about available SKALE Chains, with categories, descriptions, website links, mapped tokens, and verified contracts. + +#### ๐Ÿ“Š Stats page + +Gain insights with a dedicated statistics page showcasing all SKALE metrics - number of transactions, daily active users, and many more. diff --git a/src/data/contractsMeta.ts b/src/data/contractsMeta.ts new file mode 100644 index 0000000..debc9ab --- /dev/null +++ b/src/data/contractsMeta.ts @@ -0,0 +1,51 @@ +/** + * @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 { type interfaces } from '@skalenetwork/metaport' + +export const CONTRACTS_META: { [key in interfaces.SkaleNetwork]: any } = { + mainnet: { + auto: true + }, + staging: { + auto: false, + manager: '', + allocator: '', + grants: null + }, + legacy: { + auto: false, + manager: '0x27C393Cd6CBD071E5F5F2227a915d3fF3650aeaE', + allocator: '0xCEabf2b0c4F9d75A49a7B1E3e3c3179cDe949C9F', + grants: '0x3982411D90792aCDaCBa37b1fE2f23E4A3E97429' + }, + regression: { + auto: true + }, + testnet: { + auto: false, + manager: '0x8d3D60BFD4c82B3043e5001b7B38B640A2F27CEb', + allocator: '0xDC2F6568608C8dABe101914489A25b07567C96bC', + grants: '0xCEabf2b0c4F9d75A49a7B1E3e3c3179cDe949C9F' + } +} diff --git a/src/data/paymaster.ts b/src/data/paymaster.ts index 32e71f2..a5f86b1 100644 --- a/src/data/paymaster.ts +++ b/src/data/paymaster.ts @@ -11,8 +11,8 @@ export default { launchTs: '0' }, legacy: { - chain: 'international-villainous-zaurak', - address: '0x1f55e3Bce4B973dcC8540188f8F2038DC89891E6', + chain: '', + address: '0x', launchTs: '0' }, regression: { diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index d263264..10cfa23 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -23,15 +23,14 @@ import Container from '@mui/material/Container' import { useParams } from 'react-router-dom' -import Button from '@mui/material/Button' +import { cmn, cls, type MetaportCore, getChainAlias, SkPaper } from '@skalenetwork/metaport' import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' +import LinkRoundedIcon from '@mui/icons-material/LinkRounded' import AdminPanelSettingsRoundedIcon from '@mui/icons-material/AdminPanelSettingsRounded' -import { Link } from 'react-router-dom' -import { cmn, cls, type MetaportCore, getChainAlias, SkPaper } from '@skalenetwork/metaport' - import Paymaster from '../components/Paymaster' +import Breadcrumbs from '../components/Breadcrumbs' export default function Admin(props: { mpc: MetaportCore }) { let { name } = useParams() @@ -43,37 +42,30 @@ export default function Admin(props: { mpc: MetaportCore }) { return ( -
-
-
- - - -
-

|

-
- - - -
-

|

-
-
- -

Manage

-
-
-
+
+ , + url: '/chains' + }, + { + text: alias, + icon: , + url: `/chains/${name}` + }, + { + text: 'Manage', + icon: + } + ]} + />
-
+

Manage {alias}

-

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

+ {alias} admin area - you can manage the chain here

diff --git a/src/pages/Apps.tsx b/src/pages/Apps.tsx index f06a513..df031b9 100644 --- a/src/pages/Apps.tsx +++ b/src/pages/Apps.tsx @@ -1,15 +1,83 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Apps.tsx + * @copyright SKALE Labs 2024-Present + */ +import { type ReactElement } from 'react' +import { Helmet } from 'react-helmet' + import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' -import { cmn, cls } from '@skalenetwork/metaport' +import Box from '@mui/material/Box' +import Grid from '@mui/material/Grid' + +import AppCard from '../components/AppCard' + +import { cmn, cls, type MetaportCore, CHAINS_META, type interfaces } from '@skalenetwork/metaport' +import { META_TAGS } from '../core/meta' + +export default function Apps(props: { mpc: MetaportCore }) { + const chainsMeta: interfaces.ChainsMetadataMap = CHAINS_META[props.mpc.config.skaleNetwork] + const appCards: ReactElement[] = [] + + for (const schainName in chainsMeta) { + if (chainsMeta.hasOwnProperty(schainName)) { + const schain = chainsMeta[schainName] + if (schain.apps) { + for (const appName in schain.apps) { + if (schain.apps.hasOwnProperty(appName)) { + appCards.push( + + + + ) + } + } + } + } + } -export default function Apps() { return ( + + {META_TAGS.chains.title} + + + +

Apps

-

Apps on SKALE Network

+

+ Explore and interact with dApps on SKALE Network. +

+ + + {appCards} + +
) diff --git a/src/pages/Bridge.tsx b/src/pages/Bridge.tsx index 60425a8..58e2c13 100644 --- a/src/pages/Bridge.tsx +++ b/src/pages/Bridge.tsx @@ -151,7 +151,6 @@ export default function Bridge() { } if (tokens.eth && tokens.eth.eth) { setToken(tokens.eth.eth) - return } } }, [tokenParams, tokens]) diff --git a/src/pages/Chains.tsx b/src/pages/Chains.tsx index 74a0b66..052b715 100644 --- a/src/pages/Chains.tsx +++ b/src/pages/Chains.tsx @@ -17,7 +17,7 @@ */ /** - * @file Network.tsx + * @file Chains.tsx * @copyright SKALE Labs 2023-Present */ @@ -35,7 +35,11 @@ import { cmn, cls, type MetaportCore, CHAINS_META, type interfaces } from '@skal import { META_TAGS } from '../core/meta' -export default function Chains(props: { loadSchains: any; schains: any[]; mpc: MetaportCore }) { +export default function Chains(props: { + loadSchains: () => Promise + schains: any[] + mpc: MetaportCore +}) { const [_, setIntervalId] = useState() useEffect(() => { @@ -81,27 +85,21 @@ export default function Chains(props: { loadSchains: any; schains: any[]; mpc: M skaleNetwork={props.mpc.config.skaleNetwork} category="hubs" schains={props.schains.filter( - (schain) => - chainsMeta[schain[0]] && - getPrimaryCategory(chainsMeta[schain[0]].category) === 'Hub' + (schain) => getPrimaryCategory(chainsMeta[schain[0]].category) === 'Hub' )} /> - chainsMeta[schain[0]] && - getPrimaryCategory(chainsMeta[schain[0]].category) === 'Game' + (schain) => getPrimaryCategory(chainsMeta[schain[0]].category) === 'Game' )} /> - chainsMeta[schain[0]] && - getPrimaryCategory(chainsMeta[schain[0]].category) === 'dApp' + (schain) => getPrimaryCategory(chainsMeta[schain[0]].category) === 'dApp' )} /> + +
+

Changelog

+
+

Gateway to the SKALE Ecosystem

+ +
+ +
+
+
+ + ) +} diff --git a/src/pages/Portfolio.tsx b/src/pages/Portfolio.tsx index fb3ffe4..9a15e06 100644 --- a/src/pages/Portfolio.tsx +++ b/src/pages/Portfolio.tsx @@ -86,7 +86,7 @@ export default function Portfolio(props: { mpc: MetaportCore }) { function getTokenDecimals(token: string) { const tokenMetadata = props.mpc.config.tokens[token] - if (!tokenMetadata || !tokenMetadata.decimals) return '18' + if (!tokenMetadata?.decimals) return '18' return tokenMetadata.decimals } diff --git a/src/pages/Schain.tsx b/src/pages/Schain.tsx index 1c1d576..ada1dab 100644 --- a/src/pages/Schain.tsx +++ b/src/pages/Schain.tsx @@ -68,7 +68,7 @@ export default function Schain(props: { loadSchains: any; schains: any[]; mpc: M ) } - if (!chain) { + if (chain === undefined || chain === null) { return

No such chain: {name}

} diff --git a/src/pages/StakeAmount.tsx b/src/pages/StakeAmount.tsx new file mode 100644 index 0000000..2199912 --- /dev/null +++ b/src/pages/StakeAmount.tsx @@ -0,0 +1,167 @@ +/** + * @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 Stake.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useState, useEffect } from 'react' +import { type Signer } from 'ethers' +import { useParams } from 'react-router-dom' +import { cmn, cls, type MetaportCore, SkPaper, type interfaces } from '@skalenetwork/metaport' + +import Container from '@mui/material/Container' +import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' +import PersonSearchRoundedIcon from '@mui/icons-material/PersonSearchRounded' +import SavingsRoundedIcon from '@mui/icons-material/SavingsRounded' +import MonetizationOnRoundedIcon from '@mui/icons-material/MonetizationOnRounded' + +import Loader from '../components/Loader' + +import ValidatorInfo from '../components/delegation/ValidatorInfo' +import Delegate from '../components/delegation/Delegate' +import Breadcrumbs from '../components/Breadcrumbs' +import ConnectWallet from '../components/ConnectWallet' + +import { + DelegationType, + type ISkaleContractsMap, + type IValidator, + type StakingInfoMap +} from '../core/interfaces' + +import ErrorTile from '../components/ErrorTile' +import Headline from '../components/Headline' +import { isDelegationTypeAvailable, isLoaded } from '../core/delegation/staking' +import { getDelegationTypeAlias } from '../core/delegation' + +export default function StakeAmount(props: { + mpc: MetaportCore + validators: IValidator[] + loadValidators: () => void + loadStakingInfo: () => void + sc: ISkaleContractsMap | null + si: StakingInfoMap + address: interfaces.AddressType | undefined + getMainnetSigner: () => Promise +}) { + const { id, delType } = useParams() + const validatorId = Number(id) ?? -1 + const delegationType = Number(delType) ?? DelegationType.REGULAR + + const [currentValidator, setCurrentValidator] = useState(undefined) + const [errorMsg, setErrorMsg] = useState() + + const loaded = isLoaded(props.si) + const available = isDelegationTypeAvailable(props.si, delegationType) + + useEffect(() => { + updateCurrentValidator() + }, []) + + useEffect(() => { + if (props.sc !== null) { + props.loadValidators() + props.loadStakingInfo() + } + }, [props.sc]) + + useEffect(() => { + props.loadStakingInfo() + }, [props.address]) + + useEffect(() => { + updateCurrentValidator() + }, [props.validators]) + + function updateCurrentValidator() { + if (props.validators.length !== 0 && props.validators[validatorId - 1]) { + setCurrentValidator(props.validators.find((validator) => validator.id === validatorId)) + } + } + + if (validatorId === -1) { + return + } + + return ( + + +
+
+ , + url: '/staking' + }, + { + text: 'Choose a validator', + icon: , + url: '/staking/new' + }, + { + text: 'Stake SKL', + icon: + } + ]} + /> +
+ {loaded && available ? ( +
+

+ {getDelegationTypeAlias(delegationType)} delegation +

+
+ ) : null} +
+
+

Stake SKL

+

+ Review validator info and enter delegation amount +

+
+ {currentValidator ? ( + + ) : ( + + )} + + } /> + {props.address ? ( + + ) : ( + + )} +
+
+ ) +} diff --git a/src/pages/StakeValidator.tsx b/src/pages/StakeValidator.tsx new file mode 100644 index 0000000..82ee09e --- /dev/null +++ b/src/pages/StakeValidator.tsx @@ -0,0 +1,109 @@ +/** + * @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 Stake.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useState, useEffect } from 'react' + +import Container from '@mui/material/Container' +import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' +import PersonSearchRoundedIcon from '@mui/icons-material/PersonSearchRounded' + +import { cmn, cls, type MetaportCore, SkPaper } from '@skalenetwork/metaport' + +import { + DelegationType, + type ISkaleContractsMap, + type IValidator, + type StakingInfoMap +} from '../core/interfaces' + +import Validators from '../components/delegation/Validators' +import DelegationTypeSelect from '../components/delegation/DelegationTypeSelect' +import Breadcrumbs from '../components/Breadcrumbs' +import { compareEnum } from '../core/helper' +import SkStack from '../components/SkStack' + +export default function StakeValidator(props: { + mpc: MetaportCore + validators: IValidator[] + loadValidators: () => void + loadStakingInfo: () => void + sc: ISkaleContractsMap | null + si: StakingInfoMap +}) { + const [delegationType, setDelegationType] = useState(DelegationType.REGULAR) + const [validatorId, setValidatorId] = useState() + + const handleChange = (event: any) => { + setDelegationType(event.target.value) + } + + useEffect(() => { + if (props.sc) { + props.loadValidators() + props.loadStakingInfo() + } + }, [props.sc]) + + return ( + + + +
+ , + url: '/staking' + }, + { + text: 'Choose a validator', + icon: + } + ]} + /> +
+
+ +
+
+
+

Stake SKL

+

Choose a validator to delegate your SKL

+
+ +
+
+ ) +} diff --git a/src/pages/Staking.tsx b/src/pages/Staking.tsx new file mode 100644 index 0000000..713f3a4 --- /dev/null +++ b/src/pages/Staking.tsx @@ -0,0 +1,288 @@ +/** + * @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 Staking.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { Link } from 'react-router-dom' +import { type Signer } from 'ethers' +import debug from 'debug' +import { useEffect, useState } from 'react' +import { + cmn, + cls, + styles, + SkPaper, + type MetaportCore, + type interfaces, + sendTransaction +} from '@skalenetwork/metaport' + +import Container from '@mui/material/Container' +import Stack from '@mui/material/Stack' +import Button from '@mui/material/Button' + +import VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded' +import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded' +import AllInboxRoundedIcon from '@mui/icons-material/AllInboxRounded' +import QueueRoundedIcon from '@mui/icons-material/QueueRounded' +import PieChartRoundedIcon from '@mui/icons-material/PieChartRounded' + +import Delegations from '../components/delegation/Delegations' + +import { + type ISkaleContractsMap, + type IValidator, + DelegationType, + type StakingInfoMap, + type IRewardInfo, + type IDelegationInfo, + type ContractType +} from '../core/interfaces' + +import Summary from '../components/delegation/Summary' +import { Collapse } from '@mui/material' +import { initActionContract } from '../core/contracts' +import { BALANCE_UPDATE_INTERVAL_MS, DEFAULT_ERROR_MSG } from '../core/constants' +import ErrorTile from '../components/ErrorTile' +import ConnectWallet from '../components/ConnectWallet' +import Headline from '../components/Headline' +import Message from '../components/Message' + +debug.enable('*') +const log = debug('portal:pages:Staking') + +export default function Staking(props: { + mpc: MetaportCore + validators: IValidator[] + loadValidators: () => Promise + loadStakingInfo: () => Promise + sc: ISkaleContractsMap | null + si: StakingInfoMap + address: interfaces.AddressType | undefined + customAddress: interfaces.AddressType | undefined + getMainnetSigner: () => Promise + isXs: boolean +}) { + const [loading, setLoading] = useState(false) + const [errorMsg, setErrorMsg] = useState() + + useEffect(() => { + props.loadValidators() + props.loadStakingInfo() + const intervalId = setInterval(() => { + props.loadStakingInfo() + }, BALANCE_UPDATE_INTERVAL_MS) + log(`Updating staking info interval: ${Number(intervalId)}`) + return () => { + log(`Clearing interval: ${Number(intervalId)}`) + clearInterval(intervalId) // Clear interval on component unmount + } + }, [props.address, props.sc]) + + async function processTx( + delegationType: DelegationType, + txName: string, + txArgs: any[], + contractType: ContractType + ) { + if (props.sc === null || props.address === undefined) return + log('processTx:', txName, txArgs, contractType, delegationType) + try { + const signer = await props.getMainnetSigner() + const contract = await initActionContract( + signer, + delegationType, + props.address, + props.mpc.config.skaleNetwork, + contractType + ) + const res = await sendTransaction(contract[txName], txArgs) + if (!res.status) { + setErrorMsg(res.err?.name) + } else { + setErrorMsg(undefined) + await props.loadStakingInfo() + } + setLoading(false) + } catch (err: any) { + console.error(err) + setErrorMsg(err.message ? err.message : DEFAULT_ERROR_MSG) + setLoading(false) + } + } + + async function retrieveRewards(rewardInfo: IRewardInfo) { + setLoading(rewardInfo) + processTx( + rewardInfo.delegationType, + 'withdrawBounty', + [rewardInfo.validatorId, props.address], + 'distributor' + ) + } + + async function unstake(delegationInfo: IDelegationInfo) { + setLoading(delegationInfo) + processTx( + delegationInfo.delegationType, + 'requestUndelegation', + [delegationInfo.delegationId], + 'delegation' + ) + } + + async function cancelRequest(delegationInfo: IDelegationInfo) { + setLoading(delegationInfo) + processTx( + delegationInfo.delegationType, + 'cancelPendingDelegation', + [delegationInfo.delegationId], + 'delegation' + ) + } + + async function retrieveUnlocked(rewardInfo: IRewardInfo): Promise { + setLoading(rewardInfo) + processTx(rewardInfo.delegationType, 'retrieve', [], 'distributor') + } + + return ( + + +
+
+

Staking

+

+ {props.isXs + ? 'Manage your delegations' + : 'Delegate, review delegations and withdraw staking rewards'} +

+
+
+ {loading !== false || props.customAddress !== undefined ? ( + + ) : ( + + + + )} +
+
+
+ + {props.customAddress !== undefined ? ( + } + link="/staking" + linkText="click to exit" + type="warning" + /> + ) : null} + + + + + + + } /> + + + + + + + + + + + + + + + + + + + + + + + + } /> +
+ +
+ Connect your wallet to view delegations +
+
+
+
+ + ) +} diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index f38afed..2c26830 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -1,3 +1,26 @@ +/** + * @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 Start.tsx + * @copyright SKALE Labs 2023-Present + */ + import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import Box from '@mui/material/Box' @@ -6,14 +29,14 @@ import Grid from '@mui/material/Grid' import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontalCircleOutlined' import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' import InsertChartOutlinedIcon from '@mui/icons-material/InsertChartOutlined' -// import AppsOutlinedIcon from '@mui/icons-material/AppsOutlined' -// import WalletOutlinedIcon from '@mui/icons-material/WalletOutlined' +import WalletOutlinedIcon from '@mui/icons-material/WalletOutlined' import PageCard from '../components/PageCard' +import Message from '../components/Message' import { cmn, cls } from '@skalenetwork/metaport' -export default function Start() { +export default function Start(props: { isXs: boolean }) { return ( @@ -23,6 +46,17 @@ export default function Start() {

Gateway to the SKALE Ecosystem

+ +

Update 2.2

+
+ } + /> @@ -39,13 +73,6 @@ export default function Start() { icon={} /> - {/* - } - /> - */} } /> - {/* + } /> - */} + diff --git a/src/pages/Validators.tsx b/src/pages/Validators.tsx new file mode 100644 index 0000000..a6cb7e1 --- /dev/null +++ b/src/pages/Validators.tsx @@ -0,0 +1,63 @@ +/** + * @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 Validators.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useEffect } from 'react' + +import Container from '@mui/material/Container' +import { cmn, cls, type MetaportCore } from '@skalenetwork/metaport' + +import Validators from '../components/delegation/Validators' + +import { DelegationType, type ISkaleContractsMap, type IValidator } from '../core/interfaces' + +export default function ValidatorsPage(props: { + mpc: MetaportCore + validators: IValidator[] + sc: ISkaleContractsMap | null + loadValidators: () => void +}) { + useEffect(() => { + if (props.sc !== null) { + props.loadValidators() + } + }, [props.sc]) + + return ( + +
+

Validators

+
+

List of validators on SKALE Network

+
+ {}} + delegationType={DelegationType.REGULAR} + size="lg" + /> +
+
+ ) +} diff --git a/vercel.json b/vercel.json index eec4297..61bd848 100644 --- a/vercel.json +++ b/vercel.json @@ -5,7 +5,7 @@ "headers": [ { "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'self' 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' * data:; connect-src 'self' wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" + "value": "default-src 'none'; script-src 'self' 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' * data:; connect-src 'self' https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" }, { "key": "X-Content-Type-Options",