From 7da7107fbb8182582d6a709b05d5a98820cfd3a3 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 31 Oct 2024 16:49:36 +0000 Subject: [PATCH 01/16] Add Validator page template --- package.json | 2 +- src/Router.tsx | 12 ++++++++ src/core/meta.ts | 5 ++++ src/pages/Validator.tsx | 61 ++++++++++++++++++++++++++++++++++++++++ src/pages/Validators.tsx | 9 +++++- 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/pages/Validator.tsx diff --git a/package.json b/package.json index ee48067..b88dd05 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "portal", "private": true, - "version": "3.1.0", + "version": "3.2.0", "type": "module", "scripts": { "build:testnet": "NETWORK_NAME=testnet bash build.sh", diff --git a/src/Router.tsx b/src/Router.tsx index 61d0a8e..6cd43aa 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -64,6 +64,7 @@ import Staking from './pages/Staking' import StakeValidator from './pages/StakeValidator' import StakeAmount from './pages/StakeAmount' import Validators from './pages/Validators' +import Validator from './pages/Validator' import Onramp from './pages/Onramp' import TermsModal from './components/TermsModal' import Changelog from './pages/Changelog' @@ -369,6 +370,17 @@ export default function Router() { /> } /> + + } + /> . + */ + +/** + * @file Validator.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 { type ISkaleContractsMap, type IValidator } from '../core/interfaces' +import SkPageInfoIcon from '../components/SkPageInfoIcon' +import { META_TAGS } from '../core/meta' + +export default function Validator(props: { + mpc: MetaportCore + validators: IValidator[] + sc: ISkaleContractsMap | null + loadValidators: () => void +}) { + useEffect(() => { + if (props.sc !== null) { + props.loadValidators() + } + }, [props.sc]) + + return ( + +
+
+

Manage Validator

+

+ {META_TAGS.validator.description} +

+
+ +
+
+
+
+ ) +} diff --git a/src/pages/Validators.tsx b/src/pages/Validators.tsx index f3b71e0..3ec95ea 100644 --- a/src/pages/Validators.tsx +++ b/src/pages/Validators.tsx @@ -31,6 +31,8 @@ import Validators from '../components/delegation/Validators' import { DelegationType, type ISkaleContractsMap, type IValidator } from '../core/interfaces' import SkPageInfoIcon from '../components/SkPageInfoIcon' import { META_TAGS } from '../core/meta' +import { Link } from 'react-router-dom' +import { Button } from '@mui/material' export default function ValidatorsPage(props: { mpc: MetaportCore @@ -53,6 +55,11 @@ export default function ValidatorsPage(props: { List of validators on SKALE Network

+ + +
@@ -60,7 +67,7 @@ export default function ValidatorsPage(props: { mpc={props.mpc} validators={props.validators} validatorId={0} - setValidatorId={(): void => {}} + setValidatorId={(): void => { }} delegationType={DelegationType.REGULAR} size="lg" /> From 385402318b0dfd11342634888291b143868c73bd Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 12 Nov 2024 18:46:56 +0000 Subject: [PATCH 02/16] Add basic validator management page - WIP --- packages/core/bun.lockb | Bin 4260 -> 1322 bytes packages/core/package.json | 1 - packages/core/src/types/staking/Delegation.ts | 19 ++ skale-network | 2 +- src/App.scss | 28 +-- src/Router.tsx | 30 ++- src/SkDrawer.tsx | 2 +- src/_variables.scss | 3 +- src/components/delegation/Delegate.tsx | 11 +- src/components/delegation/Delegation.tsx | 51 +++-- .../delegation/DelegationTotals.tsx | 86 +++++++++ .../delegation/DelegationTypeSelect.tsx | 16 +- src/components/delegation/Delegations.tsx | 64 +++---- .../delegation/DelegationsToValidator.tsx | 42 ++--- .../delegation/RetrieveRewardModal.tsx | 23 ++- src/components/delegation/Reward.tsx | 28 ++- src/components/delegation/ShowMoreButton.tsx | 53 ++++++ src/components/delegation/SortToggle.tsx | 69 +++++++ src/components/delegation/Summary.tsx | 29 ++- src/components/delegation/ValidatorBadges.tsx | 6 +- src/components/delegation/ValidatorCard.tsx | 6 +- src/components/delegation/ValidatorInfo.tsx | 46 +++-- src/components/delegation/ValidatorLogo.tsx | 4 +- src/components/delegation/Validators.tsx | 9 +- src/core/constants.ts | 3 + src/core/contracts.ts | 21 +-- src/core/delegation/delegations.ts | 174 +++++++++++------ src/core/delegation/staking.ts | 53 +++--- src/core/delegation/validators.ts | 132 ++++++++++--- src/core/explorer.ts | 4 +- src/core/helper.ts | 7 +- src/core/interfaces/Beneficiary.ts | 34 ---- src/core/interfaces/Delegation.ts | 72 ------- src/core/interfaces/Delegator.ts | 36 ---- src/core/interfaces/SkaleContract.ts | 39 ---- src/core/interfaces/Staking.ts | 31 --- src/core/interfaces/Validator.ts | 49 ----- src/core/interfaces/index.ts | 28 --- src/core/meta.ts | 2 +- src/pages/App.tsx | 4 +- src/pages/StakeAmount.tsx | 31 +-- src/pages/StakeValidator.tsx | 23 +-- src/pages/Staking.tsx | 50 +++-- src/pages/Validator.tsx | 178 ++++++++++++++++-- src/pages/Validators.tsx | 25 ++- 45 files changed, 923 insertions(+), 701 deletions(-) create mode 100644 src/components/delegation/DelegationTotals.tsx create mode 100644 src/components/delegation/ShowMoreButton.tsx create mode 100644 src/components/delegation/SortToggle.tsx delete mode 100644 src/core/interfaces/Beneficiary.ts delete mode 100644 src/core/interfaces/Delegation.ts delete mode 100644 src/core/interfaces/Delegator.ts delete mode 100644 src/core/interfaces/SkaleContract.ts delete mode 100644 src/core/interfaces/Staking.ts delete mode 100644 src/core/interfaces/Validator.ts delete mode 100644 src/core/interfaces/index.ts diff --git a/packages/core/bun.lockb b/packages/core/bun.lockb index c8746163a9ffeac645829ceeb48d24872384b95e..e6b0635c946d7faa665584cf574d7a8ab11f1dfa 100755 GIT binary patch delta 256 zcmZ3YxQc6no~F5ADc_C553hEbP8!)m+G6L0rFbu%? zlWkcxPgZ6WnS6xZW%3FRBd!Bb!>=$+&f)Um18ZW2Xk%fXe2hzt%K*wX0dbYUDl(u# uP0W*vxYZaLCa>p~<+=bB6kwTri^pZM0 literal 4260 zcmd^Cc~BE)6i;F}RlpXn;t>?>SlJB;w+)nnSc_m)L`MZQ2^)xn1a>#zS&@oIwF;va z>xHN&g0&tM6}-lAtO!-6fMqJ8RY#@ZHHcCV==-wyWOWc|`%j&|%;tU9@Atj;zGL4G zb(R_#TB_3OrIbEdqB81j;URPeb-YHaqX>Pm_sX2Up^(P>rCmI7rYFJDf!JXII&SFUD@|mH6|8M^`IKg&gfa{zp+m0=@_oV7C~w4Lla`bVV?J5)AePyd`LehXA*C#(~FO zgn^-eNB*GTchxV9{{R3VoAKQMkK3Wxq2O^5JUnje5NPhiV>19h3g&+g+U?;%G3bh5 z`~~y;DE@p=QLK`Lz!EoyDysHY)bsmL8RIRWAcy%;{Y{QHG5k8ldqaT$JUS7*-~n^G zEQka1AZ-N?+CxbA8gb%-)t=^a@XP;>uf@LQ?>J+!x|2WP6ACz9KDD?q;@0D^Wt(of zQ+AEbKiMp&n>`bPt3%6*D`r0-8ihIeD+;sstP(|x|F&cnSt6cTR+qH>fsAs^at&Ir zE$_(<9xwJESt4$lHgbhWZOVfu4ZZfMXLzKy_*@RzKVVDZk5{)Qj}sTV7-kOMyTfs6 ztxHzequB+94QCGJ>qfmlNbVdsr({gzg^fI3bT+VvpYBSQyu7Va8l!t|wlfUAY_W9F zqsoXo6KcYTER1$>YAnC$Y=0**=&*zM%idu}vRaErg=J8gujrRc1UI{#UA{<>?I20Bz$4Px&e7=3!?d@3jXgm2wG zucUadIvy|Dt60QNwa5DG%H3a6;GZY%cXR&cRmS;^>m4e$Ee~`*Paa9n?NQVidwRL` zs#4$I`iEQR#aJ#V?xxQ4np)ko>EvqV&MF=+ep4ZCamwEDI|HsAB+{s9Uj)gv77y<0 zE~)C*fYJ>IL!F)vOlsKj{Bl*UuJ4pVMe?V=?4CR*e@L*?q4iHE@_6z2 zH1XE47AM$S2~Bn}H@=n}7*qN1LUnohp{0jL*d*8a^&6LQV2|p0RNfW8?3^2x(HWaF z^CA?x{=6bAY-nY#kA88oKk&9EcyD0c-k5j2?Jcgq>u&uI?9Vg5ijJGLt%9M|q*l+c zN}Z0xi6%LjrYL`j+)cu0=TQcYtHeX1)M;b&1`U3d30hh?K|w?3>aMw`2PA|@Y!ya# zxjz8kLV@-Wy7$o?jm`?X`_Mgw?i6(Xeat*@UEo7Jh!5F;u!sY3Ax^}N=P;Yxy1~p@ z-UB*8Y!x%)UQtFLaM|mXy|Ol!aUP8GS8L%%B>XrqddsKc4A)v1?LqpY90upWICm8Z zeMp%%DQnA;ads;b%1IB>4cKrVjq_iTa2d3Mk*PS-#u>6mC<8iAQs$0xahyX#4=FdX z6#y1x>p06sJ?ONQBw z9{9^hf!18s-jbWWPsxegcmF>tR#R?oZSd6ISw#QA=MjVr~th|rK6;3Be=|%D{)FD z4l2qRTB*^@g|A44(4&t=9h~D1=58N=Y~d3S+%D(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 [sc, setSc] = useState(null) + const [validators, setValidators] = useState([]) + const [validator, setValidator] = useState(null) + const [validatorDelegations, setValidatorDelegations] = useState(null) + const [si, setSi] = useState({ 0: null, 1: null, 2: null }) const [customAddress, setCustomAddress] = useState(undefined) @@ -199,6 +200,15 @@ export default function Router() { setValidators(validatorsData) } + async function loadValidator(address: types.AddressType) { + if (!sc || !address) return + const validatorData = await getValidator(sc.validatorService, address) + setValidator(validatorData) + if (validatorData && validatorData.id) { + setValidatorDelegations(await getValidatorDelegations(sc, validatorData.id)) + } + } + async function loadStakingInfo() { if (!sc) return setSi(await getStakingInfoMap(sc, customAddress ?? address)) @@ -375,9 +385,13 @@ export default function Router() { element={ } /> diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index 7403511..d0c666d 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -133,7 +133,7 @@ export default function SkDrawer() { diff --git a/src/_variables.scss b/src/_variables.scss index 3a27519..8bd89e6 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -3,9 +3,10 @@ $sk-bg: #191919; $sk-bg-prim: #000000; $sk-btn-height: 47px; +$sk-prim: #93B8EC; $border-color: #353535; $border-color-light: #171616; // legacy $sk-paper-color: rgb(136 135 135 / 15%); -$sk-gray-background-color: rgba(161, 161, 161, 0.2); +$sk-gray-background-color: rgba(161, 161, 161, 0.2); \ No newline at end of file diff --git a/src/components/delegation/Delegate.tsx b/src/components/delegation/Delegate.tsx index 98b013d..1d2cda2 100644 --- a/src/components/delegation/Delegate.tsx +++ b/src/components/delegation/Delegate.tsx @@ -28,7 +28,6 @@ import { cmn, cls, TokenIcon, - type interfaces, fromWei, styles, toWei, @@ -49,7 +48,6 @@ import SkStack from '../SkStack' import ErrorTile from '../ErrorTile' import Loader from '../Loader' -import { type DelegationType, type IValidator, type StakingInfoMap } from '../../core/interfaces' import { formatBalance } from '../../core/helper' import { DEFAULT_DELEGATION_INFO, @@ -58,17 +56,18 @@ import { DEFAULT_ERROR_MSG } from '../../core/constants' import { initActionContract } from '../../core/contracts' +import { types } from '@/core' debug.enable('*') const log = debug('portal:pages:Delegate') export default function Delegate(props: { mpc: MetaportCore - validator: IValidator | undefined - si: StakingInfoMap + validator: types.staking.IValidator | undefined + si: types.staking.StakingInfoMap getMainnetSigner: () => Promise - address: interfaces.AddressType - delegationType: DelegationType + address: types.AddressType + delegationType: types.staking.DelegationType loaded: boolean delegationTypeAvailable: boolean errorMsg: string | undefined diff --git a/src/components/delegation/Delegation.tsx b/src/components/delegation/Delegation.tsx index 4bac474..17e2a4e 100644 --- a/src/components/delegation/Delegation.tsx +++ b/src/components/delegation/Delegation.tsx @@ -21,7 +21,7 @@ */ import { useState } from 'react' -import { cmn, cls, styles, type interfaces } from '@skalenetwork/metaport' +import { cmn, cls, styles } from '@skalenetwork/metaport' import { Collapse, Grid, Tooltip } from '@mui/material' import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded' @@ -31,35 +31,28 @@ 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 { types } from '@/core' + import { DelegationSource, DelegationState, getDelegationSource, - getKeyByValue, - getValidatorById + getKeyByValue } 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 + delegation: types.staking.IDelegation + validator: types.staking.IValidator + delegationType: types.staking.DelegationType + unstake?: (delegationInfo: types.staking.IDelegationInfo) => Promise + cancelRequest?: (delegationInfo: types.staking.IDelegationInfo) => Promise + loading: types.staking.IRewardInfo | types.staking.IDelegationInfo | false isXs: boolean - customAddress: interfaces.AddressType | undefined + customAddress: types.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) @@ -72,7 +65,7 @@ export default function Delegation(props: { delId === DelegationState.PROPOSED || delId === DelegationState.ACCEPTED - const delegationInfo: IDelegationInfo = { + const delegationInfo: types.staking.IDelegationInfo = { delegationId: props.delegation.id, delegationType: props.delegationType } @@ -98,7 +91,7 @@ export default function Delegation(props: { } } - if (!validator) return + if (!props.validator) return return (
@@ -124,12 +117,12 @@ export default function Delegation(props: { {formatBigIntTimestampSeconds(props.delegation.created)}

- {props.delegationType === DelegationType.ESCROW ? ( + {props.delegationType === types.staking.DelegationType.ESCROW ? ( ) : null} - {props.delegationType === DelegationType.ESCROW2 ? ( + {props.delegationType === types.staking.DelegationType.ESCROW2 ? ( @@ -139,7 +132,7 @@ export default function Delegation(props: {
-
+

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

@@ -150,7 +143,7 @@ export default function Delegation(props: {
-
+

{source}

@@ -185,26 +178,26 @@ export default function Delegation(props: {

) : null} - {Number(props.delegation.stateId) === DelegationState.DELEGATED ? ( + {Number(props.delegation.stateId) === DelegationState.DELEGATED && props.unstake ? ( { - await props.unstake(delegationInfo) + props.unstake && (await props.unstake(delegationInfo)) }} disabled={props.loading !== false || props.customAddress !== undefined} /> ) : null} - {Number(props.delegation.stateId) === DelegationState.PROPOSED ? ( + {Number(props.delegation.stateId) === DelegationState.PROPOSED && props.cancelRequest ? ( { - await props.cancelRequest(delegationInfo) + props.cancelRequest && (await props.cancelRequest(delegationInfo)) }} disabled={props.loading !== false || props.customAddress !== undefined} /> diff --git a/src/components/delegation/DelegationTotals.tsx b/src/components/delegation/DelegationTotals.tsx new file mode 100644 index 0000000..ff0c0a8 --- /dev/null +++ b/src/components/delegation/DelegationTotals.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 DelegationTotals.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useMemo } from 'react' +import { cls, styles } from '@skalenetwork/metaport' +import { type types } from '@/core' + +import InboxRoundedIcon from '@mui/icons-material/InboxRounded' +import TaskAltRoundedIcon from '@mui/icons-material/TaskAltRounded' +import DonutLargeRoundedIcon from '@mui/icons-material/DonutLargeRounded' +import LibraryAddCheckRoundedIcon from '@mui/icons-material/LibraryAddCheckRounded' + +import { calculateDelegationTotals } from '../../core/delegation/delegations' +import { formatBalance } from '../../core/helper' + +import SkStack from '../SkStack' +import Tile from '../Tile' + +interface DelegationTotalsProps { + delegations: types.staking.IDelegation[] + className?: string +} + +const DelegationTotals: React.FC = ({ delegations, className }) => { + const totals = useMemo( + () => (delegations ? calculateDelegationTotals(delegations) : null), + [delegations] + ) + + const getTileText = (status: string, count?: number) => `${status}${count ? ` (${count})` : ''}` + + return ( + + } + /> + } + /> + } + /> + } + /> + + ) +} + +export default DelegationTotals diff --git a/src/components/delegation/DelegationTypeSelect.tsx b/src/components/delegation/DelegationTypeSelect.tsx index d2055b0..6597e3d 100644 --- a/src/components/delegation/DelegationTypeSelect.tsx +++ b/src/components/delegation/DelegationTypeSelect.tsx @@ -22,14 +22,14 @@ 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' +import { types } from '@/core' export default function DelegationTypeSelect(props: { - delegationType: DelegationType + delegationType: types.staking.DelegationType handleChange: (event: any) => void - si: StakingInfoMap + si: types.staking.StakingInfoMap }) { return (
@@ -39,16 +39,16 @@ export default function DelegationTypeSelect(props: { value={props.delegationType} onChange={props.handleChange} > - - {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 index 0916b35..0af660e 100644 --- a/src/components/delegation/Delegations.tsx +++ b/src/components/delegation/Delegations.tsx @@ -21,46 +21,38 @@ * @copyright SKALE Labs 2024-Present */ -import { cmn, cls, styles, type interfaces } from '@skalenetwork/metaport' +import { cmn, cls, styles } 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' +import { types } from '@/core' export default function Delegations(props: { - si: StakingInfoMap - validators: IValidator[] - retrieveRewards: (rewardInfo: IRewardInfo) => Promise - loading: IRewardInfo | IDelegationInfo | false + si: types.staking.StakingInfoMap + validators: types.staking.IValidator[] + retrieveRewards: (rewardInfo: types.staking.IRewardInfo) => Promise + loading: types.staking.IRewardInfo | types.staking.IDelegationInfo | false setErrorMsg: (errorMsg: string | undefined) => void errorMsg: string | undefined - unstake: (delegationInfo: IDelegationInfo) => Promise - cancelRequest: (delegationInfo: IDelegationInfo) => Promise + unstake: (delegationInfo: types.staking.IDelegationInfo) => Promise + cancelRequest: (delegationInfo: types.staking.IDelegationInfo) => Promise isXs: boolean - address: interfaces.AddressType | undefined - customAddress: interfaces.AddressType | undefined - customRewardAddress: interfaces.AddressType | undefined - setCustomRewardAddress: (customRewardAddress: interfaces.AddressType | undefined) => void + address: types.AddressType | undefined + customAddress: types.AddressType | undefined + customRewardAddress: types.AddressType | undefined + setCustomRewardAddress: (customRewardAddress: types.AddressType | undefined) => void }) { - const loaded = props.si[DelegationType.REGULAR] !== null + const loaded = props.si[types.staking.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) + (!props.si[types.staking.DelegationType.REGULAR] || + props.si[types.staking.DelegationType.REGULAR]?.delegations.length === 0) && + (!props.si[types.staking.DelegationType.ESCROW] || + props.si[types.staking.DelegationType.ESCROW]?.delegations.length === 0) && + (!props.si[types.staking.DelegationType.ESCROW2] || + props.si[types.staking.DelegationType.ESCROW2]?.delegations.length === 0) return (
) : (
- {props.si[DelegationType.REGULAR]?.delegations.map( - (delegationsToValidator: IDelegationsToValidator, index: number) => ( + {props.si[types.staking.DelegationType.REGULAR]?.delegations.map( + (delegationsToValidator: types.staking.IDelegationsToValidator, index: number) => ( ) )} - {props.si[DelegationType.ESCROW]?.delegations.map( - (delegationsToValidator: IDelegationsToValidator, index: number) => ( + {props.si[types.staking.DelegationType.ESCROW]?.delegations.map( + (delegationsToValidator: types.staking.IDelegationsToValidator, index: number) => ( ) )} - {props.si[DelegationType.ESCROW2]?.delegations.map( - (delegationsToValidator: IDelegationsToValidator, index: number) => ( + {props.si[types.staking.DelegationType.ESCROW2]?.delegations.map( + (delegationsToValidator: types.staking.IDelegationsToValidator, index: number) => ( Promise - loading: IRewardInfo | IDelegationInfo | false - unstake: (delegationInfo: IDelegationInfo) => Promise - cancelRequest: (delegationInfo: IDelegationInfo) => Promise + delegationsToValidator: types.staking.IDelegationsToValidator + validators: types.staking.IValidator[] + delegationType: types.staking.DelegationType + retrieveRewards: (rewardInfo: types.staking.IRewardInfo) => Promise + loading: types.staking.IRewardInfo | types.staking.IDelegationInfo | false + unstake: (delegationInfo: types.staking.IDelegationInfo) => Promise + cancelRequest: (delegationInfo: types.staking.IDelegationInfo) => Promise isXs: boolean - address: interfaces.AddressType | undefined - customAddress: interfaces.AddressType | undefined - customRewardAddress: interfaces.AddressType | undefined - setCustomRewardAddress: (customRewardAddress: interfaces.AddressType | undefined) => void + address: types.AddressType | undefined + customAddress: types.AddressType | undefined + customRewardAddress: types.AddressType | undefined + setCustomRewardAddress: (customRewardAddress: types.AddressType | undefined) => void }) { const [open, setOpen] = useState(true) + const validator = getValidatorById(props.validators, props.delegationsToValidator.validatorId) + if (!validator) return return (
{props.delegationsToValidator.delegations.map( - (delegation: IDelegation, index: number) => ( + (delegation: types.staking.IDelegation, index: number) => ( void + address: types.AddressType | undefined + customRewardAddress: types.AddressType | undefined + setCustomRewardAddress: (customRewardAddress: types.AddressType | undefined) => void retrieveRewards: () => void loading: boolean disabled: boolean @@ -65,7 +64,7 @@ export default function RetrieveRewardModal(props: { const saveAddress = () => { if (isAddress(inputAddress)) { - props.setCustomRewardAddress(inputAddress as interfaces.AddressType) + props.setCustomRewardAddress(inputAddress as types.AddressType) setEdit(false) setErrorMsg(undefined) } else { diff --git a/src/components/delegation/Reward.tsx b/src/components/delegation/Reward.tsx index 6b44f47..cfb0843 100644 --- a/src/components/delegation/Reward.tsx +++ b/src/components/delegation/Reward.tsx @@ -20,7 +20,7 @@ * @copyright SKALE Labs 2024-Present */ -import { cmn, cls, styles, type interfaces } from '@skalenetwork/metaport' +import { cmn, cls, styles } from '@skalenetwork/metaport' import { Grid } from '@mui/material' import LoadingButton from '@mui/lab/LoadingButton' @@ -30,30 +30,24 @@ 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' import RetrieveRewardModal from './RetrieveRewardModal' +import { types } from '@/core' export default function Reward(props: { - validators: IValidator[] - delegationsToValidator: IDelegationsToValidator + validators: types.staking.IValidator[] + delegationsToValidator: types.staking.IDelegationsToValidator setOpen: (open: boolean) => void open: boolean - retrieveRewards: (rewardInfo: IRewardInfo) => Promise - loading: IRewardInfo | IDelegationInfo | false - delegationType: DelegationType + retrieveRewards: (rewardInfo: types.staking.IRewardInfo) => Promise + loading: types.staking.IRewardInfo | types.staking.IDelegationInfo | false + delegationType: types.staking.DelegationType isXs: boolean - address: interfaces.AddressType | undefined - customAddress: interfaces.AddressType | undefined - customRewardAddress: interfaces.AddressType | undefined - setCustomRewardAddress: (customRewardAddress: interfaces.AddressType | undefined) => void + address: types.AddressType | undefined + customAddress: types.AddressType | undefined + customRewardAddress: types.AddressType | undefined + setCustomRewardAddress: (customRewardAddress: types.AddressType | undefined) => void }) { const validator = getValidatorById(props.validators, props.delegationsToValidator.validatorId) const rewardsAmount = formatBalance(props.delegationsToValidator.rewards, 'SKL') diff --git a/src/components/delegation/ShowMoreButton.tsx b/src/components/delegation/ShowMoreButton.tsx new file mode 100644 index 0000000..308497c --- /dev/null +++ b/src/components/delegation/ShowMoreButton.tsx @@ -0,0 +1,53 @@ +/** + * @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 ShowMoreButton.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { Button } from '@mui/material' +import { cls, cmn } from '@skalenetwork/metaport' +import ExpandCircleDownRoundedIcon from '@mui/icons-material/ExpandCircleDownRounded' + +interface ShowMoreButtonProps { + onClick: () => void + remainingItems: number + loading?: boolean + className?: string +} + +const ShowMoreButton: React.FC = ({ + onClick, + remainingItems, + loading, + className +}) => { + return ( + + ) +} + +export default ShowMoreButton diff --git a/src/components/delegation/SortToggle.tsx b/src/components/delegation/SortToggle.tsx new file mode 100644 index 0000000..7a6d183 --- /dev/null +++ b/src/components/delegation/SortToggle.tsx @@ -0,0 +1,69 @@ +/** + * @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 SortToggle.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useState } from 'react' +import { cls, cmn, styles } from '@skalenetwork/metaport' +import { ToggleButtonGroup, ToggleButton } from '@mui/material' +import FilterListRoundedIcon from '@mui/icons-material/FilterListRounded' +import ContrastRoundedIcon from '@mui/icons-material/ContrastRounded' + +interface SortToggleProps { + onChange: (sort: 'id' | 'status') => void + className?: string +} + +const SortToggle: React.FC = ({ onChange, className }) => { + const [sortBy, setSortBy] = useState<'id' | 'status'>('id') + + const handleChange = (_: React.MouseEvent, newSort: 'id' | 'status') => { + if (newSort !== null) { + setSortBy(newSort) + onChange(newSort) + } + } + + return ( + + + + Sort by ID + + + + Sort by Status + + + ) +} + +export default SortToggle diff --git a/src/components/delegation/Summary.tsx b/src/components/delegation/Summary.tsx index 6d9745a..d105ed3 100644 --- a/src/components/delegation/Summary.tsx +++ b/src/components/delegation/Summary.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2024-Present */ -import { cmn, cls, styles, TokenIcon, type interfaces } from '@skalenetwork/metaport' +import { cmn, cls, styles, TokenIcon } from '@skalenetwork/metaport' import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' import AccountBalanceRoundedIcon from '@mui/icons-material/AccountBalanceRounded' @@ -36,16 +36,11 @@ 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' +import { types } from '@/core' -const icons: { [key in DelegationType]: any } = { +const icons: { [key in types.staking.DelegationType]: any } = { 0: , 1: , 2: @@ -54,20 +49,20 @@ const icons: { [key in DelegationType]: any } = { 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 + type: types.staking.DelegationType + accountInfo: types.staking.IDelegatorInfo | undefined + retrieveUnlocked: (rewardInfo: types.staking.IRewardInfo) => Promise + loading: types.staking.IRewardInfo | types.staking.IDelegationInfo | false + customAddress: types.AddressType | undefined isXs: boolean }) { function getTitle() { - if (props.type === DelegationType.ESCROW) return 'Escrow' - if (props.type === DelegationType.ESCROW2) return 'Grant Escrow' + if (props.type === types.staking.DelegationType.ESCROW) return 'Escrow' + if (props.type === types.staking.DelegationType.ESCROW2) return 'Grant Escrow' return 'Account' } - const rewardInfo: IRewardInfo = { + const rewardInfo: types.staking.IRewardInfo = { validatorId: SUMMARY_VALIDATOR_ID, delegationType: props.type } @@ -95,7 +90,7 @@ export default function Summary(props: { icon={} childrenRi={ - {props.type !== DelegationType.REGULAR ? ( + {props.type !== types.staking.DelegationType.REGULAR ? (
@@ -42,7 +42,7 @@ export function ValidatorBadge(props: { validator: IValidator; className?: strin return null } -export function TrustBadge(props: { validator: IValidator }) { +export function TrustBadge(props: { validator: types.staking.IValidator }) { if (props.validator.trusted) { return ( diff --git a/src/components/delegation/ValidatorCard.tsx b/src/components/delegation/ValidatorCard.tsx index 414d3f8..1ca35a2 100644 --- a/src/components/delegation/ValidatorCard.tsx +++ b/src/components/delegation/ValidatorCard.tsx @@ -30,14 +30,14 @@ import { cmn, cls, styles, fromWei, SkPaper } 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' +import { types } from '@/core' export default function ValidatorCard(props: { - validator: IValidator + validator: types.staking.IValidator validatorId: number | undefined setValidatorId: any - delegationType: DelegationType + delegationType: types.staking.DelegationType size?: 'md' | 'lg' }) { if (!props.validator.trusted) return diff --git a/src/components/delegation/ValidatorInfo.tsx b/src/components/delegation/ValidatorInfo.tsx index 9548a56..917139b 100644 --- a/src/components/delegation/ValidatorInfo.tsx +++ b/src/components/delegation/ValidatorInfo.tsx @@ -22,6 +22,7 @@ */ import { cmn, cls, fromWei, TokenIcon } from '@skalenetwork/metaport' +import { types } from '@/core' import PercentRoundedIcon from '@mui/icons-material/PercentRounded' import PersonRoundedIcon from '@mui/icons-material/PersonRounded' @@ -31,45 +32,56 @@ 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' +import { Skeleton } from '@mui/material' -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) +export default function ValidatorInfo(props: { + validator: types.staking.IValidator | null + className?: string +}) { + const description = props.validator?.description ? props.validator.description : 'No description' + const minDelegation = + props.validator && fromWei(props.validator.minimumDelegationAmount, DEFAULT_ERC20_DECIMALS) return (
- -
-
-

{props.validator.name}

- - + + {props.validator ? ( +
+
+

{props.validator.name}

+ + +
+

+ {description} +

-

- {description} -

-
+ ) : ( +
+ + +
+ )}
} /> } /> - {validators.map((validator: IValidator, index) => ( + {validators.map((validator: types.staking.IValidator, index) => ( { +export async function initContracts(mpc: MetaportCore): Promise { log('Initializing contracts') const provider = mpc.provider('mainnet') const network = await skaleContracts.getNetworkByProvider(provider) @@ -57,15 +56,15 @@ export async function initContracts(mpc: MetaportCore): Promise { log('initActionContract:', skaleNetwork, beneficiary, contractType, delegationType) const network = await skaleContracts.getNetworkByProvider(signer.provider!) let contract: Contract - if (delegationType === DelegationType.REGULAR) { + if (delegationType === types.staking.DelegationType.REGULAR) { contract = await getManagerContract( network, skaleNetwork, @@ -92,14 +91,14 @@ function connectedContract(contract: Contract, signer: Signer): Contract { async function getEscrowContract( network: any, skaleNetwork: types.SkaleNetwork, - delegationType: DelegationType, - beneficiary: interfaces.AddressType + delegationType: types.staking.DelegationType, + beneficiary: types.AddressType ): Promise { const project = await network.getProject('skale-allocator') const instance = await getInstance( project, skaleNetwork, - delegationType === DelegationType.ESCROW ? 'allocator' : 'grants' + delegationType === types.staking.DelegationType.ESCROW ? 'allocator' : 'grants' ) return (await instance.getContract('Escrow', [beneficiary])) as Contract } diff --git a/src/core/delegation/delegations.ts b/src/core/delegation/delegations.ts index 9488549..ab78905 100644 --- a/src/core/delegation/delegations.ts +++ b/src/core/delegation/delegations.ts @@ -22,16 +22,10 @@ */ 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 { ERC_ABIS } from '@skalenetwork/metaport' +import { types } from '@/core' import { maxBigInt } from '../helper' +import { BATCH_SIZE } from '../constants' export enum DelegationState { PROPOSED = 0, @@ -55,46 +49,66 @@ export enum DelegationSource { export async function getDelegationIdsByHolder( delegationController: Contract, - address: interfaces.AddressType + address: types.AddressType ): Promise { - const delegationIdsLen = await delegationController.getDelegationsByHolderLength(address) + const idsLen = await delegationController.getDelegationsByHolderLength(address) return await Promise.all( Array.from( - { length: Number(delegationIdsLen) }, + { length: Number(idsLen) }, async (_, id) => await delegationController.delegationsByHolder(address, id) ) ) } -async function getDelegationsRaw( +async function loadDelegationBatch( delegationController: Contract, - delegationIds: bigint[] -): Promise> { + valId: number, + start: number, + size: number +): Promise { return await Promise.all( - delegationIds - .map((delegationId) => [ - delegationController.getDelegation(delegationId), - delegationController.getState(delegationId) - ]) - .flat() + Array.from( + { length: size }, + async (_, index) => await delegationController.delegationsByValidator(valId, start + index) + ) ) } -export async function getDelegations( +export async function getDelegationIdsByValidator( + delegationController: Contract, + valId: number +): Promise { + const totalDelegations = Number(await delegationController.getDelegationsByValidatorLength(valId)) + const batchCount = Math.ceil(totalDelegations / BATCH_SIZE) + let allDelegations: bigint[] = [] + + for (let i = 0; i < batchCount; i++) { + const start = i * BATCH_SIZE + const batchSize = Math.min(BATCH_SIZE, totalDelegations - start) + const batch = await loadDelegationBatch(delegationController, valId, start, batchSize) + allDelegations = [...allDelegations, ...batch] + } + + return allDelegations +} + +async function loadDelegationDetailsBatch( delegationController: Contract, delegationIds: bigint[] -): Promise { - const rawDelegations: Array = await getDelegationsRaw( - delegationController, - delegationIds +): Promise { + const rawData = await Promise.all( + delegationIds.flatMap((id) => [ + delegationController.getDelegation(id), + delegationController.getState(id) + ]) ) - 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], + return delegationIds.map((id, index) => { + const delegationArray = rawData[index * 2] + const stateId = rawData[index * 2 + 1] + + return { + id, address: delegationArray[0], validator_id: delegationArray[1], amount: delegationArray[2], @@ -105,12 +119,28 @@ export async function getDelegations( info: delegationArray[7], stateId, state: DelegationState[Number(stateId)] - }) + } + }) +} + +export async function getDelegations( + delegationController: Contract, + delegationIds: bigint[] +): Promise { + const batchCount = Math.ceil(delegationIds.length / BATCH_SIZE) + let allDelegations: types.staking.IDelegation[] = [] + + for (let i = 0; i < batchCount; i++) { + const start = i * BATCH_SIZE + const batchIds = delegationIds.slice(start, start + BATCH_SIZE) + const batchDelegations = await loadDelegationDetailsBatch(delegationController, batchIds) + allDelegations = [...allDelegations, ...batchDelegations] } - return delegations + + return allDelegations } -export function getDelegationSource(delegation: IDelegation): DelegationSource { +export function getDelegationSource(delegation: types.staking.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 @@ -125,11 +155,11 @@ export function getKeyByValue(enumType: any, enumValue: string): string | undefi } export async function groupDelegationsByValidator( - delegations: IDelegation[], + delegations: types.staking.IDelegation[], distributor: Contract, - address: interfaces.AddressType -): Promise { - const groupedDelegations = new Map() + address: types.AddressType +): Promise { + const groupedDelegations = new Map() delegations.forEach((delegation) => { const { validator_id } = delegation const existingDelegations = groupedDelegations.get(validator_id) || [] @@ -147,7 +177,7 @@ export async function groupDelegationsByValidator( const res = await Promise.all( delegationsArray.map( - async (delegationsToValidator: IDelegationsToValidator) => + async (delegationsToValidator: types.staking.IDelegationsToValidator) => await distributor.getAndUpdateEarnedBountyAmountOf.staticCallResult( address, delegationsToValidator.validatorId @@ -171,7 +201,7 @@ export async function groupDelegationsByValidator( return delegationsArray } -export const sumRewards = (delegations: IDelegationsToValidator[]): bigint => +export const sumRewards = (delegations: types.staking.IDelegationsToValidator[]): bigint => delegations.reduce((total, del) => total + del.rewards, BigInt(0)) export async function initSkaleToken(provider: Provider, instance: any): Promise { @@ -180,13 +210,13 @@ export async function initSkaleToken(provider: Provider, instance: any): Promise } export async function getDelegatorInfo( - sc: ISkaleContractsMap, + sc: types.staking.ISkaleContractsMap, rewards: bigint, - address: interfaces.AddressType, - beneficiary?: interfaces.AddressType, - type?: DelegationType -): Promise { - const info: IDelegatorInfo = { + address: types.AddressType, + beneficiary?: types.AddressType, + type?: types.staking.DelegationType +): Promise { + const info: types.staking.IDelegatorInfo = { balance: await sc.skaleToken.balanceOf(address), staked: ( await sc.delegationController.getAndUpdateDelegatedAmount.staticCallResult(address) @@ -201,11 +231,11 @@ export async function getDelegatorInfo( info.allowedToDelegate = maxBigInt(info.balance - info.forbiddenToDelegate, 0n) if (beneficiary) { - if (type === DelegationType.ESCROW) { + if (type === types.staking.DelegationType.ESCROW) { info.vested = await getVestedAmount(sc.allocator, address, beneficiary) info.fullAmount = await sc.allocator.getFullAmount(beneficiary) } - if (type === DelegationType.ESCROW2) { + if (type === types.staking.DelegationType.ESCROW2) { info.vested = await getVestedAmount(sc.grantsAllocator, address, beneficiary) info.fullAmount = await sc.grantsAllocator.getFullAmount(beneficiary) } @@ -218,8 +248,8 @@ export async function getDelegatorInfo( export async function getVestedAmount( allocator: Contract, - escrowAddress: interfaces.AddressType, - address: interfaces.AddressType + escrowAddress: types.AddressType, + address: types.AddressType ): Promise { let vestedAmount: bigint if (await allocator.isVestingActive(address)) { @@ -236,8 +266,44 @@ export async function getVestedAmount( return vestedAmount } -export function getDelegationTypeAlias(type: DelegationType): string { - if (type === DelegationType.ESCROW) return 'Escrow' - if (type === DelegationType.ESCROW2) return 'Grant' +export function getDelegationTypeAlias(type: types.staking.DelegationType): string { + if (type === types.staking.DelegationType.ESCROW) return 'Escrow' + if (type === types.staking.DelegationType.ESCROW2) return 'Grant' return 'Regular' } + +export function calculateDelegationTotals( + delegations: types.staking.IDelegation[] +): types.staking.IDelegationTotals { + const initialTotals: types.staking.IDelegationTotals = { + proposed: { count: 0, amount: 0n }, + accepted: { count: 0, amount: 0n }, + delegated: { count: 0, amount: 0n }, + completed: { count: 0, amount: 0n } + } + + return delegations.reduce((totals, delegation) => { + const amount = delegation.amount + + switch (Number(delegation.stateId)) { + case DelegationState.PROPOSED: + totals.proposed.count++ + totals.proposed.amount += amount + break + case DelegationState.ACCEPTED: + totals.accepted.count++ + totals.accepted.amount += amount + break + case DelegationState.DELEGATED: + totals.delegated.count++ + totals.delegated.amount += amount + break + case DelegationState.COMPLETED: + totals.completed.count++ + totals.completed.amount += amount + break + } + + return totals + }, initialTotals) +} diff --git a/src/core/delegation/staking.ts b/src/core/delegation/staking.ts index 4083604..b036243 100644 --- a/src/core/delegation/staking.ts +++ b/src/core/delegation/staking.ts @@ -21,26 +21,21 @@ * @copyright SKALE Labs 2024-Present */ -import { - DelegationType, - type ISkaleContractsMap, - type StakingInfo, - type StakingInfoMap -} from '../interfaces' -import { type interfaces } from '@skalenetwork/metaport' +import { types } from '@/core' import { isZeroAddr } from '../helper' import { getDelegationIdsByHolder, getDelegations, groupDelegationsByValidator, sumRewards, - getDelegatorInfo + getDelegatorInfo, + getDelegationIdsByValidator } from '.' export async function getStakingInfoMap( - sc: ISkaleContractsMap, - address: interfaces.AddressType | undefined -): Promise { + sc: types.staking.ISkaleContractsMap, + address: types.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) @@ -48,19 +43,19 @@ export async function getStakingInfoMap( 0: await getStakingInfo(sc, address), 1: isZeroAddr(escrowAddress) ? null - : await getStakingInfo(sc, escrowAddress, address, DelegationType.ESCROW), + : await getStakingInfo(sc, escrowAddress, address, types.staking.DelegationType.ESCROW), 2: isZeroAddr(escrowGrantsAddress) ? null - : await getStakingInfo(sc, escrowGrantsAddress, address, DelegationType.ESCROW2) + : await getStakingInfo(sc, escrowGrantsAddress, address, types.staking.DelegationType.ESCROW2) } } export async function getStakingInfo( - sc: ISkaleContractsMap, - address: interfaces.AddressType, - beneficiary?: interfaces.AddressType, - type?: DelegationType -): Promise { + sc: types.staking.ISkaleContractsMap, + address: types.AddressType, + beneficiary?: types.AddressType, + type?: types.staking.DelegationType +): Promise { const delegationIds = await getDelegationIdsByHolder(sc.delegationController, address) const delegationsArray = await getDelegations(sc.delegationController, delegationIds) const groupedDelegations = await groupDelegationsByValidator( @@ -75,10 +70,24 @@ export async function getStakingInfo( } } -export function isDelegationTypeAvailable(si: StakingInfoMap, type: DelegationType): boolean { - return si[DelegationType.REGULAR] !== null && si[type] !== undefined && si[type] !== null +export async function getValidatorDelegations( + sc: types.staking.ISkaleContractsMap, + valId: number +): Promise { + const delegationIds = await getDelegationIdsByValidator(sc.delegationController, valId) + const delegationsArray = await getDelegations(sc.delegationController, delegationIds) + return delegationsArray +} + +export function isDelegationTypeAvailable( + si: types.staking.StakingInfoMap, + type: types.staking.DelegationType +): boolean { + return ( + si[types.staking.DelegationType.REGULAR] !== null && si[type] !== undefined && si[type] !== null + ) } -export function isLoaded(si: StakingInfoMap): boolean { - return si[DelegationType.REGULAR] !== null +export function isLoaded(si: types.staking.StakingInfoMap): boolean { + return si[types.staking.DelegationType.REGULAR] !== null } diff --git a/src/core/delegation/validators.ts b/src/core/delegation/validators.ts index ec6d202..e567b3d 100644 --- a/src/core/delegation/validators.ts +++ b/src/core/delegation/validators.ts @@ -23,8 +23,8 @@ import debug from 'debug' import { type Contract } from 'ethers' - -import { type IValidatorArray, type IValidator } from '../interfaces' +import { types } from '@/core' +import { DelegationState } from './delegations' debug.enable('*') const log = debug('portal:core:validators') @@ -33,10 +33,42 @@ export const ESCROW_VALIDATORS = [ 43, 46, 54, 37, 48, 49, 42, 41, 47, 40, 52, 35, 36, 39, 50, 45, 51, 68, 30 ] +const STATUS_ORDER = { + [DelegationState.PROPOSED]: 1, + [DelegationState.ACCEPTED]: 2, + [DelegationState.DELEGATED]: 3, + [DelegationState.COMPLETED]: 4, + [DelegationState.CANCELED]: 5, + [DelegationState.REJECTED]: 6, + [DelegationState.UNDELEGATION_REQUESTED]: 7 +} + +export type SortType = 'id' | 'status' + +export function sortDelegations( + delegations: types.staking.IDelegation[], + sortBy: SortType +): types.staking.IDelegation[] { + return [...delegations].sort((a, b) => { + if (sortBy === 'id') { + return Number(b.id) - Number(a.id) + } else { + const aStatus = Number(a.stateId) as DelegationState + const bStatus = Number(b.stateId) as DelegationState + const statusComparison = STATUS_ORDER[aStatus] - STATUS_ORDER[bStatus] + if (statusComparison === 0) { + return Number(b.id) - Number(a.id) + } + + return statusComparison + } + }) +} + async function getValidatorsRaw( validatorService: Contract, numberOfValidators: bigint[] -): Promise> { +): Promise> { const validatorIds = Array.from(Array(Number(numberOfValidators)).keys()) return await Promise.all( validatorIds @@ -49,39 +81,82 @@ async function getValidatorsRaw( ) } +export async function getValidatorRaw( + validatorService: Contract, + validatorId: number +): Promise<[types.staking.IValidatorArray, boolean, string[]]> { + const [validatorData, isAuthorized, nodeAddresses] = await Promise.all([ + validatorService.validators(validatorId), + validatorService.isAuthorizedValidator(validatorId), + validatorService.getNodeAddresses(validatorId) + ]) + return [validatorData, isAuthorized, nodeAddresses] +} + +function formatValidator( + validatorData: types.staking.IValidatorArray, + isAuthorized: boolean, + nodeAddresses: string[], + validatorId: number +): types.staking.IValidator { + return { + name: validatorData[0], + validatorAddress: validatorData[1], + requestedAddress: validatorData[2], + description: validatorData[3], + feeRate: validatorData[4], + registrationTime: validatorData[5], + minimumDelegationAmount: validatorData[6], + acceptNewRequests: validatorData[7], + trusted: isAuthorized, + id: validatorId, + linkedNodes: nodeAddresses.length + } +} + export async function getValidators( validatorService: Contract, sorted: boolean = true -): Promise { +): Promise { const numberOfValidators = await validatorService.numberOfValidators() log('getValidators: ', numberOfValidators) - const rawValidators: Array = await getValidatorsRaw( - validatorService, - numberOfValidators - ) - const validatorsData: IValidator[] = [] + const rawValidators: Array = + await getValidatorsRaw(validatorService, numberOfValidators) + const validatorsData: types.staking.IValidator[] = [] for (let i = 0; i < rawValidators.length; i += 3) { - const IValidatorArray: IValidatorArray = rawValidators[i] as IValidatorArray + const validatorArray: types.staking.IValidatorArray = rawValidators[ + i + ] as types.staking.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 - }) + validatorsData.push(formatValidator(validatorArray, isTrusted, linkedNodeAddresses, i / 3 + 1)) } return sorted ? sortValidators(validatorsData) : validatorsData } -function sortValidators(validatorsData: IValidator[]): IValidator[] { +export async function getValidator( + validatorService: Contract, + address: types.AddressType +): Promise { + try { + const validatorId = await validatorService.getValidatorId(address) + const [validatorData, isAuthorized, nodeAddresses] = await getValidatorRaw( + validatorService, + validatorId + ) + return formatValidator(validatorData, isAuthorized, nodeAddresses, Number(validatorId)) + } catch (error: any) { + if ( + error?.message?.includes('Validator address does not exist') || + error?.message?.includes('ValidatorAddressDoesNotExist') + ) { + return undefined + } + throw error + } +} + +function sortValidators(validatorsData: types.staking.IValidator[]): types.staking.IValidator[] { validatorsData.sort((a, b) => { if (a.trusted !== b.trusted) { return a.trusted ? -1 : 1 @@ -100,15 +175,18 @@ function sortValidators(validatorsData: IValidator[]): IValidator[] { } export function filterValidators( - validators: IValidator[], + validators: types.staking.IValidator[], ids: number[], internal: boolean -): IValidator[] { +): types.staking.IValidator[] { return validators.filter( (val) => (ids.includes(val.id) && internal) || (!ids.includes(val.id) && !internal) ) } -export function getValidatorById(validators: IValidator[], id: bigint): IValidator | undefined { +export function getValidatorById( + validators: types.staking.IValidator[], + id: bigint +): types.staking.IValidator | undefined { return validators.find((val) => Number(val.id) === Number(id)) } diff --git a/src/core/explorer.ts b/src/core/explorer.ts index 57f2489..1f7b72f 100644 --- a/src/core/explorer.ts +++ b/src/core/explorer.ts @@ -20,7 +20,7 @@ * @copyright SKALE Labs 2024-Present */ -import { BASE_EXPLORER_URLS, interfaces } from '@skalenetwork/metaport' +import { BASE_EXPLORER_URLS } from '@skalenetwork/metaport' import { HTTPS_PREFIX } from './chain' import { type types } from '@/core' @@ -48,7 +48,7 @@ export function getTotalAppCounters( } for (const address in countersArray) { if (countersArray.hasOwnProperty(address)) { - const addressCounters = countersArray[address as interfaces.AddressType] + const addressCounters = countersArray[address as types.AddressType] if (addressCounters.gas_usage_count === undefined) continue totalCounters.gas_usage_count = ( parseInt(totalCounters.gas_usage_count) + parseInt(addressCounters.gas_usage_count) diff --git a/src/core/helper.ts b/src/core/helper.ts index e843f10..fd26158 100644 --- a/src/core/helper.ts +++ b/src/core/helper.ts @@ -20,10 +20,11 @@ * @copyright SKALE Labs 2024-Present */ -import { fromWei, type interfaces } from '@skalenetwork/metaport' +import { fromWei } from '@skalenetwork/metaport' +import { types } from '@/core' import { DEFAULT_ERC20_DECIMALS, ZERO_ADDRESS, DEFAULT_FRACTION_DIGITS } from './constants' -export function isZeroAddr(address: interfaces.AddressType): boolean { +export function isZeroAddr(address: types.AddressType): boolean { return address === ZERO_ADDRESS } @@ -85,7 +86,7 @@ export function minBigInt(a: bigint, b: bigint): bigint { return a < b ? a : b } -export function shortAddress(address: interfaces.AddressType | undefined): string { +export function shortAddress(address: types.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 deleted file mode 100644 index 66d9969..0000000 --- a/src/core/interfaces/Beneficiary.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/** - * @file 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 deleted file mode 100644 index 85b180b..0000000 --- a/src/core/interfaces/Delegation.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file 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 deleted file mode 100644 index 3c511e7..0000000 --- a/src/core/interfaces/Delegator.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file 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 deleted file mode 100644 index 2d4267b..0000000 --- a/src/core/interfaces/SkaleContract.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file 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 deleted file mode 100644 index 5521306..0000000 --- a/src/core/interfaces/Staking.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file 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 deleted file mode 100644 index d0c2e0b..0000000 --- a/src/core/interfaces/Validator.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file 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 deleted file mode 100644 index 93cd7a3..0000000 --- a/src/core/interfaces/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file 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 48a300d..8d22575 100644 --- a/src/core/meta.ts +++ b/src/core/meta.ts @@ -73,7 +73,7 @@ export const META_TAGS = { }, validator: { title: 'SKALE Portal - Validator', - description: 'Manage your validator and delegations', + description: 'Delegations and chain rewards management', help: 'Manage your validator and delegations, review your delegations and chain rewards.' }, onramp: { diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 9940a36..ce0b59b 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -301,9 +301,7 @@ export default function App(props: { } /> diff --git a/src/pages/StakeAmount.tsx b/src/pages/StakeAmount.tsx index 5d1f931..6396ba0 100644 --- a/src/pages/StakeAmount.tsx +++ b/src/pages/StakeAmount.tsx @@ -24,14 +24,8 @@ import { useState, useEffect } from 'react' import { type Signer } from 'ethers' import { useParams } from 'react-router-dom' -import { - cmn, - cls, - type MetaportCore, - SkPaper, - type interfaces, - styles -} from '@skalenetwork/metaport' +import { cmn, cls, type MetaportCore, SkPaper, styles } from '@skalenetwork/metaport' +import { types } from '@/core' import Container from '@mui/material/Container' import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' @@ -46,13 +40,6 @@ 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' @@ -60,19 +47,21 @@ import { getDelegationTypeAlias } from '../core/delegation' export default function StakeAmount(props: { mpc: MetaportCore - validators: IValidator[] + validators: types.staking.IValidator[] loadValidators: () => void loadStakingInfo: () => void - sc: ISkaleContractsMap | null - si: StakingInfoMap - address: interfaces.AddressType | undefined + sc: types.staking.ISkaleContractsMap | null + si: types.staking.StakingInfoMap + address: types.AddressType | undefined getMainnetSigner: () => Promise }) { const { id, delType } = useParams() const validatorId = Number(id) ?? -1 - const delegationType = Number(delType) ?? DelegationType.REGULAR + const delegationType = Number(delType) ?? types.staking.DelegationType.REGULAR - const [currentValidator, setCurrentValidator] = useState(undefined) + const [currentValidator, setCurrentValidator] = useState( + undefined + ) const [errorMsg, setErrorMsg] = useState() const loaded = isLoaded(props.si) diff --git a/src/pages/StakeValidator.tsx b/src/pages/StakeValidator.tsx index 82ee09e..1bb7383 100644 --- a/src/pages/StakeValidator.tsx +++ b/src/pages/StakeValidator.tsx @@ -22,20 +22,13 @@ */ import { useState, useEffect } from 'react' +import { cmn, cls, type MetaportCore, SkPaper } from '@skalenetwork/metaport' +import { types } from '@/core' 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' @@ -44,13 +37,15 @@ import SkStack from '../components/SkStack' export default function StakeValidator(props: { mpc: MetaportCore - validators: IValidator[] + validators: types.staking.IValidator[] loadValidators: () => void loadStakingInfo: () => void - sc: ISkaleContractsMap | null - si: StakingInfoMap + sc: types.staking.ISkaleContractsMap | null + si: types.staking.StakingInfoMap }) { - const [delegationType, setDelegationType] = useState(DelegationType.REGULAR) + const [delegationType, setDelegationType] = useState( + types.staking.DelegationType.REGULAR + ) const [validatorId, setValidatorId] = useState() const handleChange = (event: any) => { @@ -100,7 +95,7 @@ export default function StakeValidator(props: { validators={props.validators} validatorId={validatorId} setValidatorId={setValidatorId} - internal={!compareEnum(delegationType, DelegationType.REGULAR)} + internal={!compareEnum(delegationType, types.staking.DelegationType.REGULAR)} delegationType={delegationType} /> diff --git a/src/pages/Staking.tsx b/src/pages/Staking.tsx index 0447134..16fd4ff 100644 --- a/src/pages/Staking.tsx +++ b/src/pages/Staking.tsx @@ -33,9 +33,9 @@ import { styles, SkPaper, type MetaportCore, - type interfaces, sendTransaction } from '@skalenetwork/metaport' +import { types } from '@/core' import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' @@ -50,16 +50,6 @@ import WarningRoundedIcon from '@mui/icons-material/WarningRounded' 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' @@ -76,22 +66,24 @@ const log = debug('portal:pages:Staking') export default function Staking(props: { mpc: MetaportCore - validators: IValidator[] + validators: types.staking.IValidator[] loadValidators: () => Promise loadStakingInfo: () => Promise - sc: ISkaleContractsMap | null - si: StakingInfoMap - address: interfaces.AddressType | undefined - customAddress: interfaces.AddressType | undefined + sc: types.staking.ISkaleContractsMap | null + si: types.staking.StakingInfoMap + address: types.AddressType | undefined + customAddress: types.AddressType | undefined getMainnetSigner: () => Promise isXs: boolean }) { - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState< + types.staking.IRewardInfo | types.staking.IDelegationInfo | false + >(false) const [errorMsg, setErrorMsg] = useState() - const [customRewardAddress, setCustomRewardAddress] = useState< - interfaces.AddressType | undefined - >(props.address) + const [customRewardAddress, setCustomRewardAddress] = useState( + props.address + ) useEffect(() => { props.loadValidators() @@ -111,10 +103,10 @@ export default function Staking(props: { }, [props.address]) async function processTx( - delegationType: DelegationType, + delegationType: types.staking.DelegationType, txName: string, txArgs: any[], - contractType: ContractType + contractType: types.staking.ContractType ) { if (props.sc === null || props.address === undefined) return log('processTx:', txName, txArgs, contractType, delegationType) @@ -142,7 +134,7 @@ export default function Staking(props: { } } - async function retrieveRewards(rewardInfo: IRewardInfo) { + async function retrieveRewards(rewardInfo: types.staking.IRewardInfo) { setLoading(rewardInfo) if (!isAddress(customRewardAddress)) { setErrorMsg('Invalid address') @@ -157,7 +149,7 @@ export default function Staking(props: { ) } - async function unstake(delegationInfo: IDelegationInfo) { + async function unstake(delegationInfo: types.staking.IDelegationInfo) { setLoading(delegationInfo) processTx( delegationInfo.delegationType, @@ -167,7 +159,7 @@ export default function Staking(props: { ) } - async function cancelRequest(delegationInfo: IDelegationInfo) { + async function cancelRequest(delegationInfo: types.staking.IDelegationInfo) { setLoading(delegationInfo) processTx( delegationInfo.delegationType, @@ -177,7 +169,7 @@ export default function Staking(props: { ) } - async function retrieveUnlocked(rewardInfo: IRewardInfo): Promise { + async function retrieveUnlocked(rewardInfo: types.staking.IRewardInfo): Promise { setLoading(rewardInfo) processTx(rewardInfo.delegationType, 'retrieve', [], 'distributor') } @@ -251,7 +243,7 @@ export default function Staking(props: { void + sc: types.staking.ISkaleContractsMap | null + address: types.AddressType | undefined + customAddress: types.AddressType | undefined + loadValidator: (address: types.AddressType) => void + validator: types.staking.IValidator | null | undefined + isXs: boolean + delegations: types.staking.IDelegation[] }) { + const [sortBy, setSortBy] = useState('id') + const [visibleItems, setVisibleItems] = useState(ITEMS_PER_PAGE) + + const sortedDelegations = useMemo( + () => sortDelegations(props.delegations || [], sortBy), + [props.delegations, sortBy] + ) + + const visibleDelegations = useMemo( + () => sortedDelegations.slice(0, visibleItems), + [sortedDelegations, visibleItems] + ) + + const remainingItems = Math.max(0, sortedDelegations.length - visibleItems) + useEffect(() => { - if (props.sc !== null) { - props.loadValidators() + if (props.sc !== null && props.address !== undefined) { + props.loadValidator(props.address) } - }, [props.sc]) + }, [props.sc, props.address]) + + useEffect(() => { + setVisibleItems(ITEMS_PER_PAGE) + }, [sortBy]) + + const handleShowMore = () => { + setVisibleItems((prevVisible) => prevVisible + ITEMS_PER_PAGE) + } + + const renderDelegationsContent = () => { + if (props.delegations === null) { + return ( +
+ + + +
+ ) + } + return ( + <> + {visibleDelegations.map((delegation: types.staking.IDelegation) => ( + + ))} + {remainingItems > 0 && ( +
+
+ +
+
+ )} + + ) + } return (

Manage Validator

-

- {META_TAGS.validator.description} -

+

{META_TAGS.validator.description}

-
-
+ + } + size="small" + className={cls(cmn.mbott20)} + /> + + + + {props.validator !== undefined ? ( +
+ + +
+ ) : ( +
+ +

+ Validator doesn't exist +

+
+ )} +
+ {props.validator !== undefined && + + } + className={cls(cmn.mbott20)} + /> + } + grow + childrenRi={ + { + // }} + /> + } + /> + } + {props.validator !== undefined && +
+
+ } + className={cls(cmn.flexg)} + /> + +
+ {renderDelegationsContent()} +
+
}
) } diff --git a/src/pages/Validators.tsx b/src/pages/Validators.tsx index 3ec95ea..5314d52 100644 --- a/src/pages/Validators.tsx +++ b/src/pages/Validators.tsx @@ -22,22 +22,22 @@ */ import { useEffect } from 'react' +import { Link } from 'react-router-dom' +import { cmn, cls, type MetaportCore } from '@skalenetwork/metaport' +import { types } from '@/core' +import { Button } from '@mui/material' import Container from '@mui/material/Container' -import { cmn, cls, type MetaportCore } from '@skalenetwork/metaport' +import ManageAccountsRoundedIcon from '@mui/icons-material/ManageAccountsRounded' import Validators from '../components/delegation/Validators' - -import { DelegationType, type ISkaleContractsMap, type IValidator } from '../core/interfaces' import SkPageInfoIcon from '../components/SkPageInfoIcon' import { META_TAGS } from '../core/meta' -import { Link } from 'react-router-dom' -import { Button } from '@mui/material' export default function ValidatorsPage(props: { mpc: MetaportCore - validators: IValidator[] - sc: ISkaleContractsMap | null + validators: types.staking.IValidator[] + sc: types.staking.ISkaleContractsMap | null loadValidators: () => void }) { useEffect(() => { @@ -56,7 +56,12 @@ export default function ValidatorsPage(props: {

- @@ -67,8 +72,8 @@ export default function ValidatorsPage(props: { mpc={props.mpc} validators={props.validators} validatorId={0} - setValidatorId={(): void => { }} - delegationType={DelegationType.REGULAR} + setValidatorId={(): void => {}} + delegationType={types.staking.DelegationType.REGULAR} size="lg" />
From 58b99c792c6626847b1d3c076cbc6c649d609b99 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 12 Nov 2024 19:16:56 +0000 Subject: [PATCH 03/16] Update Validator page --- src/pages/Validator.tsx | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/pages/Validator.tsx b/src/pages/Validator.tsx index 6694ff5..53275b8 100644 --- a/src/pages/Validator.tsx +++ b/src/pages/Validator.tsx @@ -145,23 +145,22 @@ export default function Validator(props: { - {props.validator !== undefined ? ( + {props.address ? (props.validator !== undefined ? (
- ) : ( -
- -

- Validator doesn't exist -

-
- )} + ) : (
+ +

+ Validator doesn't exist +

+
)) :
+ } - {props.validator !== undefined && + {props.validator && } - {props.validator !== undefined && + {props.validator &&
Date: Tue, 12 Nov 2024 20:16:40 +0000 Subject: [PATCH 04/16] Fix custom address logic --- src/Router.tsx | 4 +- src/SkDrawer.tsx | 7 +-- src/pages/Validator.tsx | 95 +++++++++++++++++++++++++---------------- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/Router.tsx b/src/Router.tsx index 180b2d5..5c1dcd9 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -200,9 +200,9 @@ export default function Router() { setValidators(validatorsData) } - async function loadValidator(address: types.AddressType) { + async function loadValidator() { if (!sc || !address) return - const validatorData = await getValidator(sc.validatorService, address) + const validatorData = await getValidator(sc.validatorService, customAddress ?? address) setValidator(validatorData) if (validatorData && validatorData.id) { setValidatorDelegations(await getValidatorDelegations(sc, validatorData.id)) diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index 3a14258..e0934aa 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -55,12 +55,7 @@ export default function SkDrawer() { - + diff --git a/src/pages/Validator.tsx b/src/pages/Validator.tsx index 53275b8..e211435 100644 --- a/src/pages/Validator.tsx +++ b/src/pages/Validator.tsx @@ -33,6 +33,7 @@ import PeopleRoundedIcon from '@mui/icons-material/PeopleRounded' import AllInboxRoundedIcon from '@mui/icons-material/AllInboxRounded' import StarsRoundedIcon from '@mui/icons-material/StarsRounded' import EventAvailableRoundedIcon from '@mui/icons-material/EventAvailableRounded' +import VisibilityRoundedIcon from '@mui/icons-material/VisibilityRounded' import SkPageInfoIcon from '../components/SkPageInfoIcon' import { META_TAGS } from '../core/meta' @@ -48,13 +49,14 @@ import { ITEMS_PER_PAGE } from '../core/constants' import ShowMoreButton from '../components/delegation/ShowMoreButton' import SkBtn from '../components/SkBtn' import DelegationTotals from '../components/delegation/DelegationTotals' +import Message from '../components/Message' export default function Validator(props: { mpc: MetaportCore sc: types.staking.ISkaleContractsMap | null address: types.AddressType | undefined customAddress: types.AddressType | undefined - loadValidator: (address: types.AddressType) => void + loadValidator: () => void validator: types.staking.IValidator | null | undefined isXs: boolean delegations: types.staking.IDelegation[] @@ -76,7 +78,7 @@ export default function Validator(props: { useEffect(() => { if (props.sc !== null && props.address !== undefined) { - props.loadValidator(props.address) + props.loadValidator() } }, [props.sc, props.address]) @@ -135,6 +137,16 @@ export default function Validator(props: {
+ {props.customAddress !== undefined ? ( + } + link="/validator" + linkText="click to exit" + type="warning" + /> + ) : null} - {props.address ? (props.validator !== undefined ? ( -
- - -
- ) : (
- -

- Validator doesn't exist -

-
)) :
- } + {props.address ? ( + props.validator !== undefined ? ( +
+ + +
+ ) : ( +
+ +

+ Validator doesn't exist +

+
+ ) + ) : ( +
+ )}
- {props.validator && + {props.validator && ( { - // }} + // disabled={ + // } + // onClick={() => { + // }} /> } /> - } - {props.validator && -
-
- } - className={cls(cmn.flexg)} - /> - + + )} + {props.validator && ( + +
+
+ } + className={cls(cmn.flexg)} + /> + +
+ {renderDelegationsContent()}
- {renderDelegationsContent()} -
- } + + )} ) } From ddfa32b581678f344cc359c384d0f2d213ae2620 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 12 Nov 2024 20:24:06 +0000 Subject: [PATCH 05/16] Fix custom address logic --- src/Router.tsx | 5 +++-- src/pages/Validator.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Router.tsx b/src/Router.tsx index 5c1dcd9..c6aef4b 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -201,8 +201,9 @@ export default function Router() { } async function loadValidator() { - if (!sc || !address) return - const validatorData = await getValidator(sc.validatorService, customAddress ?? address) + const addr = customAddress ?? address + if (!sc || !addr) return + const validatorData = await getValidator(sc.validatorService, addr) setValidator(validatorData) if (validatorData && validatorData.id) { setValidatorDelegations(await getValidatorDelegations(sc, validatorData.id)) diff --git a/src/pages/Validator.tsx b/src/pages/Validator.tsx index e211435..dd61831 100644 --- a/src/pages/Validator.tsx +++ b/src/pages/Validator.tsx @@ -77,10 +77,10 @@ export default function Validator(props: { const remainingItems = Math.max(0, sortedDelegations.length - visibleItems) useEffect(() => { - if (props.sc !== null && props.address !== undefined) { + if (props.sc !== null) { props.loadValidator() } - }, [props.sc, props.address]) + }, [props.sc, props.address, props.customAddress]) useEffect(() => { setVisibleItems(ITEMS_PER_PAGE) @@ -154,10 +154,10 @@ export default function Validator(props: { size="small" className={cls(cmn.mbott20)} /> - + - {props.address ? ( + {props.address || props.customAddress ? ( props.validator !== undefined ? (
From a53b441118209c2eb8f824d5c588a5488ed15745 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 13 Nov 2024 13:36:36 +0000 Subject: [PATCH 06/16] Update logic of delegation card, fix ui issues --- src/App.scss | 15 ++-- src/components/Tile.tsx | 2 +- src/components/delegation/Delegation.tsx | 72 +++++++++++++------ .../delegation/DelegationTotals.tsx | 5 +- src/pages/Validator.tsx | 1 + 5 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/App.scss b/src/App.scss index cb0ca03..a822bc3 100644 --- a/src/App.scss +++ b/src/App.scss @@ -76,6 +76,10 @@ body { width: 100%; } +.fullH { + height: 100%; +} + .mp__btnConnect { position: relative; @@ -1149,7 +1153,6 @@ input[type=number] { } } - .trustedBadge { color: #0095f6; } @@ -1158,12 +1161,6 @@ input[type=number] { color: #ffb817; } -.validatorCard { - height: 100% !important; - cursor: pointer; -} - - .pOneLine { overflow: hidden; white-space: nowrap; @@ -1426,6 +1423,10 @@ input[type=number] { display: none; } +.opacity0 { + opacity: 0; +} + .MuiTooltip-tooltip { font-size: 0.8rem !important; padding: 8px 12px !important; diff --git a/src/components/Tile.tsx b/src/components/Tile.tsx index 59ffad4..6c5fb25 100644 --- a/src/components/Tile.tsx +++ b/src/components/Tile.tsx @@ -114,7 +114,7 @@ export default function Tile(props: { cmn.flex, cmn.flexcv, cmn.mbott5, - ['pSec', !props.color], + ['pSec', !props.color && !props.textColor], ['blackP', props.color] )} > diff --git a/src/components/delegation/Delegation.tsx b/src/components/delegation/Delegation.tsx index 17e2a4e..e613506 100644 --- a/src/components/delegation/Delegation.tsx +++ b/src/components/delegation/Delegation.tsx @@ -27,6 +27,8 @@ 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 AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded' +import HistoryRoundedIcon from '@mui/icons-material/HistoryRounded' import SkBtn from '../SkBtn' import ValidatorLogo from './ValidatorLogo' @@ -42,16 +44,19 @@ import { import { formatBigIntTimestampSeconds } from '../../core/timeHelper' import { convertMonthIndexToText, formatBalance } from '../../core/helper' +import Tile from '../Tile' export default function Delegation(props: { delegation: types.staking.IDelegation validator: types.staking.IValidator delegationType: types.staking.DelegationType + accept?: (delegationInfo: types.staking.IDelegationInfo) => Promise unstake?: (delegationInfo: types.staking.IDelegationInfo) => Promise cancelRequest?: (delegationInfo: types.staking.IDelegationInfo) => Promise loading: types.staking.IRewardInfo | types.staking.IDelegationInfo | false isXs: boolean customAddress: types.AddressType | undefined + isValidatorPage?: boolean }) { const source = getDelegationSource(props.delegation) const delegationAmount = formatBalance(props.delegation.amount, 'SKL') @@ -91,6 +96,12 @@ export default function Delegation(props: { } } + const noActions = + Number(props.delegation.stateId) !== DelegationState.PROPOSED && + Number(props.delegation.stateId) !== DelegationState.DELEGATED && + !isCompleted && + !props.isValidatorPage + if (!props.validator) return return (
@@ -98,19 +109,22 @@ export default function Delegation(props: { container spacing={0} alignItems="center" - className="validatorCard" + className={cls('fullH', ['pointer', !noActions])} onClick={() => { + if (noActions) return setOpen(!open) }} >
- + {!props.isValidatorPage && ( + + )}

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

@@ -162,22 +176,43 @@ export default function Delegation(props: {

{delegationAmount}

{getStakingText()}

+
- {isCompleted ? ( -
-

Delegation completed

-

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

-
- ) : null} + {props.isValidatorPage && ( + } + /> + )} + {isCompleted && ( + } + /> + )} {Number(props.delegation.stateId) === DelegationState.DELEGATED && props.unstake ? ( ) : null} - {Number(props.delegation.stateId) !== DelegationState.PROPOSED && - Number(props.delegation.stateId) !== DelegationState.DELEGATED && - !isCompleted ? ( -

- No actions available -

- ) : null}
diff --git a/src/components/delegation/DelegationTotals.tsx b/src/components/delegation/DelegationTotals.tsx index ff0c0a8..a9d46a2 100644 --- a/src/components/delegation/DelegationTotals.tsx +++ b/src/components/delegation/DelegationTotals.tsx @@ -22,7 +22,7 @@ */ import { useMemo } from 'react' -import { cls, styles } from '@skalenetwork/metaport' +import { cls, styles, useUIStore } from '@skalenetwork/metaport' import { type types } from '@/core' import InboxRoundedIcon from '@mui/icons-material/InboxRounded' @@ -46,7 +46,7 @@ const DelegationTotals: React.FC = ({ delegations, classN () => (delegations ? calculateDelegationTotals(delegations) : null), [delegations] ) - + const theme = useUIStore((state) => state.theme) const getTileText = (status: string, count?: number) => `${status}${count ? ` (${count})` : ''}` return ( @@ -56,6 +56,7 @@ const DelegationTotals: React.FC = ({ delegations, classN text={getTileText('Proposed', totals?.proposed.count)} grow size="md" + textColor={totals?.proposed.count ? theme.primary : undefined} icon={} /> ))} {remainingItems > 0 && ( From 14379bf17a66673912b5e09aee7d713a7e26a9a3 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Fri, 15 Nov 2024 19:58:39 +0000 Subject: [PATCH 07/16] Move staking actions to the separate module, add accept function to actions --- src/App.scss | 8 ++ src/Router.tsx | 1 + src/assets/validators/v10.png | Bin 0 -> 7829 bytes src/assets/validators/v10.webp | Bin 5078 -> 0 bytes src/assets/validators/v43.png | Bin 0 -> 7829 bytes src/assets/validators/v43.webp | Bin 5078 -> 0 bytes src/components/SkBtn.tsx | 9 +- src/components/delegation/Delegation.tsx | 14 +- src/core/delegation/stakingActions.ts | 175 +++++++++++++++++++++++ src/pages/Staking.tsx | 154 ++++++++------------ src/pages/Validator.tsx | 66 +++++++-- 11 files changed, 323 insertions(+), 104 deletions(-) create mode 100644 src/assets/validators/v10.png delete mode 100644 src/assets/validators/v10.webp create mode 100644 src/assets/validators/v43.png delete mode 100644 src/assets/validators/v43.webp create mode 100644 src/core/delegation/stakingActions.ts diff --git a/src/App.scss b/src/App.scss index a822bc3..5abf33e 100644 --- a/src/App.scss +++ b/src/App.scss @@ -461,6 +461,14 @@ body::-webkit-scrollbar { background: rgb(244 139 54 / 13%); } +.btnprimary { + background: rgba(147, 184, 236, 0.16); +} + +.btnDisabled { + background: #262626; +} + .btnSmLoading { padding: 0.5em 1.5em 0.5em 3em !important; } diff --git a/src/Router.tsx b/src/Router.tsx index c6aef4b..ff2dc74 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -393,6 +393,7 @@ export default function Router() { validator={validator} isXs={isXs} delegations={validatorDelegations} + getMainnetSigner={getMainnetSigner} /> } /> diff --git a/src/assets/validators/v10.png b/src/assets/validators/v10.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac46d907b38f9d4dd69f2f2c8d1647ba7629683 GIT binary patch literal 7829 zcmeHM={FnN*N)EWs-lKgD=imoQB>6|r6jGfsCfv|8fvO}h@d(khV-_Snl;x zX+w#HvwsyEiNv&N`it>t>%8E9G>4uT{#Wy1*INARzvXr`d@Zed&o@fzr~6+Ld;1?(c8H#oTD;m?3)iiPN)7;+g1rOy zM&?ul{C$-ScpIH}{NAyca{$gW{|)^2J^mxg;ZR%AR0tYjTJ6{IbfBr$Gc#yyypDBw z4xbW4C08x&Z_RT7H}A&s06mgL)yFSu`TwgL`n&d2^zA{l%@{6{r=*jQ&*zU=XM^gM zj547)X4PKq$BMYa*$(WpJrKh_;PQN$&wvKo_!C`sTVOV@YeY5F)FaP=A=tTp5sx@wJ4BqPU zOf&xsBWc>lA9clv^M)=&q zd!qxiY1;kDuM24I@Xo`J2otzZM2unE@xxYAHoJhz1Bd56>I@ z$+GFv^4nsfa*vyyw!TxEm!K-e7ZlBtC`ncrpcAo-y7Yi|`(bgpT=mfC5)r*&A&86M zx2;+Gb&kZp$yGa<>D4Le!KAR|E8O26TL7sl*JSS|zZh+#)KMrN;Q;f;LBp`eJ|Xg! zE;MD}?cH;RW@Fmy>~rRn@&}y+oVvW;*eOjNEfBWgMJlo+WSZl+xw-c+Ebv^};_~VA zjGHfg0tY`Ld7Dht#65a6af zEF_e8FL>+(f@mv#ovs*C#|6Gxj|7u*1PVcstj$4SLc`5PL7%BEf{yf@4h7tK6OwaQeU*?-Dd!wYFadd0~_{0hy~cc}VYP*~UE z0DTu2#meoGbbN`8{589`<5VitF(RbNj0B z=+L*BpJsf>0BS`%ncdvA4frJm@&NHXWmk!h>*Bt8PbRu2xmHnVOV#nE%`aJc9e(u8q$vBA-P|z$D@>~W%hYk`U55Y*b+gOi zYAVKxiOR!cVZLwkAfX{Mjj*@kzOWP^u%1|lxl#dC`R!lLYFrLSF;=ms7lW#WPCL^3 z=GMEuy+CaS7*+P@j|m1Q%zd~Pv)hBW$4B;yIVvEdT%5sIj}EzRwfq~lU4j^pVL!HT zv#^3z^`^)4NY2%niFu`XluZ_tG$@&Di8EoUTW=b72Gui467qBnT?Mvz_(_@R zf~W=~Sue`aqC-ec6pcs1`26|^ptOswxy!N!3o4S{-l>Y_4yQ7&8-DyLRXME zym}qHNMd41qFvsx!d2aWwLeCWzebb_-GzyfrQ)t1jXcdQ4t{)O+VuMavgs|4RLmY5 zFoG2Yc)J>`?AD48(Sfx6A`LT2;at-AngkikKZ{J~K|q^Q_M3+8tmqHREQFNT|eVp;IP-p2@+=Ou9L>iw0={9!T?&KDU>%gmvNo^>~8aq53zaY&J zcUM5)-j%xGQls@cVYupJ#4ya%ru@>7SdIfq2_P-8P+iZ+~i;^DT_Y)n*KfU z`%0&tyG%M-^!+*d%xm~!Rk2KFs4GIk6qV#T2m&p)Zo~M{tzN!w{VFxU0h66N3Z~GA zE4mob_uS!$PeMMs{H-on^~HDdtRM}s%^&0|r!GK|1U}RmNq(|9Ac7IjPxkhM0{`Uy z6&_Nl+MJHC-Vkdvz(=*@pW{W^-nFr**l-L8R$w@d(Oht{DVdmuroPN{Y&5hzCzSV8$m~8BLIUwA=`{NgS;9}0 z!U?u<>6A%^@&K-DXuYU%T7x`Nb3Z4=s`=qbP4FVZH+3pIF?caGtzHT(tF%oSG-b8% zLDq9~XEe?e2<4WHw5-m1ct1SLq7 zBFaCKnriV0iR?e^5qLj4Pl#7Nlsx?PXnH?mj`210pXxoP;v(Zb*0-Eva<^dC_?pPP zo-b5VfNK5sIQ}ubQ+@CjcCpag`*NE2F%cFbqDDxBYZmdT=sj-kZpV;gxL+Sja?nYL z+SK{!YX>Ab(e(nK&{ag2So;MbW!}NXWWc=2@csmrbwJ}0W%qH<(b#KlkHwbUZi}B6 zQu9mnZ4oTMLj6F9IGFnZ;(cSRXxqA{0p6DqS)8;ucqJDa+;1Y_7j+0Hc|aNF(L(1|CtH%%Sy>j_W@S;s$IkxtBez^n{dlg-lB^rh_kAZYRLst; zB}C%0d0M24bB&|gYLQ4mQnTFaY3AA?PT>cy(klQsr4~#ez^_i)sdk)oEy=! zu2UU1^$0!M!#YwgYn5X;8`ByHu@3)sl9a|d59zriuZ`yAjZaV>Y4f$Vt5wLBf8+Fl z;ydQD#w#R8szRBy1!$?mPHeS6u%uaOhK@kIU}8EUD7vbNqAjuE9QO*8}_kFp_%`;mVW^XT+<*=W1Z)_Txw zg)$>66yrJ}R90X1UA2mj%E8OlPWQk9*T(bff$Erw&J{4ZaBNLz$Qq%^2{kn)8{($uAOStTcp1$Am zW~Kk#7Lxm+tHdoyeWC%7kpg!QK}*YfI!_@~Rzi?)%%frP!g_*6b6#wANJMMz0mTof z8T=~X6uqHVJ(m~;8F8zz60hTvz2;&|SNNKWO!Cy%@EP;2R>N6-LBvnK<69G-J62Sg zJwHRoNBO+r8(PD+9M8Eyds;f{=)cYbMVivrQ|wJ0IzAE#!06LF14l_;7w}F~%;N>O z=+?HYc|8wmaPh9W%>LSe^3<$g*1(BmRE32M{pQ78o1DHY;v;6GCDlzKk)Pg;Zmf+b zv-T(0uhxJ2NyABnQzb7Q{eu_dk% zG93P&Yh>rfq9)m{GYH{E2zfI_-M3}aVfMq?&)ui3$?qz;rG+vJQ#1r?HQ&Q$Bk99S zOM^c8D{!lZMdT<&Ki$rJqxNF~pE$d-zft&zAq?pn*=2p8+s@ox)C#By)9*9&AeaEx z#C0TmwX;ghxF*Ld9Pm#b&F!X-$>x_lyh06`8B{*&Fn{p-4t*jxkj+^xQ3nDx)^Cef z&i9bKpP7z!oh;W~&0|6GJ)XwC9Zu7#yK-f_c+AXv0~Xc7_3W)0xv=Qs%ZcEcg&jN_ zdWxAduoqCy!Y8x+jj>v{FZzmq2~BkCGkr!d2?<iM1E``P2wQC(h9 zO(=-TW%N*CcZSwmkHc(cfXNb88*aajxoP;H#{4?=)i!7PR&7USht(=S$~%?NtuzxC z5;fmnf7VgI@8pwOUCe}K-R6?D{c><`XhUqPA?vq(qvF_*ZV6^(2!z+cUP{&KRxBhM ztQ#Gf>d#0{G~_4SohYdD(xm&U3er;>zEzR^gjY86)6}9Dc9mVRv@kt%C^)IsddSYwVdzz zA=2^r2PY<7Ex2`_)^%s^vmBAAQN6iRn35ijBCgQS*PqqbeI6P4qu(T1TGimuvz|tq zn?Y2M+-5-crh`Aes{j}2{7#EpP)LL+*q#XbQ1n^z0M}Z0VQ3sLTS#O}u9}I!uoKFb zt+agHgFEfK93L0AQg)QRy!HF!Z)THhmv+ekbz*|9AJhbV)_x;&HBUUBR(iv8UI99ZYA z!%kUj1b#L!sz-qhG5A&ik3Qu!qs-44lQ1*&O_};o`Hnyan-U`l`ti9W+jBF0w^*MV zgp&mA0hYJRf9&FUGzgA{DP|p$8!{$|+xnulQ?c*RBXI|hnhDw0+XZO94N7LcXo|d* zkdkRl@N&n;PGi4Ju2VAY^c@bMb3!_a<}~RR&nD{Y;~Zc73sAO{`+d=R*0D2-pBIjG z%D=IbFo>T-nw2qoBK;I4-=CqoRm!Q05!q}|trrKEiIEoFd2M%oBNsKqW zBS(`h4(@xq^NEkVi?ZyD=%)?AN-;PuJ;VAxOu9o}l9w79Sg|nZUNy%=RW^Rcdt1XG z4R%L|dxbR=Wp7X6qMwk84;O?)3PO?+Dm)pjxuOcFWvamxix4ni-L-Z!2LD0aj^ono z89C?1?n3ENo*|QNy|7nDkTk@nw12`6bNt$dPmfaOE$GE#@8*n}sopOpswiw7yL;E{ zkI3$ViFT_IMJodc;n%}>1x%jFXZ2;(1^;O8t)Z@N+-i|P&oFZkiAVEF@M8~~Xsjxnl~v`VtViuNnzFk=#%myUKgNznT)AZ4+yM3S{)znH&fU!FUg zhd&W@#jKGo7~LdSUG>`8WompCNWiY|3I2N0z`Cdh~1_bdEBnPz}Doge{j07A{~?e9CX)haHwlxw@PQ=}j68h*$n0 z%sn*skuuuIYGXMZDts}fwS-3KYRm`48QHrqh8n)bo4RJXX@%_Bi)^S^KZ#3vHnq!r zJb6&rXX4BG1=p_e3OoFU6^X`wVk3K~dm%P8$vG84j38Z8-Y@2@2NWsEGq{hdL=MWwEEnAP#M!s2*w?AuE;%^@f2;c!x4JZGu)ANr=`?;U zc8?o@?a*!?Rusrr$GKUa6IH|ET+C?0CKt>%Cm|JEv^ChZtGnc6+X|xy2W6k;8%)Js z-Y*Q`Zx-yDW5RxpqB@)S8)86M7Q@JrcPAG^bs6CfDmFLt!#8|tNOG7<9d!8SmG$*{ zD_+z*v!d^mX6L8cMDdAROBAtYjbcW6;whGIN zL>a+kqS59%re(Y@6pXPwp7cg+aX?6GzU-hGVcisSmXm(Ng^rMEE)6nYrx9U7D#n(7 zl6$ITQq~G56e~DX_^bAByWa3xvz5|jo#*X^vVLS*m_SLj^6$@^uPcn5D=KCFQ^&^Vm#f&C6wIaw&2aPT1%h z#*b@!O=RD)`C{Q0NQ>CQ+-@6YJFbx{lg>%^=o~L`m~d0QlnhcY#LtW8TuNi;YxN@82|kXi~5V^)*W6hU_`}1Gap@MQ-fW z)TCdGOQrD!n6H(C>rZ-SacFgxz}&>zd4W|d`1-&Vcw5h`A|jILSH%ynV%s583y{36 zYQEUS&S#P6Ek8c00J1mOkB4_x<*d><_)4;#LlQsA&H?+o>Oc{nVOD9qj#R354a$#4 zYsKP-CMa=bE9n^Jl)GQC3GlcEXvByfWjV+bUt8o^0X`vZ$?EYdQ#c0t{qx8B95Y{3 zu0yD-*bH$wU-rm8dn|EsR(w~()PFoo$bR3^{6%&MvFdjfy6-BOQi*L8-iwQ%ZO+*2 zTUVc7xrOsHrXMWlPG?1JG@=tjvMg!cH|3nvmb*^=1PrmJ0pe-GwIR;X`d|u!b!$*( za}dnaLqgPZTSegbHV;JV?o^x2uW74-$(nI0CeFx+on3q#P`)N^#JcFUr>c5pgbecd z|M##s{?F7ZmL%Wzt!t~^k|bY90uJ8pGtOe*1V^$BoZk{@A1iKXt-Dn9c_e zJ^sYfXxI}p_s)$ct%F4wA?XwOWv;Z-+0wK0w)?S{t`7_e$e&D28YV)DTCNRk zpGr?HjfwnKkS227qrb=NSh=o+Hf_nkgKgD3JT8B7%$vE&o&K($&bIm-hG`&13ncNn znCNSb=ig3>$#ajL#AFB8$HgBLg&+2va=KZn;XNRtAklz0Zul@&8}-^xu*Xl5ZRKVW zPrrCrvZq1SL}^TYd#2ktI~4y zxz4I$3xAZ~DW_+pSMp=6w<1|Dp}tOyK7?TN#*89+zHO*q8UyC*?poFQ?a;p3v+(!|e7}PEeDI&rSHF z1P|>2@0Bz1Vuj=5)v~)1S~3aA9PVI2>ebZ~3a3>j$uXj0tA7AMc_$ue6dS?fFuwae zFV^Q(@c!@0ONHW=)pl1oe>0BORHODu8=l}*wk?pkxd|_1?sg}MT-ruPZ#Jw&smAmC z5`T=@UDGeOtFH@lC5=+t{Npv5n^C{6q5Y#J{^bmJi;Og=-Wj~oo^y{NjWVZQF5r3| zz7jLTySK&XADwC-LN$Jch+G?g4PQ@F!bum7^0ZL>)iJjg0Mcn%d?s} zs)36PMqPH=mE@*3^D$)E00w^Rr1b2HVaFCcita9g_Vv*M7c9&Q-$#miX7oy)5C ywBK{90iN8RN_q;2=K1fa>c6AA|Io?hp;YB29<(ak#$r#A0T1%RB;?Q8${+Sh$u*Iw)1_7!pO+h977!fjJZ_+DHO&GKnN)+!rsgff5gQVj~)OpKnqYn0RS&TWY{4G zQ^$R^g^3Y9k}BHgU+w!#0PT-PWXx^x_+R$_7qJk+BBB6*Y^iD`qE94&$|+P1j)@A} zx35we=N+`qFvLDbP#vT)=RW)W&C-8q{$~AsCWerRRL#E6VZ;#PKKD}jOf=bt%5W=P9wW9vl-@`@nhjY31hyhyPC{Qb@Q zs{reMZ1L0}t14)ys>&$JQp5jG`;U|VsQxqT=k{;KH@m-b2C?`5vi(~7%NBYA0QC)O zY_fmZyz>EQ`U3#YiC;G1YXC5%0MI!2+xM{V*NcBtRG6lmTue-iY=93zc0Zv1wEt7^ zkMiHcZ~Mva@Apsa@WwvAUgY2?{C-dgp~0c#2z+Fi7r_TF^S_Juzh3y8Tfg}sZSUjj z6X8RmZskO+vH+4FHQgj)KvX~|2_HcEcNzXKm;L6$KK@vyr_co-e>BW z*zb^l4ln^6-~#+W2#5n2pa|4}7SIPK;2^L8j=%*R0|ej?LO=wF0r4OSq=IuG8{~ij zPy)(8C8!3qpb4~rF7OESfgvykCczAt2cN(y_zrd;2tq>)5EkNs1RxPe3Q~mBAzjD> zvV!a(7swOxgGdk=iib`^=b(#F0dyUzgziBtP#5$R8irm&@1P}U4f+9t!!R%$j1MLX zlY^N)2U> zazO>6PM|VSMW`B7Cu$fqi`qb=(OhULv<}(^?S+m)r=s)Gx6vKw7wCEP77aa(0F5Gz zDUB-)i6)sQm!^uQo#qA20?iIBGpz`%Can!Ekv5h#i?)omm3EMJo_2?hg-(o4ht82M zkS>WXkFJ{TA>9<+H+p*d1N0j7cJu-Cr|9$O@6tb^e@DNKVZ}&dj4N`f#Rm)^Lt-{@}uMnQ#ShUEpfs zdc%$2mgKhMj^Qrme#rfq2gjquL*Pm0spXmAh4D)A+Vh^^E#>X!-QeTrGvf>6%jfIi z`^?YIug@RIf0@6Xe@Orw*JcgOKeJOUks02zvT_Q-LNMcA5CaEInFIgx#CGmVs+6eItn^h`O4&!bSoxI-R^^aNno76IPgOP52-RxUB{g9+ zPqnLRCrF>N31GVNKN13F$h z*L7xe@w%S6*K}v}1ogc1Zs@(&7t$x{m+LPWNEief+%{M?lsAkpY%tt3(lk10)M*Si zHZx8)9x!1wIc!p3^49c#sh{aB(^WGSvlC{W=16l(^9$zV7JL>2i%N^tgK7s)9_+Qm zSUOl1SiZNCw2H85Jp?;+@X)10Q`REZBxo%m~FW2eLIw$tzDtr2YUtk zc>Bi=SO*V>Du?fmhK^Z|lZV9*lMi=0F*&(ARXS}t8#`Zgo;f0SxP?&TaMejyNY|N`^%$ZM~@$UdW`#6@UaITOdehyb)HC1XU{6nT`wE28(tgU z7T#CAR|!UhD}+x(ed1-}l8=thMV|#<9p8(-i+(zOm;9Fe_55@FKL;2E6a;(?G!HBZ z+zhe_x*4<=>>OMZLKET{(n4Y-1(A9~`9qI~4u?sFrH0LfYli29uSQr#lt)65M{@qD~ zlh+fV1n-3IM4`l##Q9TZrz(=@l7f<+C(9>aOkO|je7ZG-FXdFq>>p--+)8CkjYu6& z(@HBjgE$j#X5g&S+5EGVbYl9mbMohM&+TRqGM=4RIG=ZZFVi=3AWJo?I2)Bs%6@r4 z??S~zmWy#0XD=PP)OZE=v@eMN7jT4iMwZ&lVU*e&v{rQ7behwd2PX|9&3zFxy#bM7vw%en7vgb(8Xs=!Gz(dQ2Pac^*>UnJNxZ{b=leVXtPg|a; zKWpk!?Q85;>2G+h^1NX{b)a!jZLoPrW2kjld$|3D-ixjgpj55Igh z?mj;Aiuh{rb@1!26VVeplc%PTQ|WJ*-{ijKeS2eCV!CEVb>_i4<9Ge<9o|pQ5@tWm zMb7Qcr+i@ekh>tTP`RkM*tTT4H1yH!GkuF|t72Py`|%Iw9}7D%yXf7#pAtV?DVCI}y^y^< z>J0<$i3T8B1QbD%r@$$oOP0TET6W`xpycHukj;2D0I+@Tn9(w)e0`SxMebc1oU!zQ z7sYICVQz)63p&^-eiW^*(`%dO&w!JDdqE%J-vky;((kZ+sH7CWT>hR85`y+F{k$?> zO7U1r%;SICetNHOSzvc?$M0umuU>QefJZZ2GvN+@xTAj%O=VTkAdGJ=+a=8C9i}g9 zd3$l{s{gu24W;^#K_-Rw^M@mgnST_aL z)#Y{S-GVX`eQ%erd~8FtDO{i1^lq|4f}VtuW5HVctHx&pwxOtda9WKA=?=tm(wBy` zvJd-uqbEd`gmh2Oj=M`4E}ZJr&E@;zTlkb^{B5UWPd;;fm&wFl-9Y>Wl|IVanY%4k zqe`Pi$0^+z*!#C*IvRyiUgtEeIA~08eL?9#HS4cei}I4$Z^Nv268Ly*W*R<@-CpgM zx4~kcA>gZTzV={Nx!;$+lNy_|n_NkL`Gvpmoi*=R>&a4>j6ePSj&wxd^>TpQhG9jO zGqcLq>BIG|eEP(7UM5>^^`#WiMO7^3X*Gu3QQtNX6$&#%_{`VEYX-YN5PGfjn9`WQ zyYvL(lpyu73u6{?E%(rmHmn4@h*RH$U9O_+=3QQ+NAI=h%ngI1KmEK^e`T)f(o)m3 zC-Cd+W{2d9El1^6o@5v8)_-TENEFNJe!e-mR8dyzJNfiP#p1Ui70=pGTvryq2LbTj z_3qldn`SQ)56~t2eyc(8q;2=+?<;(Yq=NOG>#2@CD1=InS_TFqzkm?;SfnH$b^J0~ znX$`MIYwaf(94L$5YZemBZm(ul5)PL360i6o1Gy~m?yU|l$OO6+#Z_3ny;B`A*ZSI9=v%DTT5t*oYp>R zy7&A(mUv1$Sf#A{$6o7-Bd4otfz}*NhIT$Lk4${p*bLX|q}g)Tcz!P51`GLKe4*sg z3VOOnT82mDtVWbny5Z=PiY2=^x4xX5m93WH9a~-P>j}vu(jvn>sRY=YWVZrOgxQ%u zM@6s61N!9mM51+JN^j;H31-9`&jxLeHsR8Pk|HOwM`en-D`#d?KKIGOM_hXg*8`+R z9$uI(1GYl0q;>KXk^WdhYPWspMZSPIp_nVhfk?})^Ovv1ShIcjvvr8~8$Zsgr&z9h zXNa$Ur8J5Z{>?P z4Gt-6>zxoZN{VhbCT<+#*xT`ptG&mehv1szH!>Sh=zQ8n*8Ahhu5$m~{H}8B{gC?Q~p{TDGQFqu|sZM92w** z%oCv<(@>V>oXT`1O2$BSp*5k=?JFk3<7Y-_6>g)6M6SRHeLwVuR1SIgPygHuP+7Od`{IwTHI|37*N^#S1wyETW`2$SmER$Y<(2cP;&_VS8nMbb8CD zY1xRTOXtgiT1%#XhTjQcao(&xWxIEAN|BQ_<=YR%AOT%ev?$ZA@d53yKej|okNhl| zSY?jO|1|O9_9t9P&SQ?RSxwFJPW@Z{hp&b%mp)zalhJRrK?KJfF_{tGs1`|2bNy+p znTjh^Gu@ZEO()Lrk%?L2f|e&dKfK|9-mnx`SR&y zq}d2AZ`0!Xq!TSYEzH%g0yrHSYJ~ zdIx7OUYSsn)Ijujud79q*DDJQTIxaW{Uu%0N2oePijtS?V^;`efw2e`k*t0)uV@oC8vgW<0A9;jto&4EXJ#w%v z+`B}l790O2=Tfy-6{1I(M!7!A_kL4Po!QYO%|OSRK5}G77nX6y$}=lZ%D5(p=&BcU nz@^i*&#pJ~;rNNp%o3m6o;8o3@2{AsQdCl(_YKgy41s?EgUY&j diff --git a/src/assets/validators/v43.png b/src/assets/validators/v43.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac46d907b38f9d4dd69f2f2c8d1647ba7629683 GIT binary patch literal 7829 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 diff --git a/src/components/SkBtn.tsx b/src/components/SkBtn.tsx index b8f9cdf..1734524 100644 --- a/src/components/SkBtn.tsx +++ b/src/components/SkBtn.tsx @@ -49,6 +49,7 @@ export default function SkBtn(props: { 'btn', ['btnSm', size === 'sm'], ['btnSmLoading', size === 'sm'], + ['btnDisabled', props.loading], props.className )} > @@ -58,7 +59,13 @@ export default function SkBtn(props: { + + + } />
diff --git a/src/core/explorer.ts b/src/core/explorer.ts index 1f7b72f..f3c3ce1 100644 --- a/src/core/explorer.ts +++ b/src/core/explorer.ts @@ -33,6 +33,14 @@ export function getExplorerUrl(network: types.SkaleNetwork, chainName: string): return HTTPS_PREFIX + chainName + '.' + explorerBaseUrl } +export function getExplorerUrlForAddress( + network: types.SkaleNetwork, + chainName: string, + address: string +): string { + return addressUrl(getExplorerUrl(network, chainName), address) +} + export function getTotalAppCounters( countersArray: types.IAppCounters | null ): types.IAddressCounters | null { From 9e05b7b478ed05c5fe6733935b746c74c9baceeb Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 20 Nov 2024 19:08:56 +0000 Subject: [PATCH 12/16] Add sFUEL check to rewards claim --- src/components/delegation/ChainRewards.tsx | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/components/delegation/ChainRewards.tsx b/src/components/delegation/ChainRewards.tsx index 4c0e84b..9b7847b 100644 --- a/src/components/delegation/ChainRewards.tsx +++ b/src/components/delegation/ChainRewards.tsx @@ -34,7 +34,8 @@ import { walletClientToSigner, sendTransaction, styles, - SkPaper + SkPaper, + Station } from '@skalenetwork/metaport' import { types } from '@/core' @@ -62,8 +63,8 @@ import { getExplorerUrlForAddress } from '../../core/explorer' interface ChainRewardsProps { mpc: MetaportCore validator: types.staking.IValidator | null | undefined - address?: string - customAddress?: string + address?: types.AddressType + customAddress?: types.AddressType className?: string isXs?: boolean } @@ -124,14 +125,25 @@ const ChainRewards: React.FC = ({ } async function retrieveRewards() { - if (!paymaster.runner?.provider || !walletClient || !switchChainAsync) { + if (!paymaster.runner?.provider || !walletClient || !switchChainAsync || !address) { setErrorMsg('Something is wrong with your wallet, try again') return } setLoading(true) - setBtnText(`Switching network`) + setBtnText('Switching network') setErrorMsg(undefined) try { + const sFuelBalance = await paymaster.runner.provider.getBalance(address) + if (sFuelBalance === 0n) { + setBtnText('Mining sFUEL') + const station = new Station(paymasterChain, mpc) + const powResult = await station.doPoW(address) + if (!powResult.ok) { + setErrorMsg('Failed to mine sFUEL') + return + } + } + const { chainId } = await paymaster.runner.provider.getNetwork() const paymasterAddress = getPaymasterAddress(network) From 2495bc11916963c724bb07c849d859e619f469dc Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 21 Nov 2024 19:31:32 +0000 Subject: [PATCH 13/16] Add notifications system, restructure components --- src/App.scss | 20 ++- src/Portal.tsx | 121 +++++++++++++- src/Router.tsx | 156 +++++------------- src/SkDrawer.tsx | 33 +++- .../delegation/DelegationTotals.tsx | 2 +- .../delegation/DelegationsNotification.tsx | 42 +++++ src/core/delegation/delegations.ts | 10 ++ src/pages/Validator.tsx | 2 +- src/pages/Validators.tsx | 3 + 9 files changed, 268 insertions(+), 121 deletions(-) create mode 100644 src/components/delegation/DelegationsNotification.tsx diff --git a/src/App.scss b/src/App.scss index 8499c7a..3212a8c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -561,10 +561,6 @@ body::-webkit-scrollbar { border: none !important; } -button:hover { - border-color: $sk-prim !important; -} - .copyBoard { margin: 10px 0 !important; padding: 13pt 15pt !important; @@ -813,6 +809,22 @@ input[type=number] { } } +.chipNotification { + background: #e94e4e; + border-radius: 20px; + width: 20px; + height: 20px; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + + p { + color: #000000de !important; + font-weight: 600 !important; + } +} + .chipTrending { background: linear-gradient(180deg, #e56d36, #D0602D) !important; diff --git a/src/Portal.tsx b/src/Portal.tsx index 9b109dd..010e97c 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -21,9 +21,19 @@ * @copyright SKALE Labs 2023-Present */ +import { useState, useEffect } from 'react' +import { types } from '@/core' + import Box from '@mui/material/Box' import CssBaseline from '@mui/material/CssBaseline' -import { useMetaportStore, useWagmiAccount, Debug, cls, cmn } from '@skalenetwork/metaport' +import { + useMetaportStore, + useWagmiAccount, + Debug, + cls, + cmn, + PROXY_ENDPOINTS +} from '@skalenetwork/metaport' import Header from './Header' import SkDrawer from './SkDrawer' @@ -31,17 +41,122 @@ import Router from './Router' import SkBottomNavigation from './SkBottomNavigation' import ProfileModal from './components/profile/ProfileModal' +import { formatSChains } from './core/chain' +import { STATS_API } from './core/constants' +import { useSearchParams } from 'react-router-dom' +import { getValidatorDelegations } from './core/delegation/staking' +import { getValidator } from './core/delegation' +import { initContracts } from './core/contracts' + export default function Portal() { const mpc = useMetaportStore((state) => state.mpc) + + const [schains, setSchains] = useState([]) + const [metrics, setMetrics] = useState(null) + const [stats, setStats] = useState(null) + const [validator, setValidator] = useState(null) + const [validatorDelegations, setValidatorDelegations] = useState< + types.staking.IDelegation[] | null + >(null) + const [customAddress, setCustomAddress] = useState(undefined) + const [sc, setSc] = useState(null) + const [loadCalled, setLoadCalled] = useState(false) + + const endpoint = PROXY_ENDPOINTS[mpc.config.skaleNetwork] + const statsApi = STATS_API[mpc.config.skaleNetwork] + + const [searchParams, _] = useSearchParams() + const { address } = useWagmiAccount() if (!mpc) return
+ + useEffect(() => { + initSkaleContracts() + loadData() + }, []) + + useEffect(() => { + loadValidator() + }, [address, customAddress, sc]) + + useEffect(() => { + setCustomAddress((searchParams.get('_customAddress') as types.AddressType) ?? undefined) + }, [location]) + + async function initSkaleContracts() { + setLoadCalled(true) + if (loadCalled) return + setSc(await initContracts(mpc)) + } + + async function loadChains() { + try { + const response = await fetch(`https://${endpoint}/files/chains.json`) + const chainsJson = await response.json() + setSchains(formatSChains(chainsJson)) + } catch (e) { + console.log('Failed to load chains') + console.error(e) + } + } + + async function loadMetrics() { + try { + const response = await fetch(`https://${endpoint}/files/metrics.json`) + const metricsJson = await response.json() + setMetrics(metricsJson) + } catch (e) { + console.log('Failed to load metrics') + console.error(e) + } + } + + async function loadStats() { + if (statsApi === null) return + try { + const response = await fetch(statsApi) + const statsResp = await response.json() + setStats(statsResp.payload) + } catch (e) { + console.log('Failed to load stats') + console.error(e) + } + } + + async function loadValidator() { + const addr = customAddress ?? address + if (!sc || !addr) return + const validatorData = await getValidator(sc.validatorService, addr) + setValidator(validatorData) + if (validatorData && validatorData.id) { + setValidatorDelegations(await getValidatorDelegations(sc, validatorData.id)) + } + } + + async function loadData() { + loadChains() + loadMetrics() + loadStats() + loadValidator() + } + return (
- +
- +
diff --git a/src/Router.tsx b/src/Router.tsx index ff2dc74..0e5b2f8 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -26,7 +26,7 @@ import { useState, useEffect } from 'react' import { WalletClient } from 'viem' import { Helmet } from 'react-helmet' -import { useLocation, Routes, Route, useSearchParams, Navigate } from 'react-router-dom' +import { useLocation, Routes, Route, Navigate } from 'react-router-dom' import { TransitionGroup, CSSTransition } from 'react-transition-group' import { useTheme } from '@mui/material/styles' @@ -35,7 +35,6 @@ import { CircularProgress } from '@mui/material' import { useMetaportStore, - PROXY_ENDPOINTS, type MetaportState, useWagmiAccount, useWagmiWalletClient, @@ -73,15 +72,23 @@ import MetricsWarning from './components/MetricsWarning' import ScrollToTop from './components/ScrollToTop' import { getHistoryFromStorage, setHistoryToStorage } from './core/transferHistory' -import { BRIDGE_PAGES, MAINNET_CHAIN_NAME, STAKING_PAGES, STATS_API } from './core/constants' -import { getValidator, getValidators } from './core/delegation/validators' -import { initContracts } from './core/contracts' -import { getStakingInfoMap, getValidatorDelegations } from './core/delegation/staking' -import { formatSChains } from './core/chain' +import { BRIDGE_PAGES, MAINNET_CHAIN_NAME, STAKING_PAGES } from './core/constants' +import { getValidators } from './core/delegation/validators' +import { getStakingInfoMap } from './core/delegation/staking' import { loadMeta } from './core/metadata' -export default function Router() { +export default function Router(props: { + loadData: () => Promise + customAddress?: types.AddressType + schains: types.ISChain[] + stats: types.IStats | null + metrics: types.IMetrics | null + validator: types.staking.IValidator | null | undefined + validatorDelegations: types.staking.IDelegation[] | null + sc: types.staking.ISkaleContractsMap | null + loadValidator: () => Promise +}) { const location = useLocation() const currentUrl = `${window.location.origin}${location.pathname}${location.search}` @@ -89,21 +96,12 @@ export default function Router() { const isXs = useMediaQuery(theme.breakpoints.down('sm')) const [chainsMeta, setChainsMeta] = useState(null) - const [schains, setSchains] = useState([]) - const [metrics, setMetrics] = useState(null) - const [stats, setStats] = useState(null) const [termsAccepted, setTermsAccepted] = useState(false) const [stakingTermsAccepted, setStakingTermsAccepted] = useState(false) - const [loadCalled, setLoadCalled] = useState(false) - const [sc, setSc] = useState(null) const [validators, setValidators] = useState([]) - const [validator, setValidator] = useState(null) - const [validatorDelegations, setValidatorDelegations] = useState(null) 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) @@ -112,20 +110,11 @@ export default function Router() { const { data: walletClient } = useWagmiWalletClient() const { switchChainAsync } = useWagmiSwitchNetwork() - const [searchParams, _] = useSearchParams() - const endpoint = PROXY_ENDPOINTS[mpc.config.skaleNetwork] - const statsApi = STATS_API[mpc.config.skaleNetwork] - useEffect(() => { setTransfersHistory(getHistoryFromStorage(mpc.config.skaleNetwork)) - initSkaleContracts() loadMetadata() }, []) - useEffect(() => { - setCustomAddress((searchParams.get('_customAddress') as types.AddressType) ?? undefined) - }, [location]) - useEffect(() => { if (transfersHistory.length !== 0) { setHistoryToStorage(transfersHistory, mpc.config.skaleNetwork) @@ -144,75 +133,19 @@ export default function Router() { return walletClientToSigner(walletClient!) } - async function loadData() { - loadChains() - loadMetrics() - loadStats() - } - async function loadMetadata() { setChainsMeta(await loadMeta(mpc.config.skaleNetwork)) } - async function loadChains() { - try { - const response = await fetch(`https://${endpoint}/files/chains.json`) - const chainsJson = await response.json() - setSchains(formatSChains(chainsJson)) - } catch (e) { - console.log('Failed to load chains') - console.error(e) - } - } - - async function loadMetrics() { - try { - const response = await fetch(`https://${endpoint}/files/metrics.json`) - const metricsJson = await response.json() - setMetrics(metricsJson) - } catch (e) { - console.log('Failed to load metrics') - console.error(e) - } - } - - async function loadStats() { - if (statsApi === null) return - try { - const response = await fetch(statsApi) - const statsResp = await response.json() - setStats(statsResp.payload) - } catch (e) { - console.log('Failed to load stats') - console.error(e) - } - } - - async function initSkaleContracts() { - setLoadCalled(true) - if (loadCalled) return - setSc(await initContracts(mpc)) - } - async function loadValidators() { - if (!sc) return - const validatorsData = await getValidators(sc.validatorService) + if (!props.sc) return + const validatorsData = await getValidators(props.sc.validatorService) setValidators(validatorsData) } - async function loadValidator() { - const addr = customAddress ?? address - if (!sc || !addr) return - const validatorData = await getValidator(sc.validatorService, addr) - setValidator(validatorData) - if (validatorData && validatorData.id) { - setValidatorDelegations(await getValidatorDelegations(sc, validatorData.id)) - } - } - async function loadStakingInfo() { - if (!sc) return - setSi(await getStakingInfoMap(sc, customAddress ?? address)) + if (!props.sc) return + setSi(await getStakingInfoMap(props.sc, props.customAddress ?? address)) } function isToSPage(pages: any): boolean { @@ -262,7 +195,7 @@ export default function Router() { - + @@ -273,8 +206,8 @@ export default function Router() { } /> @@ -288,9 +221,9 @@ export default function Router() { element={ @@ -301,10 +234,10 @@ export default function Router() { path=":name" element={ } /> @@ -336,8 +269,8 @@ export default function Router() { chainsMeta={chainsMeta} mpc={mpc} isXs={isXs} - metrics={metrics} - loadData={loadData} + metrics={props.metrics} + loadData={props.loadData} /> } /> @@ -362,10 +295,10 @@ export default function Router() { validators={validators} loadValidators={loadValidators} loadStakingInfo={loadStakingInfo} - sc={sc} + sc={props.sc} si={si} - address={customAddress ?? address} - customAddress={customAddress} + address={props.customAddress ?? address} + customAddress={props.customAddress} getMainnetSigner={getMainnetSigner} /> } @@ -377,7 +310,8 @@ export default function Router() { mpc={mpc} validators={validators} loadValidators={loadValidators} - sc={sc} + sc={props.sc} + validatorDelegations={props.validatorDelegations} /> } /> @@ -387,12 +321,12 @@ export default function Router() { } @@ -406,7 +340,7 @@ export default function Router() { validators={validators} loadValidators={loadValidators} loadStakingInfo={loadStakingInfo} - sc={sc} + sc={props.sc} si={si} address={address} getMainnetSigner={getMainnetSigner} @@ -421,7 +355,7 @@ export default function Router() { validators={validators} loadValidators={loadValidators} loadStakingInfo={loadStakingInfo} - sc={sc} + sc={props.sc} si={si} /> } diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index e0934aa..6efbb36 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -1,5 +1,29 @@ +/** + * @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 SkDrawer.tsx + * @copyright SKALE Labs 2024-Present + */ + import { cls, cmn } from '@skalenetwork/metaport' import { useLocation, Link } from 'react-router-dom' +import { types } from '@/core' import Box from '@mui/material/Box' @@ -23,10 +47,13 @@ import LinkRoundedIcon from '@mui/icons-material/LinkRounded' import ExploreOutlinedIcon from '@mui/icons-material/ExploreOutlined' import { GET_STARTED_URL } from './core/constants' +import DelegationsNotification from './components/delegation/DelegationsNotification' const drawerWidth = 220 -export default function SkDrawer() { +export default function SkDrawer(props: { + validatorDelegations: types.staking.IDelegation[] | null +}) { const location = useLocation() return ( @@ -152,6 +179,10 @@ export default function SkDrawer() { + diff --git a/src/components/delegation/DelegationTotals.tsx b/src/components/delegation/DelegationTotals.tsx index a9d46a2..a18c757 100644 --- a/src/components/delegation/DelegationTotals.tsx +++ b/src/components/delegation/DelegationTotals.tsx @@ -37,7 +37,7 @@ import SkStack from '../SkStack' import Tile from '../Tile' interface DelegationTotalsProps { - delegations: types.staking.IDelegation[] + delegations: types.staking.IDelegation[] | null className?: string } diff --git a/src/components/delegation/DelegationsNotification.tsx b/src/components/delegation/DelegationsNotification.tsx new file mode 100644 index 0000000..90b4196 --- /dev/null +++ b/src/components/delegation/DelegationsNotification.tsx @@ -0,0 +1,42 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file DelegationsNotification.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { cls, cmn } from '@skalenetwork/metaport' +import { types } from '@/core' + +import { getProposedDelegationsCount } from '../../core/delegation' + +export default function DelegationsNotification(props: { + validatorDelegations: types.staking.IDelegation[] | null + className?: string +}) { + const proposedDelegations = getProposedDelegationsCount(props.validatorDelegations) + + if (proposedDelegations && proposedDelegations > 0) { + return ( +
+

{proposedDelegations}

+
+ ) + } +} diff --git a/src/core/delegation/delegations.ts b/src/core/delegation/delegations.ts index ab78905..72642f9 100644 --- a/src/core/delegation/delegations.ts +++ b/src/core/delegation/delegations.ts @@ -307,3 +307,13 @@ export function calculateDelegationTotals( return totals }, initialTotals) } + +export function getProposedDelegationsCount( + validatorDelegations: types.staking.IDelegation[] | null +): number | null { + if (!validatorDelegations) return null + + return validatorDelegations.filter( + (delegation) => Number(delegation.stateId) === DelegationState.PROPOSED + ).length +} diff --git a/src/pages/Validator.tsx b/src/pages/Validator.tsx index f311f16..6b8317f 100644 --- a/src/pages/Validator.tsx +++ b/src/pages/Validator.tsx @@ -66,7 +66,7 @@ export default function Validator(props: { loadValidator: () => Promise validator: types.staking.IValidator | null | undefined isXs: boolean - delegations: types.staking.IDelegation[] + delegations: types.staking.IDelegation[] | null getMainnetSigner: () => Promise }) { const [sortBy, setSortBy] = useState('id') diff --git a/src/pages/Validators.tsx b/src/pages/Validators.tsx index 5314d52..0f957d0 100644 --- a/src/pages/Validators.tsx +++ b/src/pages/Validators.tsx @@ -33,12 +33,14 @@ import ManageAccountsRoundedIcon from '@mui/icons-material/ManageAccountsRounded import Validators from '../components/delegation/Validators' import SkPageInfoIcon from '../components/SkPageInfoIcon' import { META_TAGS } from '../core/meta' +import DelegationsNotification from '../components/delegation/DelegationsNotification' export default function ValidatorsPage(props: { mpc: MetaportCore validators: types.staking.IValidator[] sc: types.staking.ISkaleContractsMap | null loadValidators: () => void + validatorDelegations: types.staking.IDelegation[] | null }) { useEffect(() => { if (props.sc !== null) { @@ -61,6 +63,7 @@ export default function ValidatorsPage(props: { variant="contained" className={cls('btnMd', cmn.mri10)} startIcon={} + endIcon={} > Manage Validator From 435d99ee739d8509f295817f41d5a712bdf7d104 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 21 Nov 2024 20:15:29 +0000 Subject: [PATCH 14/16] Fix custom address handling in chain rewards --- src/components/delegation/ChainRewards.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/delegation/ChainRewards.tsx b/src/components/delegation/ChainRewards.tsx index 9b7847b..7d21437 100644 --- a/src/components/delegation/ChainRewards.tsx +++ b/src/components/delegation/ChainRewards.tsx @@ -94,6 +94,8 @@ const ChainRewards: React.FC = ({ const { data: walletClient } = useWagmiWalletClient() const { switchChainAsync } = useWagmiSwitchNetwork() + const addr = customAddress ?? address + useEffect(() => { loadData() const intervalId = setInterval(loadData, DEFAULT_UPDATE_INTERVAL_MS) @@ -121,7 +123,7 @@ const ChainRewards: React.FC = ({ setTokenUrl(getExplorerUrlForAddress(network, paymasterChain, tokenAddress)) setSklToken(skl) } - setTokenBalance(await skl.balanceOf(address)) + setTokenBalance(await skl.balanceOf(addr)) } async function retrieveRewards() { From 06c91ba46c4ce7630d0bbda744400922ca1d98d1 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Fri, 22 Nov 2024 19:16:32 +0000 Subject: [PATCH 15/16] Add tooltip to notification, update custom address logic --- src/Portal.tsx | 17 +++++++++-------- src/Router.tsx | 8 +++++++- .../delegation/DelegationsNotification.tsx | 13 ++++++++++--- src/pages/Validator.tsx | 6 ------ 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/Portal.tsx b/src/Portal.tsx index 010e97c..ffa3259 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -43,7 +43,6 @@ import ProfileModal from './components/profile/ProfileModal' import { formatSChains } from './core/chain' import { STATS_API } from './core/constants' -import { useSearchParams } from 'react-router-dom' import { getValidatorDelegations } from './core/delegation/staking' import { getValidator } from './core/delegation' import { initContracts } from './core/contracts' @@ -65,8 +64,6 @@ export default function Portal() { const endpoint = PROXY_ENDPOINTS[mpc.config.skaleNetwork] const statsApi = STATS_API[mpc.config.skaleNetwork] - const [searchParams, _] = useSearchParams() - const { address } = useWagmiAccount() if (!mpc) return
@@ -79,10 +76,6 @@ export default function Portal() { loadValidator() }, [address, customAddress, sc]) - useEffect(() => { - setCustomAddress((searchParams.get('_customAddress') as types.AddressType) ?? undefined) - }, [location]) - async function initSkaleContracts() { setLoadCalled(true) if (loadCalled) return @@ -125,11 +118,18 @@ export default function Portal() { async function loadValidator() { const addr = customAddress ?? address - if (!sc || !addr) return + if (!sc || !addr) { + setValidator(undefined) + setValidatorDelegations(null) + return + } const validatorData = await getValidator(sc.validatorService, addr) setValidator(validatorData) if (validatorData && validatorData.id) { setValidatorDelegations(await getValidatorDelegations(sc, validatorData.id)) + } else { + setValidator(undefined) + setValidatorDelegations(null) } } @@ -154,6 +154,7 @@ export default function Portal() { validator={validator} validatorDelegations={validatorDelegations} customAddress={customAddress} + setCustomAddress={setCustomAddress} sc={sc} loadValidator={loadValidator} /> diff --git a/src/Router.tsx b/src/Router.tsx index 0e5b2f8..c29cb48 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -26,7 +26,7 @@ import { useState, useEffect } from 'react' import { WalletClient } from 'viem' import { Helmet } from 'react-helmet' -import { useLocation, Routes, Route, Navigate } from 'react-router-dom' +import { useLocation, Routes, Route, Navigate, useSearchParams } from 'react-router-dom' import { TransitionGroup, CSSTransition } from 'react-transition-group' import { useTheme } from '@mui/material/styles' @@ -81,6 +81,7 @@ import { loadMeta } from './core/metadata' export default function Router(props: { loadData: () => Promise customAddress?: types.AddressType + setCustomAddress: (address: types.AddressType) => void schains: types.ISChain[] stats: types.IStats | null metrics: types.IMetrics | null @@ -105,6 +106,7 @@ export default function Router(props: { const mpc = useMetaportStore((state: MetaportState) => state.mpc) const transfersHistory = useMetaportStore((state) => state.transfersHistory) const setTransfersHistory = useMetaportStore((state) => state.setTransfersHistory) + const [searchParams, _] = useSearchParams() const { address } = useWagmiAccount() const { data: walletClient } = useWagmiWalletClient() @@ -115,6 +117,10 @@ export default function Router(props: { loadMetadata() }, []) + useEffect(() => { + props.setCustomAddress((searchParams.get('_customAddress') as types.AddressType) ?? undefined) + }, [location]) + useEffect(() => { if (transfersHistory.length !== 0) { setHistoryToStorage(transfersHistory, mpc.config.skaleNetwork) diff --git a/src/components/delegation/DelegationsNotification.tsx b/src/components/delegation/DelegationsNotification.tsx index 90b4196..f9a9191 100644 --- a/src/components/delegation/DelegationsNotification.tsx +++ b/src/components/delegation/DelegationsNotification.tsx @@ -25,6 +25,7 @@ import { cls, cmn } from '@skalenetwork/metaport' import { types } from '@/core' import { getProposedDelegationsCount } from '../../core/delegation' +import { Tooltip } from '@mui/material' export default function DelegationsNotification(props: { validatorDelegations: types.staking.IDelegation[] | null @@ -34,9 +35,15 @@ export default function DelegationsNotification(props: { if (proposedDelegations && proposedDelegations > 0) { return ( -
-

{proposedDelegations}

-
+ 1 && 's' + }`} + > +
+

{proposedDelegations}

+
+
) } } diff --git a/src/pages/Validator.tsx b/src/pages/Validator.tsx index 6b8317f..7176d3a 100644 --- a/src/pages/Validator.tsx +++ b/src/pages/Validator.tsx @@ -106,12 +106,6 @@ export default function Validator(props: { const remainingItems = Math.max(0, sortedDelegations.length - visibleItems) - useEffect(() => { - if (props.sc !== null) { - props.loadValidator() - } - }, [props.sc, props.address, props.customAddress]) - useEffect(() => { setVisibleItems(ITEMS_PER_PAGE) }, [sortBy]) From d0abddddec48bf8500f8abcc68512448ccd18ea0 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 25 Nov 2024 13:41:34 +0000 Subject: [PATCH 16/16] Minor hotfix for validator loading state --- src/Portal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Portal.tsx b/src/Portal.tsx index ffa3259..490ffb6 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -119,7 +119,7 @@ export default function Portal() { async function loadValidator() { const addr = customAddress ?? address if (!sc || !addr) { - setValidator(undefined) + setValidator(null) setValidatorDelegations(null) return }