From b6efa1a8641be708cca05b1f6bb2906f5dffbee2 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Mon, 18 Mar 2024 20:27:02 +0100 Subject: [PATCH 1/2] feat: Prepare account page to support 5 linked CRNs + staking halving --- .../common/AvailableCRNSpotChart/cmp.tsx | 5 +- .../common/EstimatedNodeRewardsChart/cmp.tsx | 4 +- src/components/common/NodeLinkedNodes/cmp.tsx | 6 +-- .../earn/CoreChannelNodeDetailPage/cmp.tsx | 8 ++- .../pages/earn/CoreChannelNodesPage/cmp.tsx | 6 ++- src/domain/node.ts | 6 ++- src/domain/stake.ts | 54 +++++++++++-------- 7 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/components/common/AvailableCRNSpotChart/cmp.tsx b/src/components/common/AvailableCRNSpotChart/cmp.tsx index 54e92f5..b570eef 100644 --- a/src/components/common/AvailableCRNSpotChart/cmp.tsx +++ b/src/components/common/AvailableCRNSpotChart/cmp.tsx @@ -1,6 +1,5 @@ import { memo, useMemo } from 'react' -import { CCN } from '@/domain/node' -import { StakeManager } from '@/domain/stake' +import { CCN, NodeManager } from '@/domain/node' import { Cell, Pie, PieChart } from 'recharts' import { useTheme } from 'styled-components' import Card1 from '../Card1' @@ -24,7 +23,7 @@ export const AvailableCRNSpotChart = ({ const [linkedSpots, freeSpots] = nodes.reduce( (ac, cv) => { const linked = cv.resource_nodes.length - const free = Math.max(3 - linked, 0) + const free = Math.max(NodeManager.maxLinkedPerNode - linked, 0) ac[0] += linked ac[1] += free return ac diff --git a/src/components/common/EstimatedNodeRewardsChart/cmp.tsx b/src/components/common/EstimatedNodeRewardsChart/cmp.tsx index d67d7e3..6659d04 100644 --- a/src/components/common/EstimatedNodeRewardsChart/cmp.tsx +++ b/src/components/common/EstimatedNodeRewardsChart/cmp.tsx @@ -4,7 +4,7 @@ import { StakeManager } from '@/domain/stake' import { Cell, Pie, PieChart } from 'recharts' import { useTheme } from 'styled-components' import Card1 from '../Card1' -import { ColorDot, Logo, TextGradient } from '@aleph-front/core' +import { ColorDot, TextGradient } from '@aleph-front/core' import { SVGGradients } from '../charts' import Price from '../Price' @@ -22,7 +22,7 @@ export const EstimatedNodeRewardsChart = ({ const data = useMemo(() => { const activeNodes = stakeManager.activeNodes(nodes || []) - const perDayRewards = 15000 / activeNodes.length + const perDayRewards = StakeManager.dailyRewardsPool / activeNodes.length const perMonthRewards = perDayRewards * 30 const total = perMonthRewards + perDayRewards diff --git a/src/components/common/NodeLinkedNodes/cmp.tsx b/src/components/common/NodeLinkedNodes/cmp.tsx index 354a980..2cc7694 100644 --- a/src/components/common/NodeLinkedNodes/cmp.tsx +++ b/src/components/common/NodeLinkedNodes/cmp.tsx @@ -1,21 +1,21 @@ import { HTMLAttributes, memo } from 'react' import { StyledDotIcon } from './styles' -import { CRN } from '@/domain/node' +import { CRN, NodeManager } from '@/domain/node' // https://github.com/aleph-im/aleph-account/blob/main/src/components/NodesTable.vue#L163 export type NodeLinkedNodesProps = HTMLAttributes & { nodes?: CRN[] subfix?: string - max?: number } export const NodeLinkedNodes = ({ nodes, subfix, - max = 3, ...rest }: NodeLinkedNodesProps) => { + const max = NodeManager.maxLinkedPerNode + return (
diff --git a/src/components/pages/earn/CoreChannelNodeDetailPage/cmp.tsx b/src/components/pages/earn/CoreChannelNodeDetailPage/cmp.tsx index 5d8d110..9ec3ce6 100644 --- a/src/components/pages/earn/CoreChannelNodeDetailPage/cmp.tsx +++ b/src/components/pages/earn/CoreChannelNodeDetailPage/cmp.tsx @@ -20,6 +20,7 @@ import NodeDetailLink from '@/components/common/NodeDetailLink' import { apiServer } from '@/helpers/constants' import Image from 'next/image' import Price from '@/components/common/Price' +import { NodeManager } from '@/domain/node' export const CoreChannelNodeDetailPage = () => { const { @@ -249,7 +250,12 @@ export const CoreChannelNodeDetailPage = () => {
{Array.from( - { length: Math.max(3, node?.crnsData.length || 0) }, + { + length: Math.max( + NodeManager.maxLinkedPerNode, + node?.crnsData.length || 0, + ), + }, (_, i) => { const crn = node?.crnsData[i] diff --git a/src/components/pages/earn/CoreChannelNodesPage/cmp.tsx b/src/components/pages/earn/CoreChannelNodesPage/cmp.tsx index 52ab4c9..ddfd997 100644 --- a/src/components/pages/earn/CoreChannelNodesPage/cmp.tsx +++ b/src/components/pages/earn/CoreChannelNodesPage/cmp.tsx @@ -14,6 +14,7 @@ import NetworkHealthChart from '@/components/common/NetworkHealthChart' import EstimatedNodeRewardsChart from '@/components/common/EstimatedNodeRewardsChart' import { useLazyRender } from '@/hooks/common/useLazyRender' import AvailableCRNSpotChart from '@/components/common/AvailableCRNSpotChart' +import { StakeManager } from '@/domain/stake' export const CoreChannelNodesPage = (props: UseCoreChannelNodesPageProps) => { const { @@ -45,7 +46,10 @@ export const CoreChannelNodesPage = (props: UseCoreChannelNodesPageProps) => { variant="secondary" size="md" tw="gap-2.5" - disabled={!account || (accountBalance || 0) <= 200_000} + disabled={ + !account || + (accountBalance || 0) <= StakeManager.minStakeToActivateNode + } > Create core node diff --git a/src/domain/node.ts b/src/domain/node.ts index 16db279..be010ed 100644 --- a/src/domain/node.ts +++ b/src/domain/node.ts @@ -25,6 +25,7 @@ import { } from '@/helpers/schemas' import { FileManager } from './file' import { subscribeSocketFeed } from '@/helpers/socket' +import { StakeManager } from './stake' const { post } = messages @@ -278,6 +279,7 @@ export class NodeManager { static updateCRNSchema = updateCRNSchema static maxStakedPerNode = 1_000_000 + static maxLinkedPerNode = 5 constructor( protected account?: Account, @@ -643,7 +645,7 @@ export class NodeManager { if (!!node.parent) return [false, `The node is already linked to ${node.parent} ccn`] - if (userNode.resource_nodes.length >= 3) + if (userNode.resource_nodes.length >= NodeManager.maxLinkedPerNode) return [ false, `The user node is already linked to ${userNode.resource_nodes.length} nodes`, @@ -660,7 +662,7 @@ export class NodeManager { return 'The linked CCN is underperforming' } else { if (node.score < 0.8) return 'The CCN is underperforming' - if ((node?.crnsData.length || 0) < 3) + if ((node?.crnsData.length || 0) < StakeManager.minLinkedNodesForPenalty) return 'The CCN has less than three linked CRNs' if (!staking && node?.crnsData.some((crn) => crn.score < 0.8)) return 'One of the linked CRN is underperforming' diff --git a/src/domain/stake.ts b/src/domain/stake.ts index eecde96..e8c8f88 100644 --- a/src/domain/stake.ts +++ b/src/domain/stake.ts @@ -23,6 +23,11 @@ export type RewardsResponse = { } export class StakeManager { + // @todo: Calculate halving automatically using dates + static dailyRewardsPool = 15_000 / 2 + static minStakeToActivateNode = 200_000 + static minLinkedNodesForPenalty = 3 + constructor( protected account?: Account, protected channel = defaultAccountChannel, @@ -161,7 +166,7 @@ export class StakeManager { } totalStakedByOperators(nodes: AlephNode[]): number { - return nodes.length * 200_000 + return nodes.length * StakeManager.minStakeToActivateNode } totalStakedInActive(nodes: CCN[]): number { @@ -170,9 +175,10 @@ export class StakeManager { totalPerDay(nodes: CCN[]): number { const activeNodes = this.activeNodes(nodes).length - if (!activeNodes) return activeNodes + if (!activeNodes) return 0 - return 15000 * ((Math.log10(activeNodes) + 1) / 3) + // @note: https://medium.com/aleph-im/aleph-im-staking-go-live-part-2-stakers-tokenomics-663164b5ec78 + return StakeManager.dailyRewardsPool * ((Math.log10(activeNodes) + 1) / 3) } totalPerAlephPerDay(nodes: CCN[]): number { @@ -190,15 +196,10 @@ export class StakeManager { let estAPY = 0 if (node.score) { - const linkedCRN = Math.min( - node.crnsData.filter((x) => x.score >= 0.2).length, - 3, - ) - const normalizedScore = normalizeValue(node.score, 0.2, 0.8, 0, 1) - const linkedCRNPenalty = (3 - linkedCRN) / 10 + const linkedCRNPenalty = this.totalLinkedCRNPenaltyFactor(node) - estAPY = this.currentAPY(nodes) * normalizedScore * (1 - linkedCRNPenalty) + estAPY = this.currentAPY(nodes) * normalizedScore * linkedCRNPenalty } return estAPY @@ -208,20 +209,31 @@ export class StakeManager { return stake * this.totalPerAlephPerDay(nodes) } - CCNRewardsPerDay(node: CCN, nodes: CCN[]): number { - let estRewards = 0 + totalLinkedCRNPenaltyFactor(node: CCN): number { + /** @note: + * 3 to 5 linked > 100% + * 2 linked > 90% + * 1 linked > 80% + * 0 linked > 70% + **/ - if (node.score) { - const linkedCRN = Math.min(node.crnsData.length, 3) - const activeNodes = this.activeNodes(nodes).length - const pool = 15_000 / activeNodes - const normalizedScore = normalizeValue(node.score, 0.2, 0.8, 0, 1) - const linkedCRNPenalty = (3 - linkedCRN) / 10 + const linkedCRN = Math.min( + node.crnsData.filter((x) => x.score >= 0.2).length, + StakeManager.minLinkedNodesForPenalty, + ) - estRewards = pool * normalizedScore * (1 - linkedCRNPenalty) - } + return 1 - (StakeManager.minLinkedNodesForPenalty - linkedCRN) / 10 + } + + CCNRewardsPerDay(node: CCN, nodes: CCN[]): number { + if (!node.score) return 0 + + const activeNodes = this.activeNodes(nodes).length + const nodePool = StakeManager.dailyRewardsPool / activeNodes + const normalizedScore = normalizeValue(node.score, 0.2, 0.8, 0, 1) + const linkedCRNPenalty = this.totalLinkedCRNPenaltyFactor(node) - return estRewards + return nodePool * normalizedScore * linkedCRNPenalty } CRNRewardsPerDay(node: CRN): number { From 5eab0d430317c2973f96b4395180bfdb04cf4369 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 21 Mar 2024 10:26:41 +0100 Subject: [PATCH 2/2] fix: apply halving on CRNs --- src/components/common/CRNRewardsCell/cmp.tsx | 3 ++- .../common/EstimatedNodeRewardsChart/cmp.tsx | 11 ++++++++-- src/domain/stake.ts | 21 +++++++++++++------ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/components/common/CRNRewardsCell/cmp.tsx b/src/components/common/CRNRewardsCell/cmp.tsx index 1be8e1a..aa68b51 100644 --- a/src/components/common/CRNRewardsCell/cmp.tsx +++ b/src/components/common/CRNRewardsCell/cmp.tsx @@ -8,7 +8,7 @@ export const CRNRewardsCell = ({ node }: { node: CRN }) => { // @todo: Refactor this (use singleton) const rewardManager = new StakeManager() - const rewards = rewardManager.CRNRewardsPerDay(node) + const rewards = rewardManager.CRNRewardsPerDay(node) * (365 / 12) const isNotFullyLinked = useMemo(() => !node.parent, [node]) return ( @@ -18,6 +18,7 @@ export const CRNRewardsCell = ({ node }: { node: CRN }) => { ) : (
~ + /M
)} diff --git a/src/components/common/EstimatedNodeRewardsChart/cmp.tsx b/src/components/common/EstimatedNodeRewardsChart/cmp.tsx index 6659d04..123fae5 100644 --- a/src/components/common/EstimatedNodeRewardsChart/cmp.tsx +++ b/src/components/common/EstimatedNodeRewardsChart/cmp.tsx @@ -20,9 +20,16 @@ export const EstimatedNodeRewardsChart = ({ const theme = useTheme() const data = useMemo(() => { - const activeNodes = stakeManager.activeNodes(nodes || []) + let perDayRewards = 0 + + if (nodes) { + const activeNodes = stakeManager.activeNodes(nodes) + const totalPerDay = stakeManager.totalPerDay(nodes) + // const totalPerDay = StakeManager.dailyCCNRewardsPool / activeNodes.length + + perDayRewards = totalPerDay / activeNodes.length + } - const perDayRewards = StakeManager.dailyRewardsPool / activeNodes.length const perMonthRewards = perDayRewards * 30 const total = perMonthRewards + perDayRewards diff --git a/src/domain/stake.ts b/src/domain/stake.ts index e8c8f88..c291709 100644 --- a/src/domain/stake.ts +++ b/src/domain/stake.ts @@ -23,8 +23,10 @@ export type RewardsResponse = { } export class StakeManager { - // @todo: Calculate halving automatically using dates - static dailyRewardsPool = 15_000 / 2 + static dailyCCNRewardsPool = 15_000 + static dailyCRNRewardsBase = 250 / (365 / 12) + static dailyCRNRewardsVariable = 1250 / (365 / 12) + static minStakeToActivateNode = 200_000 static minLinkedNodesForPenalty = 3 @@ -178,7 +180,9 @@ export class StakeManager { if (!activeNodes) return 0 // @note: https://medium.com/aleph-im/aleph-im-staking-go-live-part-2-stakers-tokenomics-663164b5ec78 - return StakeManager.dailyRewardsPool * ((Math.log10(activeNodes) + 1) / 3) + return ( + StakeManager.dailyCCNRewardsPool * ((Math.log10(activeNodes) + 1) / 3) + ) } totalPerAlephPerDay(nodes: CCN[]): number { @@ -229,7 +233,7 @@ export class StakeManager { if (!node.score) return 0 const activeNodes = this.activeNodes(nodes).length - const nodePool = StakeManager.dailyRewardsPool / activeNodes + const nodePool = StakeManager.dailyCCNRewardsPool / activeNodes const normalizedScore = normalizeValue(node.score, 0.2, 0.8, 0, 1) const linkedCRNPenalty = this.totalLinkedCRNPenaltyFactor(node) @@ -241,8 +245,13 @@ export class StakeManager { if (!node.score || !node.decentralization) return 0 const { decentralization, score } = node - const maxRewards = 500 + decentralization * 2500 - return maxRewards * normalizeValue(score, 0.2, 0.8, 0, 1) + const maxRewards = + StakeManager.dailyCRNRewardsBase + + StakeManager.dailyCRNRewardsVariable * decentralization + + const normalizedScore = normalizeValue(score, 0.2, 0.8, 0, 1) + + return maxRewards * normalizedScore } }