Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Prepare account page to support 5 linked CRNs + staking halving #2

Merged
merged 2 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/components/common/AvailableCRNSpotChart/cmp.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/components/common/CRNRewardsCell/cmp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -18,6 +18,7 @@ export const CRNRewardsCell = ({ node }: { node: CRN }) => {
) : (
<div tw="inline-flex gap-2 items-center">
~ <Price value={rewards} />
/M
</div>
)}
</>
Expand Down
13 changes: 10 additions & 3 deletions src/components/common/EstimatedNodeRewardsChart/cmp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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 = 15000 / activeNodes.length
const perMonthRewards = perDayRewards * 30
const total = perMonthRewards + perDayRewards

Expand Down
6 changes: 3 additions & 3 deletions src/components/common/NodeLinkedNodes/cmp.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> & {
nodes?: CRN[]
subfix?: string
max?: number
}

export const NodeLinkedNodes = ({
nodes,
subfix,
max = 3,
...rest
}: NodeLinkedNodesProps) => {
const max = NodeManager.maxLinkedPerNode

return (
<div tw="inline-flex items-center gap-3" {...rest}>
<div tw="flex items-stretch gap-0.5">
Expand Down
8 changes: 7 additions & 1 deletion src/components/pages/earn/CoreChannelNodeDetailPage/cmp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -249,7 +250,12 @@ export const CoreChannelNodeDetailPage = () => {
<div tw="flex-1 w-1/3 min-w-[20rem] flex flex-col gap-9">
<Card2 title="LINKED RESOURCES">
{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]

Expand Down
6 changes: 5 additions & 1 deletion src/components/pages/earn/CoreChannelNodesPage/cmp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
>
<Icon name="key" />
Create core node
Expand Down
6 changes: 4 additions & 2 deletions src/domain/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '@/helpers/schemas'
import { FileManager } from './file'
import { subscribeSocketFeed } from '@/helpers/socket'
import { StakeManager } from './stake'

const { post } = messages

Expand Down Expand Up @@ -278,6 +279,7 @@ export class NodeManager {
static updateCRNSchema = updateCRNSchema

static maxStakedPerNode = 1_000_000
static maxLinkedPerNode = 5

constructor(
protected account?: Account,
Expand Down Expand Up @@ -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`,
Expand All @@ -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'
Expand Down
67 changes: 44 additions & 23 deletions src/domain/stake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export type RewardsResponse = {
}

export class StakeManager {
static dailyCCNRewardsPool = 15_000
static dailyCRNRewardsBase = 250 / (365 / 12)
static dailyCRNRewardsVariable = 1250 / (365 / 12)

static minStakeToActivateNode = 200_000
static minLinkedNodesForPenalty = 3

constructor(
protected account?: Account,
protected channel = defaultAccountChannel,
Expand Down Expand Up @@ -161,7 +168,7 @@ export class StakeManager {
}

totalStakedByOperators(nodes: AlephNode[]): number {
return nodes.length * 200_000
return nodes.length * StakeManager.minStakeToActivateNode
}

totalStakedInActive(nodes: CCN[]): number {
Expand All @@ -170,9 +177,12 @@ 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.dailyCCNRewardsPool * ((Math.log10(activeNodes) + 1) / 3)
)
}

totalPerAlephPerDay(nodes: CCN[]): number {
Expand All @@ -190,15 +200,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
Expand All @@ -208,29 +213,45 @@ 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%
**/

const linkedCRN = Math.min(
node.crnsData.filter((x) => x.score >= 0.2).length,
StakeManager.minLinkedNodesForPenalty,
)

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
return 1 - (StakeManager.minLinkedNodesForPenalty - linkedCRN) / 10
}

estRewards = pool * normalizedScore * (1 - linkedCRNPenalty)
}
CCNRewardsPerDay(node: CCN, nodes: CCN[]): number {
if (!node.score) return 0

return estRewards
const activeNodes = this.activeNodes(nodes).length
const nodePool = StakeManager.dailyCCNRewardsPool / activeNodes
const normalizedScore = normalizeValue(node.score, 0.2, 0.8, 0, 1)
const linkedCRNPenalty = this.totalLinkedCRNPenaltyFactor(node)

return nodePool * normalizedScore * linkedCRNPenalty
}

CRNRewardsPerDay(node: CRN): number {
if (!node.parent) return 0
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
}
}
Loading