From 540f46c18569bec7c1e81035b2a9727ff57ad9b3 Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Thu, 1 Jun 2023 17:32:05 +0200 Subject: [PATCH 1/5] refactor collators table --- src/pages/collators/CollatorRewards.tsx | 110 +++++++++++++ src/pages/collators/Collators.tsx | 198 +----------------------- src/pages/collators/CollatorsTable.tsx | 143 +++++++++++++++++ src/pages/collators/columns.tsx | 8 +- 4 files changed, 265 insertions(+), 194 deletions(-) create mode 100644 src/pages/collators/CollatorRewards.tsx create mode 100644 src/pages/collators/CollatorsTable.tsx diff --git a/src/pages/collators/CollatorRewards.tsx b/src/pages/collators/CollatorRewards.tsx new file mode 100644 index 00000000..846e96fe --- /dev/null +++ b/src/pages/collators/CollatorRewards.tsx @@ -0,0 +1,110 @@ +import { useEffect, useMemo, useState } from 'react'; +import RewardsIcon from '../../assets/collators-rewards-icon'; +import StakedIcon from '../../assets/collators-staked-icon'; +import { nativeToFormat } from '../../helpers/parseNumbers'; +import { UserStaking } from './columns'; +import { useNodeInfoState } from '../../NodeInfoProvider'; +import { useGlobalState } from '../../GlobalStateProvider'; +import { getAddressForFormat } from '../../helpers/addressFormatter'; +import { ParachainStakingCandidate, useStakingPallet } from '../../hooks/staking/staking'; +import ClaimRewardsDialog from './dialogs/ClaimRewardsDialog'; + +function CollatorRewards() { + const [userAvailableBalance, setUserAvailableBalance] = useState('0.00'); + const [userStaking, setUserStaking] = useState(); + const [claimDialogOpen, setClaimDialogOpen] = useState(false); + + const { api, tokenSymbol, ss58Format } = useNodeInfoState().state; + const { walletAccount } = useGlobalState(); + const { candidates, estimatedRewards } = useStakingPallet(); + + const userAccountAddress = useMemo(() => { + return walletAccount && ss58Format ? getAddressForFormat(walletAccount?.address, ss58Format) : ''; + }, [walletAccount, ss58Format]); + + useMemo(() => { + setUserStaking(undefined); + return candidates?.forEach((candidate) => { + const isDelegator = candidate.delegators.find((delegator) => delegator.owner === userAccountAddress); + if (isDelegator) { + setUserStaking({ + candidateId: candidate.id, + amount: isDelegator.amount, + }); + } + }); + }, [candidates, userAccountAddress, setUserStaking]); + + useEffect(() => { + const fetchAvailableBalance = async () => { + if (!api || !walletAccount) { + return '0'; + } + const { data: balance } = await api.query.system.account(walletAccount?.address); + return balance.free.sub(balance.miscFrozen).toString(); + }; + + fetchAvailableBalance().then((balance) => setUserAvailableBalance(balance)); + }, [api, walletAccount]); + + return ( + <> +
+
+
+

Collators

+
+
+ +
+
+

{nativeToFormat(userStaking?.amount || '0.00', tokenSymbol)}

+

My Staking

+
+
+

{nativeToFormat(userAvailableBalance, tokenSymbol)}

+

Free balance

+
+
+ +
+
+
+
+
+
+

Staking Rewards

+
+
+ +
+
+

{nativeToFormat(estimatedRewards, tokenSymbol)}

+

Estimated reward

+
+
+ +
+
+
+
+
+ setClaimDialogOpen(false)} + /> + + ); +} + +export default CollatorRewards; diff --git a/src/pages/collators/Collators.tsx b/src/pages/collators/Collators.tsx index 96c0b72d..ea0dbb45 100644 --- a/src/pages/collators/Collators.tsx +++ b/src/pages/collators/Collators.tsx @@ -1,199 +1,11 @@ -import { useEffect, useMemo, useState } from 'preact/hooks'; -import RewardsIcon from '../../assets/collators-rewards-icon'; -import StakedIcon from '../../assets/collators-staked-icon'; -import { useGlobalState } from '../../GlobalStateProvider'; -import { nativeToFormat } from '../../helpers/parseNumbers'; -import { useNodeInfoState } from '../../NodeInfoProvider'; - -import Table from '../../components/Table'; -import { getAddressForFormat } from '../../helpers/addressFormatter'; -import { ParachainStakingCandidate, useStakingPallet } from '../../hooks/staking/staking'; -import { - actionsColumn, - apyColumn, - delegatorsColumn, - myStakedColumn, - nameColumn, - stakedColumn, - TCollator, - UserStaking, -} from './columns'; -import ClaimRewardsDialog from './dialogs/ClaimRewardsDialog'; -import ExecuteDelegationDialogs from './dialogs/ExecuteDelegationDialogs'; -import { PalletIdentityInfo, useIdentityPallet } from '../../hooks/useIdentityPallet'; -import { PalletIdentityRegistrarInfo } from '@polkadot/types/lookup'; +import CollatorsTable from './CollatorsTable'; +import CollatorRewards from './CollatorRewards'; function Collators() { - const { api, tokenSymbol, ss58Format } = useNodeInfoState().state; - const { walletAccount } = useGlobalState(); - - const { candidates, inflationInfo, estimatedRewards } = useStakingPallet(); - const { identityOf } = useIdentityPallet(); - - // Holds the candidate for which the delegation modal is to be shown - const [selectedCandidate, setSelectedCandidate] = useState(undefined); - - const [userAvailableBalance, setUserAvailableBalance] = useState('0.00'); - const [userStaking, setUserStaking] = useState(); - const [claimDialogOpen, setClaimDialogOpen] = useState(false); - const [unbonding, setUnbonding] = useState(false); - const [data, setData] = useState(); - - const userAccountAddress = useMemo(() => { - return walletAccount && ss58Format ? getAddressForFormat(walletAccount?.address, ss58Format) : ''; - }, [walletAccount, ss58Format]); - - useMemo(() => { - setUserStaking(undefined); - return candidates?.forEach((candidate) => { - const isDelegator = candidate.delegators.find((delegator) => delegator.owner === userAccountAddress); - if (isDelegator) { - setUserStaking({ - candidateId: candidate.id, - amount: isDelegator.amount, - }); - } - }); - }, [candidates, userAccountAddress, setUserStaking]); - - useEffect(() => { - const fetchAvailableBalance = async () => { - if (!api || !walletAccount) { - return '0'; - } - const { data: balance } = await api.query.system.account(walletAccount?.address); - return balance.free.sub(balance.miscFrozen).toString(); - }; - - fetchAvailableBalance().then((balance) => setUserAvailableBalance(balance)); - }, [api, walletAccount]); - - useEffect(() => { - const identitiesPrefetch = async (candidatesArray: any) => { - const m: Map = new Map(); - for (let i = 0; i < candidatesArray.length; i++) { - const c = candidatesArray[i]; - m.set(c.id, await identityOf(c.id)); - } - return m; - }; - - const decorateCandidates = ( - candidatesArray: ParachainStakingCandidate[], - identities: Map, - ) => { - return candidatesArray?.map((candidate) => ({ - candidate: candidate, - collator: candidate.id, - identityInfo: identities.get(candidate.id), - totalStaked: nativeToFormat(candidate.total, tokenSymbol), - delegators: candidate.delegators.length, - apy: inflationInfo?.delegator.rewardRate.annual || '0.00%', - })); - }; - - if (candidates) { - identitiesPrefetch(candidates).then((identitiesMap) => { - const d = decorateCandidates(candidates, identitiesMap); - setData(d); - }); - } - }, [candidates, inflationInfo?.delegator.rewardRate.annual, tokenSymbol, identityOf]); - - const columns = useMemo(() => { - return [ - nameColumn, - stakedColumn, - delegatorsColumn, - apyColumn, - myStakedColumn({ userAccountAddress, tokenSymbol }), - actionsColumn({ - userAccountAddress, - walletAccount, - userStaking, - setSelectedCandidate, - setUnbonding, - }), - ]; - }, [tokenSymbol, userAccountAddress, userStaking, walletAccount, setUnbonding]); - return ( -
-
-
-
-

Collators

-
-
- -
-
-

{nativeToFormat(userStaking?.amount || '0.00', tokenSymbol)}

-

My Staking

-
-
-

{nativeToFormat(userAvailableBalance, tokenSymbol)}

-

Free balance

-
-
- -
-
-
-
-
-
-

Staking Rewards

-
-
- -
-
-

{nativeToFormat(estimatedRewards, tokenSymbol)}

-

Estimated reward

-
-
- -
-
-
-
-
- { - setSelectedCandidate(undefined); - setUnbonding(false); - }} - /> - setClaimDialogOpen(false)} - /> - +
+ +
); } diff --git a/src/pages/collators/CollatorsTable.tsx b/src/pages/collators/CollatorsTable.tsx new file mode 100644 index 00000000..2e14e497 --- /dev/null +++ b/src/pages/collators/CollatorsTable.tsx @@ -0,0 +1,143 @@ +import { useEffect, useMemo, useState } from 'preact/hooks'; +import RewardsIcon from '../../assets/collators-rewards-icon'; +import StakedIcon from '../../assets/collators-staked-icon'; +import { useGlobalState } from '../../GlobalStateProvider'; +import { nativeToFormat } from '../../helpers/parseNumbers'; +import { useNodeInfoState } from '../../NodeInfoProvider'; + +import Table from '../../components/Table'; +import { getAddressForFormat } from '../../helpers/addressFormatter'; +import { ParachainStakingCandidate, useStakingPallet } from '../../hooks/staking/staking'; +import { + actionsColumn, + apyColumn, + delegatorsColumn, + myStakedColumn, + nameColumn, + stakedColumn, + TCollator, + UserStaking, +} from './columns'; +import ExecuteDelegationDialogs from './dialogs/ExecuteDelegationDialogs'; +import { PalletIdentityInfo, useIdentityPallet } from '../../hooks/useIdentityPallet'; + +function CollatorsTable() { + const { api, tokenSymbol, ss58Format } = useNodeInfoState().state; + const { walletAccount } = useGlobalState(); + const { candidates, inflationInfo } = useStakingPallet(); + const { identityOf } = useIdentityPallet(); + + // Holds the candidate for which the delegation modal is to be shown + const [selectedCandidate, setSelectedCandidate] = useState(undefined); + const [userAvailableBalance, setUserAvailableBalance] = useState('0.00'); + const [userStaking, setUserStaking] = useState(); + const [claimDialogOpen, setClaimDialogOpen] = useState(false); + const [unbonding, setUnbonding] = useState(false); + const [data, setData] = useState(); + + const userAccountAddress = useMemo(() => { + return walletAccount && ss58Format ? getAddressForFormat(walletAccount?.address, ss58Format) : ''; + }, [walletAccount, ss58Format]); + + useMemo(() => { + setUserStaking(undefined); + return candidates?.forEach((candidate) => { + const isDelegator = candidate.delegators.find((delegator) => delegator.owner === userAccountAddress); + if (isDelegator) { + setUserStaking({ + candidateId: candidate.id, + amount: isDelegator.amount, + }); + } + }); + }, [candidates, userAccountAddress, setUserStaking]); + + useEffect(() => { + const fetchAvailableBalance = async () => { + if (!api || !walletAccount) { + return '0'; + } + const { data: balance } = await api.query.system.account(walletAccount?.address); + return balance.free.sub(balance.miscFrozen).toString(); + }; + + fetchAvailableBalance().then((balance) => setUserAvailableBalance(balance)); + }, [api, walletAccount]); + + useEffect(() => { + const identitiesPrefetch = async (candidatesArray: any) => { + const m: Map = new Map(); + for (let i = 0; i < candidatesArray.length; i++) { + const c = candidatesArray[i]; + m.set(c.id, await identityOf(c.id)); + } + return m; + }; + + const decorateCandidates = ( + candidatesArray: ParachainStakingCandidate[], + identities: Map, + ) => { + return candidatesArray?.map((candidate) => ({ + candidate: candidate, + collator: candidate.id, + identityInfo: identities.get(candidate.id), + totalStaked: nativeToFormat(candidate.total, tokenSymbol), + delegators: candidate.delegators.length, + apy: inflationInfo?.delegator.rewardRate.annual || '0.00%', + })); + }; + + if (candidates) { + identitiesPrefetch(candidates).then((identitiesMap) => { + const d = decorateCandidates(candidates, identitiesMap); + setData(d); + }); + } + }, [candidates, inflationInfo?.delegator.rewardRate.annual, tokenSymbol, identityOf]); + + const columns = useMemo(() => { + return [ + nameColumn, + stakedColumn, + delegatorsColumn, + apyColumn, + myStakedColumn({ userAccountAddress, tokenSymbol }), + actionsColumn({ + userAccountAddress, + walletAccount, + userStaking, + setSelectedCandidate, + setUnbonding, + }), + ]; + }, [tokenSymbol, userAccountAddress, userStaking, walletAccount, setUnbonding]); + + return ( + <> + { + setSelectedCandidate(undefined); + setUnbonding(false); + }} + /> +
+ + ); +} + +export default CollatorsTable; diff --git a/src/pages/collators/columns.tsx b/src/pages/collators/columns.tsx index c4b01d61..35ea3b20 100644 --- a/src/pages/collators/columns.tsx +++ b/src/pages/collators/columns.tsx @@ -30,8 +30,14 @@ export const nameColumn: ColumnDef = { accessorKey: 'collator', accessorFn: ({ identityInfo }) => identityInfo?.display || '0', cell: ({ row }) => { + const desc = row.original.identityInfo + ? `${row.original.identityInfo.email ? row.original.identityInfo.email + '\n' : ''}` + + `${row.original.identityInfo.riot ? row.original.identityInfo.riot + '\n' : ''}` + + `${row.original.identityInfo.twitter ? row.original.identityInfo.twitter + '\n' : ''}` + + `${row.original.identityInfo.web ? row.original.identityInfo.web + '\n' : ''}` + : ''; return ( -
+
{(row.original.identityInfo ? row.original.identityInfo.display : 'Unknown') + ' |'}
From 362122d4e3a7fb7ba0f6109a73cc097c70018500 Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Thu, 1 Jun 2023 18:20:55 +0200 Subject: [PATCH 2/5] Call increment delegator rewards o user's demand --- src/hooks/staking/staking.tsx | 6 +++ src/pages/collators/CollatorRewards.tsx | 58 +++++++++++++++++++++++-- src/pages/collators/CollatorsTable.tsx | 3 -- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/hooks/staking/staking.tsx b/src/hooks/staking/staking.tsx index cd16eaca..717cf5bb 100644 --- a/src/hooks/staking/staking.tsx +++ b/src/hooks/staking/staking.tsx @@ -174,6 +174,12 @@ export function useStakingPallet() { return api.tx.parachainStaking.leaveDelegators(); }, + createUpdateDelegatorRewardsExtrinsic() { + if (!api) { + return undefined; + } + return api.tx.parachainStaking?.incrementDelegatorRewards(); + }, }; }, [api, candidates, inflationInfo, fees, minDelegatorStake, estimatedRewards, ss58Format]); diff --git a/src/pages/collators/CollatorRewards.tsx b/src/pages/collators/CollatorRewards.tsx index 846e96fe..80339cea 100644 --- a/src/pages/collators/CollatorRewards.tsx +++ b/src/pages/collators/CollatorRewards.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import RewardsIcon from '../../assets/collators-rewards-icon'; import StakedIcon from '../../assets/collators-staked-icon'; import { nativeToFormat } from '../../helpers/parseNumbers'; @@ -8,15 +8,19 @@ import { useGlobalState } from '../../GlobalStateProvider'; import { getAddressForFormat } from '../../helpers/addressFormatter'; import { ParachainStakingCandidate, useStakingPallet } from '../../hooks/staking/staking'; import ClaimRewardsDialog from './dialogs/ClaimRewardsDialog'; +import { toast } from 'react-toastify'; +import { getErrors, getEventBySectionAndMethod } from '../../helpers/substrate'; +import { Button } from 'react-daisyui'; function CollatorRewards() { const [userAvailableBalance, setUserAvailableBalance] = useState('0.00'); const [userStaking, setUserStaking] = useState(); const [claimDialogOpen, setClaimDialogOpen] = useState(false); + const [submissionPending, setSubmissionPending] = useState(false); const { api, tokenSymbol, ss58Format } = useNodeInfoState().state; const { walletAccount } = useGlobalState(); - const { candidates, estimatedRewards } = useStakingPallet(); + const { candidates, estimatedRewards, createUpdateDelegatorRewardsExtrinsic } = useStakingPallet(); const userAccountAddress = useMemo(() => { return walletAccount && ss58Format ? getAddressForFormat(walletAccount?.address, ss58Format) : ''; @@ -47,6 +51,44 @@ function CollatorRewards() { fetchAvailableBalance().then((balance) => setUserAvailableBalance(balance)); }, [api, walletAccount]); + const updateRewardsExtrinsic = useMemo(() => { + if (!api) { + return undefined; + } + + return createUpdateDelegatorRewardsExtrinsic(); + }, [api, createUpdateDelegatorRewardsExtrinsic]); + + const submitUpdateExtrinsic = useCallback(() => { + if (!updateRewardsExtrinsic || !api || !walletAccount) { + return; + } + setSubmissionPending(true); + updateRewardsExtrinsic + .signAndSend(walletAccount.address, { signer: walletAccount.signer as any }, (result) => { + const { status, events } = result; + const errors = getErrors(events, api); + if (status.isInBlock) { + if (errors.length > 0) { + const errorMessage = `Transaction failed with errors: ${errors.join('\n')}`; + console.error(errorMessage); + toast(errorMessage, { type: 'error' }); + } + } else if (status.isFinalized) { + setSubmissionPending(false); + if (errors.length === 0) { + toast('Delegator rewards updated', { type: 'success' }); + } + } + }) + .catch((error) => { + toast('Transaction submission failed: ' + error.toString(), { + type: 'error', + }); + setSubmissionPending(false); + }); + }, [api, setSubmissionPending, updateRewardsExtrinsic, walletAccount]); + return ( <>
@@ -85,13 +127,21 @@ function CollatorRewards() {

Estimated reward

- + +
diff --git a/src/pages/collators/CollatorsTable.tsx b/src/pages/collators/CollatorsTable.tsx index 2e14e497..33cb1bb2 100644 --- a/src/pages/collators/CollatorsTable.tsx +++ b/src/pages/collators/CollatorsTable.tsx @@ -1,6 +1,4 @@ import { useEffect, useMemo, useState } from 'preact/hooks'; -import RewardsIcon from '../../assets/collators-rewards-icon'; -import StakedIcon from '../../assets/collators-staked-icon'; import { useGlobalState } from '../../GlobalStateProvider'; import { nativeToFormat } from '../../helpers/parseNumbers'; import { useNodeInfoState } from '../../NodeInfoProvider'; @@ -31,7 +29,6 @@ function CollatorsTable() { const [selectedCandidate, setSelectedCandidate] = useState(undefined); const [userAvailableBalance, setUserAvailableBalance] = useState('0.00'); const [userStaking, setUserStaking] = useState(); - const [claimDialogOpen, setClaimDialogOpen] = useState(false); const [unbonding, setUnbonding] = useState(false); const [data, setData] = useState(); From 711d0cbd84da480224867257057219f07c70368e Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Thu, 1 Jun 2023 18:30:57 +0200 Subject: [PATCH 3/5] rename collatos to staking and Fix #179 --- src/components/Layout/links.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Layout/links.tsx b/src/components/Layout/links.tsx index e3981187..68844ad4 100644 --- a/src/components/Layout/links.tsx +++ b/src/components/Layout/links.tsx @@ -68,7 +68,7 @@ export const links: Links = ({ tenantName }) => [ }, { link: './collators', - title: 'Collators', + title: 'Staking', props: { className: ({ isActive } = {}) => (isActive ? 'active' : ''), }, From 91027d8539419979fc11b642391e32105ff48120 Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Fri, 14 Jul 2023 11:49:24 +0200 Subject: [PATCH 4/5] refresh estimated rewards after update --- src/hooks/staking/staking.tsx | 15 +++++++++------ src/pages/collators/CollatorRewards.tsx | 3 ++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/hooks/staking/staking.tsx b/src/hooks/staking/staking.tsx index dcc58c30..11dc3cf3 100644 --- a/src/hooks/staking/staking.tsx +++ b/src/hooks/staking/staking.tsx @@ -56,6 +56,12 @@ export function useStakingPallet() { const [estimatedRewards, setEstimatedRewards] = useState('0'); const [fees, setFees] = useState(defaultTransactionFees); + const fetchEstimatedReward = async () => { + if (!api || !walletAccount) return '0'; + const formattedAddr = ss58Format ? getAddressForFormat(walletAccount.address, ss58Format) : walletAccount.address; + return (await api.query.parachainStaking.rewards(formattedAddr)).toString(); + }; + useEffect(() => { if (!api) { return; @@ -84,12 +90,6 @@ export function useStakingPallet() { return inflationInfo.toHuman() as unknown as ParachainStakingInflationInflationInfo; }; - const fetchEstimatedReward = async () => { - if (!walletAccount) return '0'; - const formattedAddr = ss58Format ? getAddressForFormat(walletAccount.address, ss58Format) : walletAccount.address; - return (await api.query.parachainStaking.rewards(formattedAddr)).toString(); - }; - const fetchFees = async () => { const dummyAddress = '5D4tzEZy9XeNSwsAXgtZrRrs1bTfpPTWGqwb1PwCYjRTKYYS'; const sender = dummyAddress; @@ -128,6 +128,9 @@ export function useStakingPallet() { minDelegatorStake, estimatedRewards, fees, + refreshRewards() { + fetchEstimatedReward().then((reward) => setEstimatedRewards(reward)); + }, async getTransactionFee(extrinsic: SubmittableExtrinsic) { if (!api || !extrinsic.hasPaymentInfo) { return new Big(0); diff --git a/src/pages/collators/CollatorRewards.tsx b/src/pages/collators/CollatorRewards.tsx index 692da2a0..f6aba88e 100644 --- a/src/pages/collators/CollatorRewards.tsx +++ b/src/pages/collators/CollatorRewards.tsx @@ -20,7 +20,7 @@ function CollatorRewards() { const { api, tokenSymbol, ss58Format } = useNodeInfoState().state; const { walletAccount } = useGlobalState(); - const { candidates, estimatedRewards, createUpdateDelegatorRewardsExtrinsic } = useStakingPallet(); + const { candidates, estimatedRewards, refreshRewards, createUpdateDelegatorRewardsExtrinsic } = useStakingPallet(); const userAccountAddress = useMemo(() => { return walletAccount && ss58Format ? getAddressForFormat(walletAccount?.address, ss58Format) : ''; @@ -76,6 +76,7 @@ function CollatorRewards() { } } else if (status.isFinalized) { setSubmissionPending(false); + refreshRewards(); if (errors.length === 0) { toast('Delegator rewards updated', { type: 'success' }); } From c08991c6d8913de87a0b5f119fbd80b387948c3e Mon Sep 17 00:00:00 2001 From: Gonza Montiel Date: Fri, 14 Jul 2023 11:49:47 +0200 Subject: [PATCH 5/5] batch delegator and collator rewards increment --- src/hooks/staking/staking.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hooks/staking/staking.tsx b/src/hooks/staking/staking.tsx index 11dc3cf3..3579a40f 100644 --- a/src/hooks/staking/staking.tsx +++ b/src/hooks/staking/staking.tsx @@ -181,7 +181,11 @@ export function useStakingPallet() { if (!api) { return undefined; } - return api.tx.parachainStaking?.incrementDelegatorRewards(); + const txs = [ + api.tx.parachainStaking?.incrementDelegatorRewards(), + api.tx.parachainStaking?.incrementCollatorRewards(), + ]; + return api.tx.utility.batch(txs); }, }; }, [api, candidates, inflationInfo, fees, minDelegatorStake, estimatedRewards]);