From a7e1faa92cea3de44d58008f703484a5b4f4e735 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 12 Oct 2023 19:42:23 +0200 Subject: [PATCH] Staking WIP --- .github/workflows/push_on_ipfs.yaml | 4 +- README.md | 4 +- package-lock.json | 6 +- package.json | 4 +- src/components/common/APYCell/cmp.tsx | 56 +++++ src/components/common/APYCell/index.ts | 1 + src/components/common/APYCell/styles.tsx | 20 ++ src/components/common/AutoBreadcrumb/cmp.tsx | 4 +- src/components/common/Header/cmp.tsx | 139 ++++++------ src/components/common/Header/styles.tsx | 11 +- src/components/common/LinkedCell/cmp.tsx | 23 ++ src/components/common/LinkedCell/index.ts | 1 + src/components/common/LinkedCell/styles.tsx | 15 ++ src/components/common/Main/styles.tsx | 2 +- src/components/common/NameCell/cmp.tsx | 54 +++++ src/components/common/NameCell/index.ts | 1 + src/components/common/NodesTable/cmp.tsx | 3 + src/components/common/NodesTable/index.ts | 1 + src/components/common/NodesTable/styles.tsx | 20 ++ src/components/common/ScoreCell/cmp.tsx | 17 ++ src/components/common/ScoreCell/index.ts | 1 + src/components/common/ScoreCell/styles.tsx | 20 ++ src/components/common/Sidebar/cmp.tsx | 138 ++++++++++++ src/components/common/Sidebar/index.ts | 1 + src/components/common/Sidebar/styles.tsx | 41 ++++ src/components/common/StakedCell/cmp.tsx | 31 +++ src/components/common/StakedCell/index.ts | 1 + src/components/common/StakedCell/styles.tsx | 23 ++ src/components/common/Viewport/cmp.tsx | 3 + src/components/common/Viewport/index.ts | 1 + src/components/common/Viewport/styles.tsx | 7 + src/components/pages/HomePage/cmp.tsx | 10 +- .../earn/ComputeResourceNodesPage/cmp.tsx | 14 ++ .../earn/ComputeResourceNodesPage/index.ts | 1 + .../pages/earn/CoreChannelNodesPage/cmp.tsx | 14 ++ .../pages/earn/CoreChannelNodesPage/index.ts | 1 + .../pages/earn/EarnHomePage/cmp.tsx | 6 + .../pages/earn/EarnHomePage/index.ts | 1 + src/components/pages/earn/StakingPage/cmp.tsx | 137 +++++++++++- src/domain/message.ts | 7 +- src/domain/node.ts | 166 ++++++++++++++ src/domain/reward.ts | 66 ++++++ src/helpers/constants.ts | 3 + src/helpers/utils.ts | 209 ++++++++++++++++++ src/hooks/common/useRedirect.ts | 10 + src/pages/_app.tsx | 13 +- src/pages/earn/ccn.tsx | 3 + src/pages/earn/crn.tsx | 3 + src/pages/earn/index.ts | 3 + src/styles/global.tsx | 13 +- 50 files changed, 1219 insertions(+), 114 deletions(-) create mode 100644 src/components/common/APYCell/cmp.tsx create mode 100644 src/components/common/APYCell/index.ts create mode 100644 src/components/common/APYCell/styles.tsx create mode 100644 src/components/common/LinkedCell/cmp.tsx create mode 100644 src/components/common/LinkedCell/index.ts create mode 100644 src/components/common/LinkedCell/styles.tsx create mode 100644 src/components/common/NameCell/cmp.tsx create mode 100644 src/components/common/NameCell/index.ts create mode 100644 src/components/common/NodesTable/cmp.tsx create mode 100644 src/components/common/NodesTable/index.ts create mode 100644 src/components/common/NodesTable/styles.tsx create mode 100644 src/components/common/ScoreCell/cmp.tsx create mode 100644 src/components/common/ScoreCell/index.ts create mode 100644 src/components/common/ScoreCell/styles.tsx create mode 100644 src/components/common/Sidebar/cmp.tsx create mode 100644 src/components/common/Sidebar/index.ts create mode 100644 src/components/common/Sidebar/styles.tsx create mode 100644 src/components/common/StakedCell/cmp.tsx create mode 100644 src/components/common/StakedCell/index.ts create mode 100644 src/components/common/StakedCell/styles.tsx create mode 100644 src/components/common/Viewport/cmp.tsx create mode 100644 src/components/common/Viewport/index.ts create mode 100644 src/components/common/Viewport/styles.tsx create mode 100644 src/components/pages/earn/ComputeResourceNodesPage/cmp.tsx create mode 100644 src/components/pages/earn/ComputeResourceNodesPage/index.ts create mode 100644 src/components/pages/earn/CoreChannelNodesPage/cmp.tsx create mode 100644 src/components/pages/earn/CoreChannelNodesPage/index.ts create mode 100644 src/components/pages/earn/EarnHomePage/cmp.tsx create mode 100644 src/components/pages/earn/EarnHomePage/index.ts create mode 100644 src/domain/node.ts create mode 100644 src/domain/reward.ts create mode 100644 src/hooks/common/useRedirect.ts create mode 100644 src/pages/earn/ccn.tsx create mode 100644 src/pages/earn/crn.tsx create mode 100644 src/pages/earn/index.ts diff --git a/.github/workflows/push_on_ipfs.yaml b/.github/workflows/push_on_ipfs.yaml index e658a40..091d1b5 100644 --- a/.github/workflows/push_on_ipfs.yaml +++ b/.github/workflows/push_on_ipfs.yaml @@ -30,12 +30,12 @@ jobs: - uses: actions/upload-artifact@v3 with: - name: aleph-cloudsolutions + name: aleph-account-page path: out/ - uses: actions/download-artifact@v3 with: - name: aleph-cloudsolutions + name: aleph-account-page path: out/ - name: Push on IPFS diff --git a/README.md b/README.md index 3a6b21b..9821655 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Aleph Cloud Solutions +# Aleph Account page -Aleph Cloud Solutions is a [next.js](https://nextjs.org/) frontend dApp that allows you to easily deploy VMs on the aleph network, without worrying about the different configuration options; just launch the app, upload your code and dependencies and enjoy. +Aleph Account page is a [next.js](https://nextjs.org/) frontend dApp that allows you to easily deploy VMs on the aleph network, without worrying about the different configuration options; just launch the app, upload your code and dependencies and enjoy. ## Develop locally diff --git a/package-lock.json b/package-lock.json index 43edc10..a281226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "front-aleph-cloud", + "name": "front-aleph-account-page", "version": "0.2.11", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "front-aleph-cloud", + "name": "front-aleph-account-page", "version": "0.2.11", "dependencies": { "@aleph-front/aleph-core": "^1.5.4", @@ -23840,4 +23840,4 @@ "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index f3a9683..034ad37 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "front-aleph-cloud", + "name": "front-aleph-account-page", "version": "0.2.11", "private": true, "scripts": { @@ -57,4 +57,4 @@ "preset": "styled-components" } } -} +} \ No newline at end of file diff --git a/src/components/common/APYCell/cmp.tsx b/src/components/common/APYCell/cmp.tsx new file mode 100644 index 0000000..e4dc6a1 --- /dev/null +++ b/src/components/common/APYCell/cmp.tsx @@ -0,0 +1,56 @@ +import { memo, useMemo } from 'react' +import { StyledAPYIcon } from './styles' +import { CCN } from '@/domain/node' +import { RewardManager } from '@/domain/reward' +import { Tooltip } from '@aleph-front/aleph-core' + +// https://github.com/aleph-im/aleph-account/blob/main/src/components/NodesTable.vue#L586 +export const APYCell = memo(({ node, nodes }: { node: CCN; nodes: CCN[] }) => { + const apyManager = new RewardManager({} as any) + + const nodeAPY = apyManager.computeEstimatedStakersAPY(node, nodes) + const currentAPY = apyManager.currentAPY(nodes) + const performance = nodeAPY / currentAPY + + const isNotFullyLinked = useMemo( + () => node.crnsData.length < 3 || node.crnsData.find((f) => f.score < 0.2), + [node], + ) + + const data = ( +
+ + {Number(nodeAPY * 100).toFixed(2)}% +
+ ) + + return ( + <> + {isNotFullyLinked ? ( + +
+
{3 - node.crnsData.length} missing CRN
+
+ Link 3 functioning CRN to that Node to maximise its rewards +
+
+
Performance: {Number(performance * 100).toFixed(2)}%
+ + } + header="Staking performance" + > + {data} +
+ ) : ( + data + )} + + ) +}) +APYCell.displayName = 'APYCell' + +export default APYCell diff --git a/src/components/common/APYCell/index.ts b/src/components/common/APYCell/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/common/APYCell/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/common/APYCell/styles.tsx b/src/components/common/APYCell/styles.tsx new file mode 100644 index 0000000..2638f95 --- /dev/null +++ b/src/components/common/APYCell/styles.tsx @@ -0,0 +1,20 @@ +import tw from 'twin.macro' +import styled, { css } from 'styled-components' + +export const StyledAPYIcon = styled.div<{ $performance: number }>( + ({ theme, $performance }) => { + const color = + $performance >= 0.8 + ? theme.color.main1 + : $performance >= 0.5 + ? theme.color.main0 + : theme.color.error + + return [ + tw`h-4 w-4 rounded-full`, + css` + background-color: ${color}; + `, + ] + }, +) diff --git a/src/components/common/AutoBreadcrumb/cmp.tsx b/src/components/common/AutoBreadcrumb/cmp.tsx index 4068e9e..26fab7c 100644 --- a/src/components/common/AutoBreadcrumb/cmp.tsx +++ b/src/components/common/AutoBreadcrumb/cmp.tsx @@ -60,7 +60,5 @@ export default function AutoBreadcrumb({ return links }, [router.pathname, router.asPath, nameProp, names, isHome, includeHome]) - return isHome ? null : ( - - ) + return isHome ? null : } diff --git a/src/components/common/Header/cmp.tsx b/src/components/common/Header/cmp.tsx index 52c1ebd..7040681 100644 --- a/src/components/common/Header/cmp.tsx +++ b/src/components/common/Header/cmp.tsx @@ -26,80 +26,75 @@ export const Header = () => { return ( - {hasBreadcrumb && } - - - - - - - -
- - {account ? ( - - ) : ( -
+
+ + + + <> + {account ? ( + + ) : ( + + )} +
+ {displayWalletPicker && ( + - Connect{' '} - - + /> )} -
- {displayWalletPicker && ( - - )} -
- -
- +
+ +
) } diff --git a/src/components/common/Header/styles.tsx b/src/components/common/Header/styles.tsx index 62185df..c03da27 100644 --- a/src/components/common/Header/styles.tsx +++ b/src/components/common/Header/styles.tsx @@ -2,15 +2,12 @@ import { Button, Navbar } from '@aleph-front/aleph-core' import styled from 'styled-components' export const StyledHeader = styled.header` - font-size: inherit; - line-height: inherit; - box-sizing: border-box; + height: 104px; width: 100%; - margin: 0; position: sticky; - top: 0; - z-index: 10; - /* background-color: #141327CC; */ + display: flex; + align-items: center; + justify-content: space-between; ` export const StyledNavbar = styled(Navbar)` diff --git a/src/components/common/LinkedCell/cmp.tsx b/src/components/common/LinkedCell/cmp.tsx new file mode 100644 index 0000000..3c986a5 --- /dev/null +++ b/src/components/common/LinkedCell/cmp.tsx @@ -0,0 +1,23 @@ +import { memo } from 'react' +import { StyledLinkIcon } from './styles' + +// https://github.com/aleph-im/aleph-account/blob/main/src/components/NodesTable.vue#L163 +export const LinkedCell = memo( + ({ nodes, max = 3 }: { nodes: string[]; max?: number }) => { + return ( +
+
+ {Array.from({ length: max }).map((_, i) => ( + + ))} +
+
+ {nodes.length} of {max} +
+
+ ) + }, +) +LinkedCell.displayName = 'LinkedCell' + +export default LinkedCell diff --git a/src/components/common/LinkedCell/index.ts b/src/components/common/LinkedCell/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/common/LinkedCell/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/common/LinkedCell/styles.tsx b/src/components/common/LinkedCell/styles.tsx new file mode 100644 index 0000000..9bf6124 --- /dev/null +++ b/src/components/common/LinkedCell/styles.tsx @@ -0,0 +1,15 @@ +import tw from 'twin.macro' +import styled, { css } from 'styled-components' + +export const StyledLinkIcon = styled.div<{ $active: boolean }>( + ({ theme, $active }) => { + const color = $active ? theme.color.main1 : `${theme.color.base0}20` + + return [ + tw`h-3 w-2`, + css` + background-color: ${color}; + `, + ] + }, +) diff --git a/src/components/common/Main/styles.tsx b/src/components/common/Main/styles.tsx index 355e94e..06745a2 100644 --- a/src/components/common/Main/styles.tsx +++ b/src/components/common/Main/styles.tsx @@ -2,5 +2,5 @@ import styled from 'styled-components' import tw from 'twin.macro' export const StyledMain = styled.main` - ${tw`flex flex-col flex-1`} + ${tw`flex flex-col flex-1 px-16`} ` diff --git a/src/components/common/NameCell/cmp.tsx b/src/components/common/NameCell/cmp.tsx new file mode 100644 index 0000000..35272a1 --- /dev/null +++ b/src/components/common/NameCell/cmp.tsx @@ -0,0 +1,54 @@ +import { memo } from 'react' +import Image from 'next/image' +import { Icon, Tooltip } from '@aleph-front/aleph-core' +import { apiServer } from '@/helpers/constants' + +// https://github.com/aleph-im/aleph-account/blob/main/src/components/NodesTable.vue#L117C113-L117C139 +export const NameCell = memo( + ({ + hash, + name, + picture, + locked, + }: { + hash: string + name: string + picture?: string + locked?: boolean + }) => { + return ( +
+ {picture ? ( + Node profile image + ) : ( + + )} +
+
CCN-ID {hash.slice(-10)}
+
+ {locked && ( + + + + )} + {name.substring(0, 30)} +
+
+
+ ) + }, +) +NameCell.displayName = 'NameCell' + +export default NameCell diff --git a/src/components/common/NameCell/index.ts b/src/components/common/NameCell/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/common/NameCell/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/common/NodesTable/cmp.tsx b/src/components/common/NodesTable/cmp.tsx new file mode 100644 index 0000000..0da3e0f --- /dev/null +++ b/src/components/common/NodesTable/cmp.tsx @@ -0,0 +1,3 @@ +import { StyledTable } from './styles' + +export default StyledTable diff --git a/src/components/common/NodesTable/index.ts b/src/components/common/NodesTable/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/common/NodesTable/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/common/NodesTable/styles.tsx b/src/components/common/NodesTable/styles.tsx new file mode 100644 index 0000000..6f007d1 --- /dev/null +++ b/src/components/common/NodesTable/styles.tsx @@ -0,0 +1,20 @@ +import { CCN } from '@/domain/node' +import { Table } from '@aleph-front/aleph-core' +import styled from 'styled-components' + +export const StyledTable = styled(Table)` + thead th { + font-size: 0.8125rem; + } + + td, + th { + padding: 0.75rem 1rem; + width: 0; + } + + tr, + td { + border: none; + } +` diff --git a/src/components/common/ScoreCell/cmp.tsx b/src/components/common/ScoreCell/cmp.tsx new file mode 100644 index 0000000..8e0c74a --- /dev/null +++ b/src/components/common/ScoreCell/cmp.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react' +import { StyledScoreIcon } from './styles' + +// https://github.com/aleph-im/aleph-account/blob/main/src/components/NodesTable.vue#L586 +export const ScoreCell = memo(({ score }: { score: number }) => { + const num = Number(score * 100).toFixed(2) + + return ( +
+ + {num}% +
+ ) +}) +ScoreCell.displayName = 'ScoreCell' + +export default ScoreCell diff --git a/src/components/common/ScoreCell/index.ts b/src/components/common/ScoreCell/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/common/ScoreCell/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/common/ScoreCell/styles.tsx b/src/components/common/ScoreCell/styles.tsx new file mode 100644 index 0000000..d4bf4ce --- /dev/null +++ b/src/components/common/ScoreCell/styles.tsx @@ -0,0 +1,20 @@ +import tw from 'twin.macro' +import styled, { css } from 'styled-components' + +export const StyledScoreIcon = styled.div<{ $score: number }>( + ({ theme, $score }) => { + const color = + $score >= 0.8 + ? theme.color.main1 + : $score >= 0.5 + ? theme.color.main0 + : theme.color.error + + return [ + tw`h-4 w-4 rounded-full`, + css` + background-color: ${color}; + `, + ] + }, +) diff --git a/src/components/common/Sidebar/cmp.tsx b/src/components/common/Sidebar/cmp.tsx new file mode 100644 index 0000000..4263a92 --- /dev/null +++ b/src/components/common/Sidebar/cmp.tsx @@ -0,0 +1,138 @@ +import { IconName } from '@fortawesome/fontawesome-svg-core' +import { Icon, Logo } from '@aleph-front/aleph-core' +import { StyledLink, StyledNav1, StyledNav2, StyledSidebar } from './styles' +import { + AnchorHTMLAttributes, + ReactNode, + useCallback, + useMemo, + useState, +} from 'react' +import { useRouter } from 'next/router' + +export type SidebarLinkProps = AnchorHTMLAttributes & { + href: string + icon?: IconName + children?: ReactNode + isActive?: boolean +} + +export const SidebarLink = ({ + href, + icon, + isActive, + children, +}: SidebarLinkProps) => { + const router = useRouter() + isActive = isActive || router.pathname.indexOf(href) >= 0 + + return ( + + {icon && } + {children} + + ) +} + +export type Route = { + name?: string + path: string + icon?: IconName + children?: Route[] +} + +const routes: Route[] = [ + { + name: 'EARN', + path: '/earn', + icon: 'circle-nodes', + children: [ + { + name: 'Staking', + path: '/earn/staking', + }, + { + name: 'Core nodes', + path: '/earn/ccn', + }, + { + name: 'Compute nodes', + path: '/earn/crn', + }, + ], + }, + { + name: 'PROFILE', + path: '/profile', + icon: 'user', + children: [ + { + name: 'My profile', + path: '/profile', + }, + { + name: 'Notification center', + path: '/notification', + }, + ], + }, +] + +export const Sidebar = () => { + const [open, setOpen] = useState(true) + + const handleToggle = useCallback(() => { + setOpen((open) => !open) + }, [setOpen]) + + const router = useRouter() + + const currentRoute = useMemo( + () => routes.find((route) => router.pathname.indexOf(route.path) === 0), + [router], + ) + + return ( + + + + + {routes.map((child) => ( + + ))} + + + + + + {currentRoute?.children && ( + <> +
+ {currentRoute?.name} +
+ {currentRoute?.children.map((child) => ( + + {child.name} + + ))} + + )} +
+
+ ) +} + +export default Sidebar diff --git a/src/components/common/Sidebar/index.ts b/src/components/common/Sidebar/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/common/Sidebar/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/common/Sidebar/styles.tsx b/src/components/common/Sidebar/styles.tsx new file mode 100644 index 0000000..c20884c --- /dev/null +++ b/src/components/common/Sidebar/styles.tsx @@ -0,0 +1,41 @@ +import { addClasses } from '@aleph-front/aleph-core' +import Link from 'next/link' +import styled from 'styled-components' +import tw from 'twin.macro' + +export const StyledSidebar = styled.aside` + display: flex; + align-items: stretch; +` + +export const StyledNav1 = styled.nav` + background-color: #0000004c; + + height: 100%; + width: 4.5rem; + + display: flex; + flex-direction: column; + align-items: center; +` + +export const StyledNav2 = styled.nav<{ $open?: boolean }>` + background-color: #00000020; + height: 100%; + width: ${({ $open }) => ($open ? '18rem' : '0')}; + transition: all ease-in-out 0.5s 0s; + padding-top: 7rem; + overflow: hidden; +` + +export const StyledLink = styled(Link).attrs(addClasses('tp-nav'))<{ + $isActive?: boolean + $hasText?: boolean +}>` + ${tw`flex items-center gap-1.5 py-2 px-6 w-full whitespace-nowrap`} + color: ${({ theme, $isActive, $hasText }) => + $isActive || !$hasText ? theme.color.main0 : theme.color.base0}; + opacity: ${({ $hasText, $isActive }) => + !$isActive && !$hasText ? '0.4' : '1'}; + justify-content: ${({ $hasText }) => (!$hasText ? 'center' : 'flex-start')}; +` diff --git a/src/components/common/StakedCell/cmp.tsx b/src/components/common/StakedCell/cmp.tsx new file mode 100644 index 0000000..5d84369 --- /dev/null +++ b/src/components/common/StakedCell/cmp.tsx @@ -0,0 +1,31 @@ +import { memo } from 'react' +import { StyledProgressBar } from './styles' + +// https://github.com/aleph-im/aleph-account/blob/main/src/components/NodesTable.vue#L137 +export const StakedCell = memo( + ({ staked, status }: { staked: number; status: string }) => { + const minStaked = 500_000 + const percent = Math.min(staked, minStaked) / minStaked + const amount = Number(staked / 1_000).toFixed(0) + + return ( +
+
+
+ {amount}k of 500k +
+
{status.toUpperCase()}
+
+
+ +
+
+ ) + }, +) +StakedCell.displayName = 'StakedCell' + +export default StakedCell diff --git a/src/components/common/StakedCell/index.ts b/src/components/common/StakedCell/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/common/StakedCell/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/common/StakedCell/styles.tsx b/src/components/common/StakedCell/styles.tsx new file mode 100644 index 0000000..207cc92 --- /dev/null +++ b/src/components/common/StakedCell/styles.tsx @@ -0,0 +1,23 @@ +import tw from 'twin.macro' +import styled, { css } from 'styled-components' + +export const StyledProgressBar = styled.div<{ $percent: number }>( + ({ theme, $percent }) => { + const color = $percent >= 1 ? theme.color.main1 : theme.color.main0 + const bgColor = `${theme.color.base0}20` + + return [ + tw`relative h-0.5 w-full`, + css` + background-color: ${bgColor}; + + &:after { + ${tw`absolute top-0 left-0 h-full`} + content: ''; + background-color: ${color}; + width: ${$percent * 100}%; + } + `, + ] + }, +) diff --git a/src/components/common/Viewport/cmp.tsx b/src/components/common/Viewport/cmp.tsx new file mode 100644 index 0000000..67fe30a --- /dev/null +++ b/src/components/common/Viewport/cmp.tsx @@ -0,0 +1,3 @@ +import { StyledViewport } from './styles' + +export default StyledViewport diff --git a/src/components/common/Viewport/index.ts b/src/components/common/Viewport/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/common/Viewport/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/common/Viewport/styles.tsx b/src/components/common/Viewport/styles.tsx new file mode 100644 index 0000000..a042dc6 --- /dev/null +++ b/src/components/common/Viewport/styles.tsx @@ -0,0 +1,7 @@ +import styled from 'styled-components' + +export const StyledViewport = styled.div` + flex: 1 0 100%; + height: 100%; + display: flex; +` diff --git a/src/components/pages/HomePage/cmp.tsx b/src/components/pages/HomePage/cmp.tsx index b977170..8fc91e3 100644 --- a/src/components/pages/HomePage/cmp.tsx +++ b/src/components/pages/HomePage/cmp.tsx @@ -1,12 +1,6 @@ -import { useRouter } from 'next/router' -import { useEffect } from 'react' +import { useRedirect } from '@/hooks/common/useRedirect' export default function HomePage() { - const router = useRouter() - - useEffect(() => { - router.replace('/earn/staking') - }) - + useRedirect('/earn/staking') return <>Loading... } diff --git a/src/components/pages/earn/ComputeResourceNodesPage/cmp.tsx b/src/components/pages/earn/ComputeResourceNodesPage/cmp.tsx new file mode 100644 index 0000000..f189158 --- /dev/null +++ b/src/components/pages/earn/ComputeResourceNodesPage/cmp.tsx @@ -0,0 +1,14 @@ +import Head from 'next/head' + +export default function ComputeResourceNodesPage() { + return ( + <> + + Aleph.im | Account + + + +
COMPUTE NODES
+ + ) +} diff --git a/src/components/pages/earn/ComputeResourceNodesPage/index.ts b/src/components/pages/earn/ComputeResourceNodesPage/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/pages/earn/ComputeResourceNodesPage/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/pages/earn/CoreChannelNodesPage/cmp.tsx b/src/components/pages/earn/CoreChannelNodesPage/cmp.tsx new file mode 100644 index 0000000..0a206a1 --- /dev/null +++ b/src/components/pages/earn/CoreChannelNodesPage/cmp.tsx @@ -0,0 +1,14 @@ +import Head from 'next/head' + +export default function CoreChannelNodesPage() { + return ( + <> + + Aleph.im | Account + + + +
CORE NODES
+ + ) +} diff --git a/src/components/pages/earn/CoreChannelNodesPage/index.ts b/src/components/pages/earn/CoreChannelNodesPage/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/pages/earn/CoreChannelNodesPage/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/pages/earn/EarnHomePage/cmp.tsx b/src/components/pages/earn/EarnHomePage/cmp.tsx new file mode 100644 index 0000000..1e78e9a --- /dev/null +++ b/src/components/pages/earn/EarnHomePage/cmp.tsx @@ -0,0 +1,6 @@ +import { useRedirect } from '@/hooks/common/useRedirect' + +export default function EarnHomePage() { + useRedirect('/earn/staking') + return <>Loading... +} diff --git a/src/components/pages/earn/EarnHomePage/index.ts b/src/components/pages/earn/EarnHomePage/index.ts new file mode 100644 index 0000000..7a9f83f --- /dev/null +++ b/src/components/pages/earn/EarnHomePage/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/pages/earn/StakingPage/cmp.tsx b/src/components/pages/earn/StakingPage/cmp.tsx index 82adc11..0001bec 100644 --- a/src/components/pages/earn/StakingPage/cmp.tsx +++ b/src/components/pages/earn/StakingPage/cmp.tsx @@ -1,6 +1,27 @@ +import { useState } from 'react' import Head from 'next/head' +import { CCN, NodeManager } from '@/domain/node' +import { useRequest } from '@/hooks/common/useRequest' +import { Button, Tabs } from '@aleph-front/aleph-core' +import NodesTable from '@/components/common/NodesTable' +import NameCell from '@/components/common/NameCell' +import LinkedCell from '@/components/common/LinkedCell' +import ScoreCell from '@/components/common/ScoreCell' +import APYCell from '@/components/common/APYCell' +import StakedCell from '@/components/common/StakedCell' export default function StakingPage() { + const [tab, setTab] = useState('nodes') + + async function doRequest(): Promise { + const manager = new NodeManager({} as any) + return await manager.getCCNNodes() + } + + const { data } = useRequest({ doRequest, triggerOnMount: true }) + + console.log(data) + return ( <> @@ -8,7 +29,121 @@ export default function StakingPage() { -
STAKING
+
+

+ Staking +

+
+

+ What is staking? +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum + aliquam lectus non eros malesuada egestas eu vitae ipsum. Donec sed + faucibus sapien. Interdum et malesuada fames ac ante ipsum primis in + faucibus. Aenean at scelerisque tortor. Sed nec placerat lacus. + Fusce facilisis arcu in vulputate eleifend. Pellentesque at ante + est. Vivamus cursus lorem odio. Aenean porttitor rutrum erat sed + suscipit. Duis viverra, ligula et lacinia lobortis, sem ante luctus + sapien, id gravida justo odio vel sapien. Ut vel volutpat mauris, in + congue lorem. Etiam mollis, magna at finibus dictum, metus diam + malesuada felis, at mattis magna lectus eget enim. +

+
+
+ + {tab === 'stake' ? ( + <>STAKE + ) : ( + <> + {!data ? ( + <>Loading... + ) : ( + , + }, + { + label: 'NAME', + width: '10%', + render: (row) => ( + + ), + }, + { + label: 'STAKED', + width: '20%', + render: (row) => ( + + ), + }, + { + label: 'LINKED', + width: '10%', + render: (row) => ( + + ), + }, + { + label: 'SCORE', + width: '20%', + render: (row) => , + }, + { + label: '', + align: 'right', + width: '30%', + render: () => ( +
+ + +
+ ), + }, + ]} + data={data} + borderType="solid" + oddRowNoise + /> + )} + + )} +
+
) } diff --git a/src/domain/message.ts b/src/domain/message.ts index 0fc28e2..babfa6f 100644 --- a/src/domain/message.ts +++ b/src/domain/message.ts @@ -1,13 +1,12 @@ -import { AnyMessage } from '@/helpers/utils' import { Account } from 'aleph-sdk-ts/dist/accounts/account' import { any, forget } from 'aleph-sdk-ts/dist/messages' import E_ from '../helpers/errors' -import { defaultConsoleChannel } from '@/helpers/constants' +import { defaultAccountChannel } from '@/helpers/constants' export class MessageManager { constructor( protected account: Account, - protected channel = defaultConsoleChannel, + protected channel = defaultAccountChannel, ) {} /** @@ -28,7 +27,7 @@ export class MessageManager { /** * Deletes a VM using a forget message */ - async del(message: AnyMessage) { + async del(message: any) { try { const msg = await forget.Publish({ account: this.account, diff --git a/src/domain/node.ts b/src/domain/node.ts new file mode 100644 index 0000000..3b53237 --- /dev/null +++ b/src/domain/node.ts @@ -0,0 +1,166 @@ +import { defaultAccountChannel, scoringAddress } from '@/helpers/constants' +import { Account } from 'aleph-sdk-ts/dist/accounts/account' +import { messages } from 'aleph-sdk-ts' + +const { post } = messages + +export type BaseNode = { + address: string + multiaddress: string + banner: string + decentralization: number + description: string + hash: string + locked: boolean + manager: string + name: string + owner: string + performance: number + picture: string + score: number + score_updated: boolean + registration_url: string + reward: string + status: string + time: number +} + +export type CCN = BaseNode & { + authorized: string[] + has_bonus: true + resource_nodes: string[] + stakers: Record + total_staked: number + scoreData?: CCNScore + crnsData: CRN[] +} + +export type CRN = BaseNode & { + authorized: string + parent: string + type: string + scoreData?: CRNScore +} + +export type BaseNodeScore = { + decentralization: number + node_id: string + performance: number + total_score: number + version: number +} + +export type CCNScore = BaseNodeScore & { + measurements: { + aggregate_latency_score_p25: number + aggregate_latency_score_p95: number + base_latency_score_p25: number + base_latency_score_p95: number + eth_height_remaining_score_p25: number + eth_height_remaining_score_p95: number + file_download_latency_score_p25: number + file_download_latency_score_p95: number + metrics_latency_score_p25: number + metrics_latency_score_p95: number + node_version_latest: number + node_version_missing: number + node_version_obsolete: number + node_version_other: number + node_version_outdated: number + node_version_prerelease: number + nodes_with_identical_asn: number + total_nodes: number + } +} + +export type CRNScore = BaseNodeScore & { + measurements: { + base_latency_score_p25: number + base_latency_score_p95: number + diagnostic_vm_latency_score_p25: number + diagnostic_vm_latency_score_p95: number + full_check_latency_score_p25: number + full_check_latency_score_p95: number + node_version_latest: number + node_version_missing: number + node_version_obsolete: number + node_version_other: number + node_version_outdated: number + node_version_prerelease: number + nodes_with_identical_asn: number + total_nodes: number + } +} + +export class NodeManager { + constructor( + protected account: Account, + protected channel = defaultAccountChannel, + ) {} + + async getCCNNodes(): Promise { + const res = await fetch( + 'https://api2.aleph.im/api/v0/aggregates/0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10.json?keys=corechannel&limit=100', + ) + + const content = await res.json() + const crns: CRN[] = content?.data?.corechannel?.resource_nodes + let ccns: CCN[] = content?.data?.corechannel?.nodes + + const scores = await this.getScores() + const scoresMap = new Map(scores.map((score) => [score.node_id, score])) + + ccns = ccns.map((ccn) => { + const scoreData = scoresMap.get(ccn.hash) + if (!scoreData) return ccn + + return { + ...ccn, + score: scoreData.total_score, + scoreData, + } + }) + + const crnsMap = crns.reduce((ac, cu) => { + const crns = (ac[cu.parent] = ac[cu.parent] || []) + crns.push(cu) + return ac + }, {} as Record) + + ccns = ccns.map((ccn) => { + const crnsData = crnsMap[ccn.hash] || [] + if (!crnsData) return ccn + + return { + ...ccn, + crnsData, + } + }) + + console.log(ccns) + + return ccns + } + + protected async getScores(): Promise { + const res = await post.Get({ + types: 'aleph-scoring-scores', + addresses: [scoringAddress], + pagination: 1, + page: 1, + }) + + console.log(res) + + return (res.posts[0]?.content as any)?.scores?.ccn + } + + protected async getMetrics() { + return post.Get({ + types: 'aleph-network-metrics', + addresses: [scoringAddress], + pagination: 1, + page: 1, + }) + } +} diff --git a/src/domain/reward.ts b/src/domain/reward.ts new file mode 100644 index 0000000..ece3030 --- /dev/null +++ b/src/domain/reward.ts @@ -0,0 +1,66 @@ +import { Account } from 'aleph-sdk-ts/dist/accounts/account' +import { defaultAccountChannel } from '@/helpers/constants' +import { CCN } from './node' +import { normalizeValue } from '@/helpers/utils' + +export class RewardManager { + constructor( + protected account: Account, + protected channel = defaultAccountChannel, + ) {} + + activeNodes(nodes: CCN[]): CCN[] { + return nodes.filter((node) => node.status === 'active') + } + + totalStakedInActive(nodes: CCN[]): number { + return this.activeNodes(nodes).reduce((ac, cu) => ac + cu.total_staked, 0) + } + + totalPerDay(nodes: CCN[]): number { + const activeNodes = this.activeNodes(nodes).length + return 15000 * ((Math.log10(activeNodes) + 1) / 3) + } + + totalPerAlephPerDay(nodes: CCN[]): number { + return this.totalPerDay(nodes) / this.totalStakedInActive(nodes) + } + + currentAPY(nodes: CCN[]): number { + return (1 + this.totalPerAlephPerDay(nodes)) ** 365 - 1 + } + + computeEstimatedStakersAPY(node: CCN, nodes: CCN[]): number { + 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 + + estAPY = this.currentAPY(nodes) * normalizedScore * (1 - linkedCRNPenalty) + } + + return estAPY + } + + computeCCNRewards(node: CCN, nodes: CCN[]): number { + let estRewards = 0 + + 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 + + estRewards = pool * normalizedScore * (1 - linkedCRNPenalty) + } + + return estRewards + } +} diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index 6286e29..6b2fc54 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -5,3 +5,6 @@ export const breadcrumbNames = { '/dashboard/function': 'SETUP NEW FUNCTION', '/dashboard/volume': 'SETUP NEW VOLUME', } + +export const apiServer = 'https://api2.aleph.im' +export const scoringAddress = '0x4D52380D3191274a04846c89c069E6C3F2Ed94e4' diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index c607dd9..a0fef97 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -314,3 +314,212 @@ export function toKebabCase(input: string): string { export function toSnakeCase(input: string): string { return toKebabCase(input).replace(/-/g, '_') } + +// ------------------------------------------ +// ------------------------------------------ + +export function convertTimestamp(timestamp: number) { + const a = new Date(timestamp * 1000) + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ] + const year = a.getFullYear() + const month = months[a.getMonth()] + const date = a.getDate() + const hour = a.getHours() + const min = a.getMinutes() + const sec = a.getSeconds() + const time = + date + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ':' + sec + return time +} + +/** + * Check if a value is null, undefined, empty string or empty array but not 0. Prevents false positive on 0 using !value + * + * @param {any} input + * @returns true if value is null, undefined, empty string or empty array + */ +export function nullButNot0(input: any) { + return ( + input === undefined || input === null || input === '' || input?.length > 0 + ) +} + +/** + * Normalises a value between a min and max value, if floor and ceil are provided, it will return the floor or ceil if the value is outside the [min, max] interval + * + * @param {number} input + * @param {number} min + * @param {number} max + * @param {number} floor + * @param {number} ceil + * @returns a number in the [min, max] interval + */ +export function normalizeValue( + input: number, + min: number, + max: number, + floor: number, + ceil: number, +) { + if (!input) return 0 + if (input > max) return 1 + if (input < min) return 0 + + const normalized = (input - min) / (max - min) + if (floor === undefined || ceil === undefined) return normalized + if (normalized < min) return floor + if (normalized > max) return ceil + return normalized +} + +/** + * Fetches a URL and caches the result in LocalStorage for a given time + * + * @param {string} url The URL to fetch + * @param {string} cacheKey The LocalStorage key to use for cachinng (must be unique) + * @param {number} cacheTime The time in ms to cache the data + * @returns + */ +export async function fetchAndCache( + url: string, + cacheKey: string, + cacheTime: number, +) { + const cached = localStorage.getItem(cacheKey) + const now = Date.now() + if (cached) { + const { cachedAt, value } = JSON.parse(cached) + if (now - cachedAt < cacheTime) { + console.log(`Retrieved ${cacheKey} from cache`) + return value + } + } + + try { + const data = await fetch(url) + const value = await data.json() + + const toCache = JSON.stringify({ + cachedAt: now, + value, + }) + localStorage.setItem(cacheKey, toCache) + return value + } catch (error) { + console.error(`Failed to fetch ${url}`, error) + if (cached) return JSON.parse(cached).value + } +} + +/** + * Takes a list of github releases and returns the latest, the latest prerelease and a list of outdated versions + * + * @param {Array} payload A list of github releases, returned from the API (https://api.github.com/repos/[owner]/[name]/releases) + * @param {Number} outdatedAfter The time in ms after which a release is considered outdated (defaults to 14 days) + */ +export function getLatestReleases( + payload: any, + outdatedAfter = 1000 * 60 * 60 * 24 * 14, +) { + const versions = { + latest: null, + prerelease: null, + outdated: null, + } + + let latestReleaseDate = 0 + if (!payload) return versions + + for (const item of payload) { + if (item.prerelease && !versions.prerelease) { + versions.prerelease = item.tag_name + } + if (!item.prerelease && !versions.latest) { + versions.latest = item.tag_name + latestReleaseDate = new Date(item.published_at).getTime() + } + if ( + versions.latest && + versions.prerelease && + !versions.outdated && + !item.prerelease && + Date.now() - latestReleaseDate < outdatedAfter + ) { + versions.outdated = item.tag_name + } + } + + return versions +} + +/** + * Strips away the extra commit number and hash from the "git describe --tags" output + * + * @param {String} gitTag The output of a "git describe --tags" command + * @returns The tag name without the extra commit number and hash + */ +export function stripExtraTagDescription(gitTag: string) { + return gitTag.replace(/-\d+-g\w{7}$/gi, '') +} + +/** + * Returns a list of issues that might explain why the node is not scored + * + */ +export function diagnoseMetrics(metrics: any) { + if (metrics.base_latency_ipv4 === undefined) { + return [ + 'Your CRN seems unreachable by our metrics. Is it up and running ? Did you configure the domain name correctly ?', + ] + } + if ( + !metrics.base_latency || + !metrics.diagnostic_vm_latency || + !metrics.full_check_latency + ) { + return ['No IPv6 connectivity detected. Please check your configuration.'] + } + + const issues = [] + if (metrics.base_latency > 2) { + issues.push('Base latency is too high (max 2 seconds)') + } + if (metrics.diagnostic_vm_latency > 2.5) { + issues.push('Diagnostic VM latency is too high (max 2.5 seconds)') + } + if (metrics.full_check_latency > 100) { + issues.push('Full check latency is too high (max 100 seconds)') + } + + return issues +} + +/** + * Returns the time in ms until the next score message is issued + * Takes a unix timestamp as input and returns a string + */ +export function timeUntilNextScoreMessage(metrics: any) { + // ! Unix timestamp is in seconds + const ONE_DAY = 60 * 60 * 24 + const offset = metrics.measured_at % ONE_DAY + const nextMessage = ONE_DAY - offset + + if (nextMessage < 60 * 60 * 2) { + return Math.floor(nextMessage / 60) + ' minutes' + } + + return Math.round(nextMessage / 3600) + ' hours' +} diff --git a/src/hooks/common/useRedirect.ts b/src/hooks/common/useRedirect.ts new file mode 100644 index 0000000..8c715e3 --- /dev/null +++ b/src/hooks/common/useRedirect.ts @@ -0,0 +1,10 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' + +export function useRedirect(path: string): void { + const router = useRouter() + + useEffect(() => { + router.replace(path) + }) +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 72880df..e8579d1 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -14,6 +14,8 @@ import { AppStateProvider } from '@/contexts/appState' import Header from '@/components/common/Header' import NotificationProvider from '@/components/common/NotificationProvider' import Main from '@/components/common/Main' +import Sidebar from '@/components/common/Sidebar' +import Viewport from '@/components/common/Viewport/cmp' export default function App({ Component, pageProps }: AppProps) { return ( @@ -22,10 +24,13 @@ export default function App({ Component, pageProps }: AppProps) { -
-
- -
+ + +
+
+ +
+
diff --git a/src/pages/earn/ccn.tsx b/src/pages/earn/ccn.tsx new file mode 100644 index 0000000..64808e0 --- /dev/null +++ b/src/pages/earn/ccn.tsx @@ -0,0 +1,3 @@ +import CoreChannelNodesPage from '@/components/pages/earn/CoreChannelNodesPage' + +export default CoreChannelNodesPage diff --git a/src/pages/earn/crn.tsx b/src/pages/earn/crn.tsx new file mode 100644 index 0000000..06d4ece --- /dev/null +++ b/src/pages/earn/crn.tsx @@ -0,0 +1,3 @@ +import ComputeResourceNodesPage from '@/components/pages/earn/ComputeResourceNodesPage' + +export default ComputeResourceNodesPage diff --git a/src/pages/earn/index.ts b/src/pages/earn/index.ts new file mode 100644 index 0000000..144dc88 --- /dev/null +++ b/src/pages/earn/index.ts @@ -0,0 +1,3 @@ +import EarnHomePage from '@/components/pages/earn/EarnHomePage' + +export default EarnHomePage diff --git a/src/styles/global.tsx b/src/styles/global.tsx index 2a5e9f5..17d23d7 100644 --- a/src/styles/global.tsx +++ b/src/styles/global.tsx @@ -7,11 +7,6 @@ export const GlobalStylesOverride = createGlobalStyle` min-height: 100vh; } - // @note: Use it only for development - /* html { - font-size: 16px; - } */ - /* FIXME: */ .unavailable-content { opacity: 0.3; @@ -34,4 +29,12 @@ export const GlobalStylesOverride = createGlobalStyle` max-width: 100%; overflow: auto; } + + p { + color: ${({ theme }) => theme.color.text}; + } + + html { + font-size: 16px !important; + } `