Feature/explorer v2 connect wallet (#5301)
* set up libs for connecting wallet * wallet - get address and balance * format wallet balance * start staking modal work * Yana/node page (#5276) * Add nym-node page api WIP * nym-node page api WIP * Add rewards card * Add account balances * fix build * Add USD price to tokenomics card * fix build * fix build * fix build * Refactor ProgressBar * Add profile card * fix build * replace hardcoded id * Add build version and node roles * fix build * rename id param to address * add node table to explorer page * get node details from unstable endpoint + layout updates * allow node select from table * stop propogation on favorite/unfavorite * update self bond data * card refactors * revert node engine requirement * use node v20 --------- Co-authored-by: Yana <yanok87@users.noreply.github.com> Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk> * set up libs for connecting wallet * wallet - get address and balance * update typings + refactor favorite component * fix event propogation * fix multiple url declarations * move pages to pages directory * build staking page and base components * refactor wallet components and add useNymClient hook * enhance DesktopHeader to highlight active menu item with icon * remove StakeModal and getNymNodes components * refactor NodeTable and NodeTableWithAction components for improved staking functionality * refactor API configuration and theme styles for improved maintainability * refactor Loading component to implement LoadingModal with enhanced styling and functionality * add InfoModal component for displaying informational messages with customizable actions * fix type * fix linting * update CI workflow to use Node.js 20 * add explorer package to dependencies in package.json * fix mui grid2 build errors * fix full page reload * fix unstaking * fix delegation operation * fix up workspace packages * refactor staking and wallet components for improved functionality and readability * fix undelegation (again) * update ci to include new explorer * update wallet ci node version * remove logs --------- Co-authored-by: Yana Matrosova <42305364+yanok87@users.noreply.github.com> Co-authored-by: Yana <yanok87@users.noreply.github.com>
This commit is contained in:
@@ -10,7 +10,7 @@ on:
|
||||
- "nym-connect/desktop/package.json"
|
||||
- "nym-wallet/src/**"
|
||||
- "nym-wallet/package.json"
|
||||
- "explorer/**"
|
||||
- "explorer-nextjs/src/**"
|
||||
- ".github/workflows/ci-lint-typescript.yml"
|
||||
|
||||
jobs:
|
||||
@@ -37,12 +37,12 @@ jobs:
|
||||
- name: Install wasm-opt
|
||||
uses: ./.github/actions/install-wasm-opt
|
||||
with:
|
||||
version: '116'
|
||||
version: "116"
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Install
|
||||
run: yarn
|
||||
|
||||
@@ -4,8 +4,8 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'explorer/**'
|
||||
- '.github/workflows/ci-nym-network-explorer.yml'
|
||||
- "explorer/**"
|
||||
- ".github/workflows/ci-nym-network-explorer.yml"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -15,78 +15,78 @@ jobs:
|
||||
build:
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
continue-on-error: true
|
||||
- name: Build shared packages
|
||||
run: cd .. && yarn && yarn build
|
||||
- name: Set environment from the example
|
||||
run: cp .env.prod .env
|
||||
# - run: yarn test
|
||||
# continue-on-error: true
|
||||
- run: yarn && yarn build
|
||||
continue-on-error: true
|
||||
- run: yarn storybook:build
|
||||
name: Build storybook
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "explorer/dist/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/network-explorer-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Deploy storybook to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "explorer/storybook-static/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ne-sb-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: network-explorer
|
||||
NYM_PROJECT_NAME: "Network Explorer"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "network-explorer-${{ env.GITHUB_REF_SLUG }}"
|
||||
NYM_CI_WWW_LOCATION_STORYBOOK: "ne-sb-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
- name: Deploy
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CD_PROD_NE_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "explorer/dist/"
|
||||
REMOTE_HOST: ${{ secrets.CD_PROD_NE_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CD_PROD_NE_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CD_PROD_NE_REMOTE_TARGET }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
continue-on-error: true
|
||||
- name: Build shared packages
|
||||
run: cd .. && yarn && yarn build
|
||||
- name: Set environment from the example
|
||||
run: cp .env.prod .env
|
||||
# - run: yarn test
|
||||
# continue-on-error: true
|
||||
- run: yarn && yarn build
|
||||
continue-on-error: true
|
||||
- run: yarn storybook:build
|
||||
name: Build storybook
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "explorer/dist/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/network-explorer-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Deploy storybook to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "explorer/storybook-static/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ne-sb-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: network-explorer
|
||||
NYM_PROJECT_NAME: "Network Explorer"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "network-explorer-${{ env.GITHUB_REF_SLUG }}"
|
||||
NYM_CI_WWW_LOCATION_STORYBOOK: "ne-sb-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
- name: Deploy
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CD_PROD_NE_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "explorer/dist/"
|
||||
REMOTE_HOST: ${{ secrets.CD_PROD_NE_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CD_PROD_NE_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CD_PROD_NE_REMOTE_TARGET }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:prod": "yarn build",
|
||||
"build:prod": "yarn --cwd .. build && next build",
|
||||
"start": "next start",
|
||||
"lint": "biome check --fix"
|
||||
},
|
||||
@@ -12,19 +12,23 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cosmjs/cosmwasm-stargate": "^0.32.4",
|
||||
"@cosmjs/proto-signing": "^0.32.4",
|
||||
"@chain-registry/types": "^0.50.36",
|
||||
"@cosmos-kit/keplr-extension": "^2.14.0",
|
||||
"@cosmos-kit/react": "^2.20.1",
|
||||
"@emotion/cache": "^11.13.5",
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@emotion/styled": "^11.13.5",
|
||||
"@interchain-ui/react": "^1.26.1",
|
||||
"@mui/icons-material": "^5.16.11",
|
||||
"@mui/material": "^6.1.10",
|
||||
"@mui/material-nextjs": "^6.1.9",
|
||||
"@mui/x-date-pickers": "^7.23.2",
|
||||
"@nivo/line": "^0.88.0",
|
||||
"@nymproject/contract-clients": "^1.4.1",
|
||||
"@nymproject/react": "1.0.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"chain-registry": "^1.69.64",
|
||||
"cldr-compact-number": "^0.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"material-react-table": "^3.0.3",
|
||||
@@ -33,7 +37,8 @@
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||
"react-random-avatars": "^1.3.1",
|
||||
"react-world-flags": "^1.6.0"
|
||||
"react-world-flags": "^1.6.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
>
|
||||
<g clip-path="url(#clip0_8428_65000)">
|
||||
<rect
|
||||
y="3"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 0 3)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
y="15"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 0 15)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="12"
|
||||
y="3"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 12 3)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="9"
|
||||
y="12"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 9 12)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="12"
|
||||
y="15"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 12 15)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="3"
|
||||
y="6"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 3 6)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="3"
|
||||
y="12"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 3 12)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="9"
|
||||
y="6"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 9 6)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="6"
|
||||
y="9"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 6 9)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_8428_65000">
|
||||
<rect
|
||||
width="15"
|
||||
height="15"
|
||||
fill="white"
|
||||
style="fill: white; fill-opacity: 1"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" />
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_angular_2549_7570"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(12 12.5) rotate(90) scale(12)"
|
||||
>
|
||||
<stop stopColor="#22D27E" />
|
||||
<stop offset="1" stopColor="#9002FF" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 515 B |
@@ -0,0 +1,23 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
>
|
||||
<g clipPath="url(#clip0_2549_7563)">
|
||||
<path
|
||||
d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2549_7563">
|
||||
<rect width="24" height="24" fill="white" transform="translate(0 0.5)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
+2
-1
@@ -6,7 +6,8 @@ import { AccountInfoCard } from "@/components/accountPageComponents/AccountInfoC
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box, Grid2, Typography } from "@mui/material";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import Grid2 from "@mui/material/Grid2";
|
||||
|
||||
export default async function Account({
|
||||
params,
|
||||
@@ -0,0 +1,121 @@
|
||||
import type NodeData from "@/app/api/types";
|
||||
import { NYM_NODES } from "@/app/api/urls";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard";
|
||||
import { NodeMetricsCard } from "@/components/nymNodePageComponents/NodeMetricsCard";
|
||||
import { NodeProfileCard } from "@/components/nymNodePageComponents/NodeProfileCard";
|
||||
import { NodeRewardsCard } from "@/components/nymNodePageComponents/NodeRewardsCard";
|
||||
import { QualityIndicatorsCard } from "@/components/nymNodePageComponents/QualityIndicatorsCard";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box } from "@mui/material";
|
||||
import Grid2 from "@mui/material/Grid2";
|
||||
|
||||
export default async function NymNode({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
try {
|
||||
const id = Number((await params).id);
|
||||
|
||||
const response = await fetch(NYM_NODES, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const nymNodes: NodeData[] = await response.json();
|
||||
|
||||
if (!nymNodes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nymNode = nymNodes.find((node) => node.node_id === id);
|
||||
|
||||
if (!nymNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid2 container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid2 size={12}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<SectionHeading title="Nym Node Details" />
|
||||
<ExplorerButtonGroup
|
||||
options={[
|
||||
{ label: "Nym Node", isSelected: true, link: "/nym-node/1" },
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: false,
|
||||
link: `/account/${nymNode.bond_information.owner}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<NodeProfileCard
|
||||
bondInfo={nymNode.bond_information}
|
||||
nodeDescription={nymNode.description}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<BasicInfoCard
|
||||
bondInfo={nymNode.bond_information}
|
||||
nodeDescription={nymNode.description}
|
||||
rewardDetails={nymNode.rewarding_details}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<QualityIndicatorsCard nodeDescription={nymNode.description} />
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeRewardsCard rewardDetails={nymNode.rewarding_details} />
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeMetricsCard
|
||||
nodeDescription={nymNode.description}
|
||||
nodeId={nymNode.bond_information.node_id}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
let errorMessage = "An error occurred";
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import OverviewCards from "@/components/staking/OverviewCards";
|
||||
import StakeTableWithAction from "@/components/staking/StakeTableWithAction";
|
||||
import SubHeaderRow from "@/components/staking/SubHeaderRow";
|
||||
|
||||
export default async function StakingPage() {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<SectionHeading title="Staking" />
|
||||
<SubHeaderRow />
|
||||
<OverviewCards />
|
||||
<StakeTableWithAction />
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -84,96 +84,6 @@ export interface ExplorerCache {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBondInfo {
|
||||
bond_information: {
|
||||
bonding_height: number;
|
||||
is_unbonding: boolean;
|
||||
node: {
|
||||
custom_http_port: number;
|
||||
host: string;
|
||||
identity_key: string;
|
||||
};
|
||||
node_id: number;
|
||||
original_pledge: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
};
|
||||
owner: string;
|
||||
};
|
||||
rewarding_details: {
|
||||
cost_params: {
|
||||
profit_margin_percent: string;
|
||||
interval_operating_cost: {
|
||||
denom: string;
|
||||
amount: string;
|
||||
};
|
||||
};
|
||||
delegates: string;
|
||||
last_rewarded_epoch: number;
|
||||
operator: string;
|
||||
total_unit_reward: string;
|
||||
unique_delegations: number;
|
||||
unit_delegation: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface INodeDescription {
|
||||
contract_node_type: string;
|
||||
description: {
|
||||
authenticator: object;
|
||||
address: string;
|
||||
auxiliary_details: {
|
||||
location: string;
|
||||
accepted_operator_terms_and_conditions: boolean;
|
||||
announce_ports: {
|
||||
verloc_port: number | null;
|
||||
mix_port: number | null;
|
||||
};
|
||||
};
|
||||
build_information: {
|
||||
binary_name: string;
|
||||
build_timestamp: string;
|
||||
build_version: string;
|
||||
cargo_profile: string;
|
||||
cargo_triple: string;
|
||||
commit_branch: string;
|
||||
commit_sha: string;
|
||||
commit_timestamp: string;
|
||||
rustc_channel: string;
|
||||
rustc_version: string;
|
||||
};
|
||||
declared_role: {
|
||||
entry: boolean;
|
||||
exit_ipr: boolean;
|
||||
exit_nr: boolean;
|
||||
mixnode: boolean;
|
||||
};
|
||||
host_information: {
|
||||
hostname: string | null;
|
||||
ip_address: string[];
|
||||
keys: {
|
||||
ed25519: string;
|
||||
x25519: string;
|
||||
x25519_noise: string | null;
|
||||
};
|
||||
};
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
};
|
||||
last_polled: string;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number | null;
|
||||
};
|
||||
network_requester: {
|
||||
address: string;
|
||||
uses_exit_policy: boolean;
|
||||
};
|
||||
wireguard: null | object;
|
||||
};
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
const getExplorerData = async () => {
|
||||
// FETCH NYMNODES
|
||||
const fetchNymNodes = await fetch(NYM_NODES_DESCRIBED, {
|
||||
|
||||
@@ -54,7 +54,7 @@ export type NodeDescription = {
|
||||
ws_port: number;
|
||||
wss_port: number | null;
|
||||
};
|
||||
};
|
||||
} | null;
|
||||
|
||||
export type BondInformation = {
|
||||
node_id: number;
|
||||
|
||||
@@ -30,3 +30,5 @@ export const NYM_ACCOUNT_ADDRESS =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/account/";
|
||||
export const NYM_PRICES_API =
|
||||
"https://canary-nym-vpn-chain-payment-watcher.nymte.ch/v1/price/average";
|
||||
export const VALIDATOR_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import { Link } from "@/components/muiLink";
|
||||
import { Button, Stack, Typography } from "@mui/material";
|
||||
|
||||
const ErrorPage = ({ error }: { error: Error }) => {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Stack spacing={2} justifyContent="flex-start">
|
||||
<Typography variant="body1">
|
||||
An error occurred: {error.message}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Please try again later or contact support
|
||||
</Typography>
|
||||
<Link href="/" underline="none">
|
||||
<Button variant="contained" sx={{ maxWidth: 100 }} size="small">
|
||||
Home
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
</ContentLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,12 +1,13 @@
|
||||
import { Header } from "@/components/header";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import ThemeProvider from "@/providers/ThemeProvider";
|
||||
import Providers from "@/providers";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import "./globals.css";
|
||||
import "@interchain-ui/react/styles";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Nym Explorer V2",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -17,10 +18,10 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<ThemeProvider>
|
||||
<Providers>
|
||||
<Header />
|
||||
<Wrapper>{children}</Wrapper>
|
||||
</ThemeProvider>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import type { IAccountBalancesTableProps } from "@/components/cards/AccountBalancesTable";
|
||||
import type { ContentCardProps } from "@/components/cards/MonoCard";
|
||||
|
||||
const explorerCard: ContentCardProps = {
|
||||
overTitle: "SINGLE",
|
||||
profileImage: {},
|
||||
title: "SINGLE",
|
||||
profileCountry: {
|
||||
countryCode: "NO",
|
||||
countryName: "Norway",
|
||||
},
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
},
|
||||
titlePrice: {
|
||||
price: 1.15,
|
||||
upDownLine: {
|
||||
percentage: 10,
|
||||
numberWentUp: true,
|
||||
},
|
||||
},
|
||||
dataRows: {
|
||||
rows: [
|
||||
{ key: "Market cap", value: "$ 1000000" },
|
||||
{ key: "24H VOL", value: "$ 1000000" },
|
||||
],
|
||||
},
|
||||
graph: {
|
||||
data: [
|
||||
{
|
||||
date_utc: "2024-11-20",
|
||||
numericData: 10,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-21",
|
||||
numericData: 12,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-22",
|
||||
numericData: 9,
|
||||
},
|
||||
{
|
||||
date_utc: "2024-11-23",
|
||||
numericData: 11,
|
||||
},
|
||||
],
|
||||
color: "#00CA33",
|
||||
label: "Label",
|
||||
},
|
||||
nymAddress: {
|
||||
address: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
|
||||
title: "Nym address",
|
||||
},
|
||||
identityKey: {
|
||||
address: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
|
||||
title: "Nym address",
|
||||
},
|
||||
qrCode: {
|
||||
url: "https://nymtech.net",
|
||||
},
|
||||
ratings: {
|
||||
ratings: [
|
||||
{ title: "Rating-1", numberOfStars: 4 },
|
||||
{ title: "Rating-2", numberOfStars: 2 },
|
||||
{ title: "Rating-3", numberOfStars: 3 },
|
||||
],
|
||||
},
|
||||
progressBar: {
|
||||
start: "2024-12-08T12:26:19Z",
|
||||
showEpoch: true,
|
||||
},
|
||||
comments: true,
|
||||
paragraph: "Additional line",
|
||||
stakeButton: {
|
||||
label: "Stake on node",
|
||||
identityKey: "n1w7tfthyfkhh3au3mqpy294p4dk65dzal2h04su",
|
||||
},
|
||||
};
|
||||
|
||||
const accountStatsCard: IAccountBalancesTableProps = {
|
||||
// overTitle: "Total value",
|
||||
// priceTitle: 1990.0174,
|
||||
rows: [
|
||||
{ type: "Spendable", allocation: 15.53, amount: 12800, value: 1200 },
|
||||
{
|
||||
type: "Delegated",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
history: [
|
||||
{ type: "Liquid", amount: 6900 },
|
||||
{ type: "Locked", amount: 6900 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Claimable",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
history: [
|
||||
{ type: "Unlocked", amount: 6900 },
|
||||
{ type: "Staking rewards", amount: 6900 },
|
||||
{ type: "Operator comission", amount: 6900 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Self bonded",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
},
|
||||
{
|
||||
type: "Locked",
|
||||
allocation: 15.53,
|
||||
amount: 12800,
|
||||
value: 1200,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export { explorerCard, accountStatsCard };
|
||||
@@ -1,31 +0,0 @@
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
export default function NodePage() {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<Box sx={{ p: 5 }}>
|
||||
<Wrapper>
|
||||
<Typography fontWeight="light">Node page</Typography>
|
||||
<ExplorerButtonGroup
|
||||
options={[
|
||||
{
|
||||
label: "Node",
|
||||
link: "/node",
|
||||
isSelected: true,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
link: "/account",
|
||||
isSelected: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import type NodeData from "@/app/api/types";
|
||||
import { NYM_NODES } from "@/app/api/urls";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard";
|
||||
import { NodeMetricsCard } from "@/components/nymNodePageComponents/NodeMetricsCard";
|
||||
import { NodeProfileCard } from "@/components/nymNodePageComponents/NodeProfileCard";
|
||||
import { NodeRewardsCard } from "@/components/nymNodePageComponents/NodeRewardsCard";
|
||||
import { QualityIndicatorsCard } from "@/components/nymNodePageComponents/QualityIndicatorsCard";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box, Grid2 } from "@mui/material";
|
||||
|
||||
export default async function NymNode({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string; account?: string }>;
|
||||
}) {
|
||||
const id = Number((await params).id);
|
||||
|
||||
const response = await fetch(NYM_NODES, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
// refresh event list cache at given interval
|
||||
});
|
||||
|
||||
const nymNodes: NodeData[] = await response.json();
|
||||
|
||||
if (!nymNodes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nymNode = nymNodes.find((node) => node.node_id === id);
|
||||
|
||||
if (!nymNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid2 container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid2 size={12}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<SectionHeading title="Nym Node Details" />
|
||||
<ExplorerButtonGroup
|
||||
options={[
|
||||
{
|
||||
label: "Nym Node",
|
||||
isSelected: true,
|
||||
link: `/nym-node/${id}`,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: false,
|
||||
link: `/account/${nymNode.bond_information.owner}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<NodeProfileCard
|
||||
bondInfo={nymNode.bond_information}
|
||||
nodeDescription={nymNode.description}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<BasicInfoCard
|
||||
bondInfo={nymNode.bond_information}
|
||||
nodeDescription={nymNode.description}
|
||||
rewardDetails={nymNode.rewarding_details}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<QualityIndicatorsCard nodeDescription={nymNode.description} />
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeRewardsCard rewardDetails={nymNode.rewarding_details} />
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeMetricsCard
|
||||
nodeDescription={nymNode.description}
|
||||
nodeId={nymNode.bond_information.node_id}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<Box sx={{ p: 5 }}>
|
||||
<Wrapper>
|
||||
<Typography fontWeight="light">Stake page</Typography>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,9 +18,9 @@ export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
|
||||
return (
|
||||
<ExplorerCard
|
||||
label="Address"
|
||||
label=""
|
||||
title={balanceFormated}
|
||||
sx={{ height: "100%" }}
|
||||
sx={{ height: "100%", pt: 0 }}
|
||||
>
|
||||
<Stack gap={5}>
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
"use client";
|
||||
import CopyToClipboard from "@/components/copyToClipboard/CopyToClipboard";
|
||||
import { Box, Button, Card, CardContent, Typography } from "@mui/material";
|
||||
import Image from "next/image";
|
||||
import type React from "react";
|
||||
import type { FC } from "react";
|
||||
import profileImagePlaceholder from "../../../public/profileImagePlaceholder.png";
|
||||
import CardProfileCountry from "../countryFlag/CountryFlag";
|
||||
import type { ICountryFlag } from "../countryFlag/CountryFlag";
|
||||
// import { Remark42Comments } from "../comments";
|
||||
import { NymTokenSVG } from "../icons/NymTokenSVG";
|
||||
import { type ILineChartData, LineChart } from "../lineChart";
|
||||
import {
|
||||
type IUpDownPriceIndicatorProps,
|
||||
UpDownPriceIndicator,
|
||||
} from "../price/UpDownPriceIndicator";
|
||||
import type { IDynamicProgressBarProps } from "../progressBars/EpochProgressBar";
|
||||
import { CardQRCode, type ICardQRCodeProps } from "../qrCode/QrCode";
|
||||
import { StarRating } from "../starRating";
|
||||
|
||||
interface ICardTitlePriceProps {
|
||||
price: number;
|
||||
upDownLine: IUpDownPriceIndicatorProps;
|
||||
}
|
||||
const CardTitlePrice = (props: ICardTitlePriceProps): React.ReactNode => {
|
||||
const { price, upDownLine } = props;
|
||||
return (
|
||||
<Box display={"flex"} flexDirection={"column"} alignItems={"flex-end"}>
|
||||
<Box display={"flex"} justifyContent={"space-between"} width={"100%"}>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<NymTokenSVG />
|
||||
<Typography variant="h3" sx={{ color: "pine.400" }}>
|
||||
NYM
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="h3" sx={{ color: "pine.400" }}>
|
||||
${price}
|
||||
</Typography>
|
||||
</Box>
|
||||
<UpDownPriceIndicator {...upDownLine} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ICardDataRowsProps {
|
||||
rows: Array<{ key: string; value: string }>;
|
||||
}
|
||||
export const CardDataRows = (props: ICardDataRowsProps): React.ReactNode => {
|
||||
const { rows } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{rows.map((row, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={row.key}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i < rows.length - 1 ? "1px solid #C3D7D7" : "none"}
|
||||
sx={{ pt: 1, pb: i < rows.length - 1 ? 1 : 0 }}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: "pine.600" }}>
|
||||
{row.key}
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ color: "pine.600" }}>
|
||||
{row.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
interface ICardProileImage {
|
||||
url?: string;
|
||||
}
|
||||
const CardProfileImage = (props: ICardProileImage) => {
|
||||
const { url } = props;
|
||||
return (
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
{url ? (
|
||||
<Image src={url} alt="linkedIn" width={80} height={80} />
|
||||
) : (
|
||||
<Image
|
||||
src={profileImagePlaceholder}
|
||||
alt="profile picture"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardCopyAddressProps {
|
||||
title: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
const CardCopyAddress = (props: ICardCopyAddressProps) => {
|
||||
const { title, address } = props;
|
||||
return (
|
||||
<Box
|
||||
paddingTop={1}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
gap={2}
|
||||
borderBottom={"1px solid #C3D7D7"}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Box display={"flex"} justifyContent={"space-between"}>
|
||||
<Typography
|
||||
variant="body4"
|
||||
maxWidth={"90%"}
|
||||
sx={{ wordWrap: "break-word", color: "pine.950" }}
|
||||
>
|
||||
{address}
|
||||
</Typography>
|
||||
|
||||
<CopyToClipboard text={`Copy identity key ${address} to clipboard`} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICardRatingsProps {
|
||||
ratings: Array<{ title: string; numberOfStars: number }>;
|
||||
}
|
||||
|
||||
const CardRatings = (props: ICardRatingsProps) => {
|
||||
const { ratings } = props;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{ratings.map((rating, i) => {
|
||||
const RatingTitle = () => {
|
||||
switch (rating.numberOfStars) {
|
||||
case 1:
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
Bad
|
||||
</Typography>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
Bad
|
||||
</Typography>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
ok
|
||||
</Typography>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
Good
|
||||
</Typography>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
Excellent
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
key={rating.title}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={"flex"}
|
||||
justifyContent={"space-between"}
|
||||
borderBottom={i < ratings.length - 1 ? "1px solid #C3D7D7" : "none"}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
{rating.title}
|
||||
</Typography>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<StarRating value={rating.numberOfStars} />
|
||||
<RatingTitle />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContentCardProps = {
|
||||
overTitle?: string;
|
||||
profileImage?: ICardProileImage;
|
||||
title?: string | number;
|
||||
profileCountry?: ICountryFlag;
|
||||
upDownLine?: IUpDownPriceIndicatorProps;
|
||||
titlePrice?: ICardTitlePriceProps;
|
||||
dataRows?: ICardDataRowsProps;
|
||||
graph?: { data: Array<ILineChartData>; color: string; label: string };
|
||||
progressBar?: IDynamicProgressBarProps;
|
||||
paragraph?: string;
|
||||
nymAddress?: ICardCopyAddressProps;
|
||||
identityKey?: ICardCopyAddressProps;
|
||||
qrCode?: ICardQRCodeProps;
|
||||
ratings?: ICardRatingsProps;
|
||||
comments?: boolean;
|
||||
stakeButton?: {
|
||||
label: string;
|
||||
identityKey: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const MonoCard: FC<ContentCardProps> = ({
|
||||
title,
|
||||
titlePrice,
|
||||
overTitle,
|
||||
upDownLine,
|
||||
dataRows,
|
||||
graph,
|
||||
paragraph,
|
||||
profileImage,
|
||||
profileCountry,
|
||||
nymAddress,
|
||||
identityKey,
|
||||
qrCode,
|
||||
ratings,
|
||||
// comments,
|
||||
stakeButton,
|
||||
}) => (
|
||||
<Card sx={{ height: "100%", borderRadius: "unset", padding: 1 }}>
|
||||
<CardContent
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
{overTitle && (
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: "pine.600", letterSpacing: 0.7 }}
|
||||
>
|
||||
{overTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{profileImage && (
|
||||
<Box mt={3}>
|
||||
<CardProfileImage {...profileImage} />
|
||||
</Box>
|
||||
)}
|
||||
{title && (
|
||||
<Typography
|
||||
variant="h3"
|
||||
mt={3}
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{upDownLine && (
|
||||
<Box>
|
||||
<UpDownPriceIndicator {...upDownLine} />
|
||||
</Box>
|
||||
)}
|
||||
{profileCountry && (
|
||||
<Box mt={3}>
|
||||
<CardProfileCountry {...profileCountry} />
|
||||
</Box>
|
||||
)}
|
||||
{titlePrice && (
|
||||
<Box mt={3}>
|
||||
<CardTitlePrice {...titlePrice} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{qrCode && (
|
||||
<Box mt={3}>
|
||||
<CardQRCode {...qrCode} />
|
||||
</Box>
|
||||
)}
|
||||
{nymAddress && (
|
||||
<Box mt={3}>
|
||||
<CardCopyAddress {...nymAddress} />
|
||||
</Box>
|
||||
)}
|
||||
{identityKey && (
|
||||
<Box mt={3}>
|
||||
<CardCopyAddress {...identityKey} />
|
||||
</Box>
|
||||
)}
|
||||
{dataRows && (
|
||||
<Box mt={3}>
|
||||
<CardDataRows {...dataRows} />
|
||||
</Box>
|
||||
)}
|
||||
{ratings && (
|
||||
<Box mt={3}>
|
||||
<CardRatings {...ratings} />
|
||||
</Box>
|
||||
)}
|
||||
{graph && (
|
||||
<Box mt={3}>
|
||||
<LineChart
|
||||
data={graph.data}
|
||||
color={graph.color}
|
||||
label={graph.label}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{paragraph && (
|
||||
<Typography variant="subtitle1" sx={{ color: "pine.950" }} mt={3}>
|
||||
{paragraph}
|
||||
</Typography>
|
||||
)}
|
||||
{/* {comments && (
|
||||
<Box mt={3}>
|
||||
<Remark42Comments />
|
||||
</Box>
|
||||
)} */}
|
||||
{stakeButton && (
|
||||
<Box mt={3}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"stakeButton.identityKey :>> ",
|
||||
stakeButton.identityKey,
|
||||
)
|
||||
}
|
||||
variant="contained"
|
||||
>
|
||||
{stakeButton.label}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CURRENT_EPOCH } from "@/app/api/urls";
|
||||
import { AccessTime } from "@mui/icons-material";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { addSeconds } from "date-fns";
|
||||
import { format } from "date-fns";
|
||||
|
||||
export const fetcNextEpoch = async () => {
|
||||
const res = await fetch(CURRENT_EPOCH, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
const dateTime = addSeconds(
|
||||
new Date(data.current_epoch_start),
|
||||
data.epoch_length.secs,
|
||||
);
|
||||
|
||||
return { data, dateTime };
|
||||
};
|
||||
|
||||
const NextEpochTime = async () => {
|
||||
try {
|
||||
const epoch = await fetcNextEpoch();
|
||||
const formattedDate = format(epoch.dateTime, "HH:mm:ss");
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<AccessTime />
|
||||
<Typography variant="h5" fontWeight="light">
|
||||
Next epoch: {formattedDate}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default NextEpochTime;
|
||||
@@ -3,13 +3,27 @@ import {
|
||||
Favorite as FavoriteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
|
||||
const Favorite = ({ address }: { address: string }) => {
|
||||
const [favorites, saveFavorites] = useLocalStorage<string[]>(
|
||||
"nym-node-favorites",
|
||||
[],
|
||||
);
|
||||
|
||||
const onFavorite = (address: string) => {
|
||||
saveFavorites([...favorites, address]);
|
||||
};
|
||||
|
||||
if (favorites.includes(address)) {
|
||||
return <UnFavorite address={address} />;
|
||||
}
|
||||
|
||||
const Favorite = ({ onFavorite }: { onFavorite: () => void }) => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFavorite();
|
||||
onFavorite(address);
|
||||
}}
|
||||
>
|
||||
<FavoriteBorderIcon sx={{ color: "accent.main" }} />
|
||||
@@ -17,12 +31,21 @@ const Favorite = ({ onFavorite }: { onFavorite: () => void }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const UnFavorite = ({ onUnfavorite }: { onUnfavorite: () => void }) => {
|
||||
const UnFavorite = ({ address }: { address: string }) => {
|
||||
const [favorites, saveFavorites] = useLocalStorage<string[]>(
|
||||
"nym-node-favorites",
|
||||
[],
|
||||
);
|
||||
|
||||
const handleUnfavorite = (address: string) => {
|
||||
saveFavorites(favorites.filter((favorite) => favorite !== address));
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUnfavorite();
|
||||
handleUnfavorite(address);
|
||||
}}
|
||||
>
|
||||
<FavoriteIcon sx={{ color: "accent.main" }} />
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import NymLogo from "@/components/icons/NymLogo";
|
||||
import { Link } from "@/components/muiLink";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { subtitles } from "@/theme/typography";
|
||||
import { Box, Button, Divider } from "@mui/material";
|
||||
import type React from "react";
|
||||
import { Box, Divider } from "@mui/material";
|
||||
import ConnectWallet from "../wallet/ConnectWallet";
|
||||
import HeaderItem from "./HeaderItem";
|
||||
|
||||
const DUMMY_MENU_DATA = [
|
||||
export type MenuItem = {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const DUMMY_MENU_DATA: MenuItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Explorer",
|
||||
@@ -65,39 +69,10 @@ export const DesktopHeader = () => {
|
||||
}}
|
||||
>
|
||||
{DUMMY_MENU_DATA.map((menu) => (
|
||||
<Button
|
||||
key={menu.title}
|
||||
href={menu.url}
|
||||
sx={{
|
||||
borderRadius: 0,
|
||||
padding: 0,
|
||||
minWidth: "auto",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
height: "100%",
|
||||
...subtitles.subtitle1,
|
||||
"& .MuiButton-endIcon": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
},
|
||||
"& .MuiButton-startIcon": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
},
|
||||
"&:hover": {
|
||||
textDecoration: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{menu.title}
|
||||
</Button>
|
||||
<HeaderItem key={menu.id} menu={menu} />
|
||||
))}
|
||||
</Box>
|
||||
<Button variant="contained" size="small">
|
||||
Connect Wallet
|
||||
</Button>
|
||||
<ConnectWallet size="small" />
|
||||
</Wrapper>
|
||||
<Divider variant="fullWidth" sx={{ width: "100%" }} />
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { Circle } from "@mui/icons-material";
|
||||
import { Button, Stack } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { MenuItem } from "./DesktopHeader";
|
||||
|
||||
type HeaderItemProps = {
|
||||
menu: MenuItem;
|
||||
};
|
||||
const HeaderItem = ({ menu }: HeaderItemProps) => {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Stack direction="row" gap={2} key={menu.id} alignItems="center">
|
||||
{pathname.includes(menu.url) && <Circle sx={{ fontSize: 10 }} />}
|
||||
<Link href={menu.url} passHref>
|
||||
<Button
|
||||
sx={{
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{menu.title}
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderItem;
|
||||
@@ -0,0 +1,7 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const Cross = () => (
|
||||
<Image src="/icons/cross.svg" alt="cross" width={12.5} height={12.5} />
|
||||
);
|
||||
|
||||
export default Cross;
|
||||
@@ -0,0 +1,6 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
|
||||
export const Elips = () => (
|
||||
<Image src="/icons/elips.svg" alt="Elips" width={20} height={20} />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
|
||||
export const Token = () => {
|
||||
return <Image src="/icons/token.svg" alt="Token" width={20} height={20} />;
|
||||
};
|
||||
@@ -74,5 +74,4 @@ export const NoiseCard = async () => {
|
||||
)}
|
||||
</ExplorerCard>
|
||||
);
|
||||
// return <MonoCard {...noiseCard} />;
|
||||
};
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
export const RewardsCard = async () => {
|
||||
return (
|
||||
<ExplorerCard label="Operator rewards this month">
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
{`${formatBigNum(10_000_111)} NYM`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
{`${formatBigNum(10_000_111)} NYM`}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
"use client";
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Modal,
|
||||
Stack,
|
||||
type SxProps,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import * as React from "react";
|
||||
const modalStyle: SxProps = {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 500,
|
||||
bgcolor: "background.paper",
|
||||
boxShadow: 24,
|
||||
borderRadius: "16px",
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginY: 10,
|
||||
width: 1,
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
const LoadingModal: FCWithChildren<{
|
||||
text?: string;
|
||||
sx?: SxProps;
|
||||
backdropProps?: object;
|
||||
}> = ({ sx, backdropProps, text = "Please wait..." }) => (
|
||||
<Modal open slotProps={{ backdrop: backdropProps }}>
|
||||
<Box sx={{ ...modalStyle, ...sx }} textAlign="center">
|
||||
<Stack spacing={4} direction="row" alignItems="center">
|
||||
<CircularProgress size={25} />
|
||||
<Typography variant="h4">{text}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export default LoadingModal;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Button, Link, Stack, Typography } from "@mui/material";
|
||||
import SimpleModal from "./SimpleModal";
|
||||
|
||||
type InfoModalPropsClosed = {
|
||||
open: false;
|
||||
};
|
||||
|
||||
type InfoModalPropsOpen = {
|
||||
open: true;
|
||||
title: string;
|
||||
message: string;
|
||||
Action?: React.ReactNode;
|
||||
tx?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export type InfoModalProps = InfoModalPropsClosed | InfoModalPropsOpen;
|
||||
|
||||
const InfoModal = (props: InfoModalProps) => {
|
||||
if (!props.open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { open, onClose, title, message, tx, Action } = props;
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
title=""
|
||||
Actions={
|
||||
Action || (
|
||||
<Button variant="contained" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Typography variant="h3">{title}</Typography>
|
||||
<Typography variant="body3">{message}</Typography>
|
||||
{tx && (
|
||||
<Link href="/">
|
||||
<Typography variant="h5">Block explorer link</Typography>
|
||||
</Link>
|
||||
)}
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoModal;
|
||||
@@ -0,0 +1,53 @@
|
||||
import Cross from "@/components/icons/Cross";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
type SimpleModalPropsClosed = {
|
||||
open: false;
|
||||
};
|
||||
|
||||
type SimpleModalPropsOpen = {
|
||||
open: true;
|
||||
title: string;
|
||||
children?: React.ReactNode;
|
||||
Actions?: React.ReactNode;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export type SimpleModalProps = SimpleModalPropsClosed | SimpleModalPropsOpen;
|
||||
|
||||
const SimpleModal = (props: SimpleModalProps) => {
|
||||
if (!props.open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { title, children, Actions, onClose } = props;
|
||||
|
||||
return (
|
||||
<Dialog open maxWidth="sm" fullWidth onClose={onClose}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ p: 4 }}
|
||||
>
|
||||
<Typography variant="body1" sx={{ textTransform: "uppercase" }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton aria-label="close" size="large" onClick={onClose}>
|
||||
<Cross />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<DialogContent sx={{ p: 4, pt: 0 }}>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 4, pb: 4 }}>{Actions}</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleModal;
|
||||
@@ -1,16 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { Box, Button, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import {
|
||||
type MRT_ColumnDef,
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from "material-react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
import { Favorite, UnFavorite } from "../favorite/Favorite";
|
||||
import { Favorite } from "../favorite/Favorite";
|
||||
import Loading from "../loading";
|
||||
import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
|
||||
import StakeModal from "../staking/StakeModal";
|
||||
import { fee } from "../staking/schemas";
|
||||
import ConnectWallet from "../wallet/ConnectWallet";
|
||||
import type { MappedNymNode, MappedNymNodes } from "./NodeTableWithAction";
|
||||
|
||||
const ColumnHeading = ({
|
||||
@@ -27,24 +34,90 @@ const ColumnHeading = ({
|
||||
|
||||
const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
const router = useRouter();
|
||||
const { nymClient } = useNymClient();
|
||||
|
||||
const [favorites, saveFavorites] = useLocalStorage<string[]>(
|
||||
"nym-node-favorites",
|
||||
[],
|
||||
);
|
||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||
open: false,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedNodeForStaking, setSelectedNodeForStaking] = useState<{
|
||||
nodeId: number;
|
||||
identityKey: string;
|
||||
}>();
|
||||
|
||||
const handleFavorite = useCallback(
|
||||
(address: string) => {
|
||||
saveFavorites([...favorites, address]);
|
||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
|
||||
const handleStakeOnNode = async ({
|
||||
nodeId,
|
||||
amount,
|
||||
}: {
|
||||
nodeId: number;
|
||||
amount: string;
|
||||
}) => {
|
||||
const amountToDelegate = (Number(amount) * 1_000_000).toString();
|
||||
const uNymFunds = [{ amount: amountToDelegate, denom: "unym" }];
|
||||
|
||||
setIsLoading(true);
|
||||
setSelectedNodeForStaking(undefined);
|
||||
try {
|
||||
const tx = await nymClient?.delegate(
|
||||
{ nodeId },
|
||||
fee,
|
||||
"Delegation from Nym Explorer V2",
|
||||
uNymFunds,
|
||||
);
|
||||
console.log({ tx });
|
||||
setSelectedNodeForStaking(undefined);
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Success",
|
||||
message: "This operation can take up to one hour to process",
|
||||
tx: tx?.transactionHash,
|
||||
|
||||
onClose: () => setInfoModalProps({ open: false }),
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : "An error occurred while staking";
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Error",
|
||||
message: errorMessage,
|
||||
onClose: () => {
|
||||
setInfoModalProps({ open: false });
|
||||
},
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleOnSelectStake = useCallback(
|
||||
(node: MappedNymNode) => {
|
||||
if (!isWalletConnected) {
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Connect Wallet",
|
||||
message: "Connect your wallet to stake",
|
||||
Action: (
|
||||
<ConnectWallet
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
setInfoModalProps({
|
||||
open: false,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
onClose: () => setInfoModalProps({ open: false }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSelectedNodeForStaking({
|
||||
nodeId: node.nodeId,
|
||||
identityKey: node.bondInformation.node.identity_key,
|
||||
});
|
||||
},
|
||||
[favorites, saveFavorites],
|
||||
);
|
||||
|
||||
const handleUnfavorite = useCallback(
|
||||
(address: string) => {
|
||||
saveFavorites(favorites.filter((favorite) => favorite !== address));
|
||||
},
|
||||
[favorites, saveFavorites],
|
||||
[isWalletConnected],
|
||||
);
|
||||
|
||||
const columns: MRT_ColumnDef<MappedNymNode>[] = useMemo(
|
||||
@@ -117,8 +190,16 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
header: "Action",
|
||||
accessorKey: "Action",
|
||||
Header: <ColumnHeading>Action</ColumnHeading>,
|
||||
Cell: () => (
|
||||
<Button size="small" variant="outlined">
|
||||
hidden: !isWalletConnected,
|
||||
Cell: ({ row }) => (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOnSelectStake(row.original);
|
||||
}}
|
||||
>
|
||||
Stake
|
||||
</Button>
|
||||
),
|
||||
@@ -130,23 +211,12 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
accessorKey: "Favorite",
|
||||
Header: <ColumnHeading>Favorite</ColumnHeading>,
|
||||
sortingFn: "Favorite",
|
||||
Cell: ({ row }) =>
|
||||
favorites.includes(row.original.bondInformation.node.identity_key) ? (
|
||||
<UnFavorite
|
||||
onUnfavorite={() =>
|
||||
handleUnfavorite(row.original.bondInformation.node.identity_key)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Favorite
|
||||
onFavorite={() =>
|
||||
handleFavorite(row.original.bondInformation.node.identity_key)
|
||||
}
|
||||
/>
|
||||
),
|
||||
Cell: ({ row }) => (
|
||||
<Favorite address={row.original.bondInformation.owner} />
|
||||
),
|
||||
},
|
||||
],
|
||||
[favorites, handleFavorite, handleUnfavorite],
|
||||
[isWalletConnected, handleOnSelectStake],
|
||||
);
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
@@ -214,7 +284,19 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
return <MaterialReactTable table={table} />;
|
||||
return (
|
||||
<>
|
||||
{isLoading && <Loading />}
|
||||
<StakeModal
|
||||
nodeId={selectedNodeForStaking?.nodeId}
|
||||
identityKey={selectedNodeForStaking?.identityKey}
|
||||
onStake={handleStakeOnNode}
|
||||
onClose={() => setSelectedNodeForStaking(undefined)}
|
||||
/>
|
||||
<InfoModal {...infoModalProps} />
|
||||
<MaterialReactTable table={table} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeTable;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import getNymNodes from "@/actions/getNymNodes";
|
||||
import type NymNode from "@/app/api/types";
|
||||
import { ClientOnly } from "../clientOnly/ClientOnly";
|
||||
import NodeTable from "./NodeTable";
|
||||
import getNymNodes from "./actions";
|
||||
|
||||
const mappedNymNodes = (nodes: NymNode[]) =>
|
||||
nodes.map((node) => {
|
||||
@@ -22,11 +21,7 @@ const NodeTableWithAction = async () => {
|
||||
try {
|
||||
const nodes = await getNymNodes();
|
||||
const data = mappedNymNodes(nodes);
|
||||
return (
|
||||
<ClientOnly>
|
||||
<NodeTable nodes={data} />
|
||||
</ClientOnly>
|
||||
);
|
||||
return <NodeTable nodes={data} />;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
|
||||
@@ -19,10 +19,12 @@ interface IBasicInfoCardProps {
|
||||
export const BasicInfoCard = (props: IBasicInfoCardProps) => {
|
||||
const { bondInfo, nodeDescription, rewardDetails } = props;
|
||||
|
||||
const timeBonded = format(
|
||||
new Date(nodeDescription.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
);
|
||||
const timeBonded = nodeDescription
|
||||
? format(
|
||||
new Date(nodeDescription.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
)
|
||||
: "-";
|
||||
|
||||
const selfBond = formatBigNum(Number(rewardDetails.operator) / 1_000_000);
|
||||
const selfBondFormated = `${selfBond} NYM`;
|
||||
|
||||
@@ -9,6 +9,7 @@ interface INodeMetricsCardProps {
|
||||
|
||||
export const NodeMetricsCard = (props: INodeMetricsCardProps) => {
|
||||
const { nodeDescription, nodeId } = props;
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Nym node metrics" sx={{ height: "100%" }}>
|
||||
<ExplorerListItem
|
||||
@@ -17,18 +18,22 @@ export const NodeMetricsCard = (props: INodeMetricsCardProps) => {
|
||||
label="Node ID."
|
||||
value={nodeId.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Host"
|
||||
value={nodeDescription.host_information.ip_address.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Version"
|
||||
value={nodeDescription.build_information.build_version}
|
||||
/>
|
||||
{nodeDescription && (
|
||||
<>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Host"
|
||||
value={nodeDescription.host_information.ip_address.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Version"
|
||||
value={nodeDescription.build_information.build_version}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ExplorerListItem row label="Active set Prob." value="High" />
|
||||
</ExplorerCard>
|
||||
);
|
||||
|
||||
@@ -26,9 +26,11 @@ export const NodeProfileCard = (props: INodeProfileCardProps) => {
|
||||
>
|
||||
{"Moniker"}
|
||||
</Typography>
|
||||
<CountryFlag
|
||||
countryCode={nodeDescription.auxiliary_details.location || ""}
|
||||
/>
|
||||
{nodeDescription && (
|
||||
<CountryFlag
|
||||
countryCode={nodeDescription.auxiliary_details.location}
|
||||
/>
|
||||
)}
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
Team of professional validators with best digital solutions. Please
|
||||
visit our Telegram🔹https://t.me/CryptoSailorsAnn🔹
|
||||
|
||||
@@ -8,7 +8,8 @@ interface IQualityIndicatorsCardProps {
|
||||
nodeDescription: NodeDescription;
|
||||
}
|
||||
|
||||
type DelcaredRoleKey = keyof NodeDescription["declared_role"];
|
||||
type NodeDescriptionNotNull = NonNullable<NodeDescription>;
|
||||
type DelcaredRoleKey = keyof NodeDescriptionNotNull["declared_role"];
|
||||
type RoleString = "Entry Node" | "Exit IPR Node" | "Exit NR Node" | "Mix Node";
|
||||
|
||||
const roleMapping: Record<DelcaredRoleKey, RoleString> = {
|
||||
@@ -19,7 +20,7 @@ const roleMapping: Record<DelcaredRoleKey, RoleString> = {
|
||||
};
|
||||
|
||||
function getNodeRoles(
|
||||
declaredRoles: NodeDescription["declared_role"],
|
||||
declaredRoles: NodeDescriptionNotNull["declared_role"],
|
||||
): RoleString[] {
|
||||
const activeRoles = Object.entries(declaredRoles)
|
||||
.filter(([, isActive]) => isActive)
|
||||
@@ -31,6 +32,10 @@ function getNodeRoles(
|
||||
export const QualityIndicatorsCard = (props: IQualityIndicatorsCardProps) => {
|
||||
const { nodeDescription } = props;
|
||||
|
||||
if (!nodeDescription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeRoles = getNodeRoles(nodeDescription.declared_role);
|
||||
const NodeRoles = nodeRoles.map((role) => (
|
||||
<Stack key={role} direction="row" gap={1}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import type { IBondInfo } from "@/app/api";
|
||||
import { NYM_NODE_BONDED } from "@/app/api/urls";
|
||||
import { Search } from "@mui/icons-material";
|
||||
import { Button, CircularProgress, Stack, Typography } from "@mui/material";
|
||||
import type { NymNodeDetails } from "@nymproject/types";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import Input from "../input/Input";
|
||||
@@ -46,8 +46,8 @@ const NodeAndAddressSearch = () => {
|
||||
if (response.ok) {
|
||||
const nodes = await response.json();
|
||||
const matchingNode = nodes.data.find(
|
||||
(node: IBondInfo) =>
|
||||
node.bond_information.node.identity_key === inputValue,
|
||||
(node: NymNodeDetails) =>
|
||||
node.bond_information.identity_key === inputValue,
|
||||
);
|
||||
|
||||
if (matchingNode) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
const OriginalStakeCard = () => {
|
||||
const [origialStake, setOriginalStake] = useState(0);
|
||||
const { nymClient, address } = useNymClient();
|
||||
|
||||
useEffect(() => {
|
||||
const getDelegations = async () => {
|
||||
if (!nymClient || !address) return;
|
||||
|
||||
const delegations = await nymClient?.getDelegatorDelegations({
|
||||
delegator: address,
|
||||
});
|
||||
|
||||
const totaluNYMStake = delegations.delegations.reduce((acc, curr) => {
|
||||
return acc + Number(curr.amount.amount);
|
||||
}, 0);
|
||||
|
||||
setOriginalStake(totaluNYMStake);
|
||||
};
|
||||
getDelegations();
|
||||
}, [address, nymClient]);
|
||||
return (
|
||||
<ExplorerCard label="Original Stake">
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
{`${formatBigNum(origialStake / 1_000_000)} NYM`}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default OriginalStakeCard;
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { Grid2 } from "@mui/material";
|
||||
import OriginalStakeCard from "./OriginalStakeCard";
|
||||
import TotalRewardsCard from "./TotalRewardsCard";
|
||||
import TotalStakeCard from "./TotalStakeCard";
|
||||
|
||||
const OverviewCards = () => {
|
||||
const { address } = useNymClient();
|
||||
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid2 container spacing={3}>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<TotalStakeCard />
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<OriginalStakeCard />
|
||||
</Grid2>
|
||||
<Grid2
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<TotalRewardsCard />
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewCards;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { MoreVert } from "@mui/icons-material";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
|
||||
type StakeAction = "unstake";
|
||||
|
||||
const StakeActions = ({
|
||||
nodeId,
|
||||
onActionSelect,
|
||||
}: {
|
||||
nodeId?: number;
|
||||
onActionSelect: (action: StakeAction) => void;
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleShowMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleActionSelect = (action: StakeAction) => {
|
||||
onActionSelect(action);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleShowMenu} disabled={!nodeId}>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
<Menu
|
||||
elevation={0}
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={() => {
|
||||
handleClose();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
hideBackdrop
|
||||
>
|
||||
<StakeAction
|
||||
actionName="Unstake"
|
||||
onSelect={() => handleActionSelect("unstake")}
|
||||
/>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const StakeAction = ({
|
||||
actionName,
|
||||
disabled,
|
||||
onSelect,
|
||||
}: {
|
||||
actionName: string;
|
||||
disabled?: boolean;
|
||||
onSelect: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<MenuItem
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect();
|
||||
}}
|
||||
>
|
||||
{actionName}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default StakeActions;
|
||||
@@ -0,0 +1,113 @@
|
||||
import SimpleModal from "@/components/modal/SimpleModal";
|
||||
import useGetWalletBalance from "@/hooks/useGetWalletBalance";
|
||||
import { Button, Stack, Typography } from "@mui/material";
|
||||
import { CurrencyFormField } from "@nymproject/react/currency/CurrencyFormField.js";
|
||||
import { IdentityKeyFormField } from "@nymproject/react/mixnodes/IdentityKeyFormField.js";
|
||||
import type { DecCoin } from "@nymproject/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import stakingSchema, { MIN_AMOUNT_TO_DELEGATE } from "./schemas";
|
||||
|
||||
const StakeModal = ({
|
||||
nodeId,
|
||||
identityKey,
|
||||
onStake,
|
||||
onClose,
|
||||
}: {
|
||||
nodeId?: number;
|
||||
identityKey?: string;
|
||||
onStake: ({
|
||||
nodeId,
|
||||
amount,
|
||||
}: {
|
||||
nodeId: number;
|
||||
amount: string;
|
||||
}) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { balance } = useGetWalletBalance();
|
||||
const [amount, setAmount] = useState<DecCoin>({
|
||||
amount: MIN_AMOUNT_TO_DELEGATE,
|
||||
denom: "nym",
|
||||
});
|
||||
const [isValidated, setValidated] = useState<boolean>(false);
|
||||
const [errorAmount, setErrorAmount] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const asyncValidate = async () => {
|
||||
await stakingSchema
|
||||
.parseAsync({ amount: amount.amount, balance, nodeId })
|
||||
.then(() => {
|
||||
setValidated(true);
|
||||
setErrorAmount(undefined);
|
||||
return true;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e.errors);
|
||||
setValidated(false);
|
||||
setErrorAmount(e.errors[0]?.message);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
asyncValidate();
|
||||
}, [amount, balance, nodeId]);
|
||||
|
||||
if (!nodeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleOnClose = () => {
|
||||
setAmount({ amount: MIN_AMOUNT_TO_DELEGATE, denom: "nym" });
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
title="Stake"
|
||||
open={!!identityKey}
|
||||
onClose={handleOnClose}
|
||||
Actions={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => onStake({ nodeId, amount: amount.amount })}
|
||||
fullWidth
|
||||
disabled={!isValidated}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={0.5}>
|
||||
<Typography variant="body5">Address</Typography>
|
||||
<IdentityKeyFormField
|
||||
placeholder="Identity Key"
|
||||
required
|
||||
fullWidth
|
||||
initialValue={identityKey}
|
||||
readOnly
|
||||
showTickOnValid={false}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack spacing={0.5}>
|
||||
<Typography variant="body5">Amount</Typography>
|
||||
<CurrencyFormField
|
||||
placeholder="Amount"
|
||||
showCoinMark={false}
|
||||
required
|
||||
fullWidth
|
||||
autoFocus
|
||||
initialValue={amount.amount}
|
||||
onChanged={setAmount}
|
||||
denom={"nym"}
|
||||
validationError={errorAmount}
|
||||
/>
|
||||
</Stack>
|
||||
<ExplorerListItem row label="Account balance" value={balance} divider />
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StakeModal;
|
||||
@@ -0,0 +1,318 @@
|
||||
"use client";
|
||||
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||
import {
|
||||
type MRT_ColumnDef,
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from "material-react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import CountryFlag from "../countryFlag/CountryFlag";
|
||||
import Loading from "../loading";
|
||||
import InfoModal, { type InfoModalProps } from "../modal/InfoModal";
|
||||
import ConnectWallet from "../wallet/ConnectWallet";
|
||||
import StakeActions from "./StakeActions";
|
||||
import type { MappedNymNode, MappedNymNodes } from "./StakeTableWithAction";
|
||||
import { fee } from "./schemas";
|
||||
|
||||
type DelegationWithNodeDetails = {
|
||||
node: MappedNymNode | undefined;
|
||||
delegation: Delegation;
|
||||
};
|
||||
|
||||
const ColumnHeading = ({
|
||||
children,
|
||||
}: {
|
||||
children: string | React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Typography sx={{ py: 2, textAlign: "center" }} variant="h5">
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const StakeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
const { nymClient, address } = useNymClient();
|
||||
const [delegations, setDelegations] = useState<DelegationWithNodeDetails[]>(
|
||||
[],
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [infoModalProps, setInfoModalProps] = useState<InfoModalProps>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!nymClient || !address) return;
|
||||
|
||||
// Fetch staking data
|
||||
const fetchDelegations = async () => {
|
||||
const data = await nymClient?.getDelegatorDelegations({
|
||||
delegator: address,
|
||||
});
|
||||
return data.delegations;
|
||||
};
|
||||
|
||||
// Combine delegations with node details
|
||||
const combineDelegationsWithNode = (delegations: Delegation[]) => {
|
||||
const delegationsWithNodeDetails = delegations.map((delegation) => {
|
||||
const node = nodes.find((node) => node.nodeId === delegation.node_id);
|
||||
|
||||
return {
|
||||
node,
|
||||
delegation,
|
||||
};
|
||||
});
|
||||
|
||||
return delegationsWithNodeDetails;
|
||||
};
|
||||
|
||||
// Fetch and map delegations
|
||||
const fetchAndMapDelegations = async () => {
|
||||
const delegations = await fetchDelegations();
|
||||
const delegationsWithNodeDetails =
|
||||
combineDelegationsWithNode(delegations);
|
||||
setDelegations(delegationsWithNodeDetails);
|
||||
};
|
||||
|
||||
fetchAndMapDelegations();
|
||||
}, [address, nodes, nymClient]);
|
||||
|
||||
const handleUnstake = useCallback(
|
||||
async (nodeId?: number) => {
|
||||
try {
|
||||
if (!nodeId || !address) {
|
||||
return;
|
||||
}
|
||||
console.log("Unstaking node", nodeId);
|
||||
setIsLoading(true);
|
||||
await nymClient?.undelegate(
|
||||
{
|
||||
nodeId,
|
||||
},
|
||||
fee,
|
||||
`Explorer V2: Unstaking node ${nodeId}`,
|
||||
);
|
||||
setIsLoading(false);
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Success",
|
||||
message: "This operation can take up to one hour to process",
|
||||
onClose: () => setInfoModalProps({ open: false }),
|
||||
});
|
||||
} catch (e) {
|
||||
setInfoModalProps({
|
||||
open: true,
|
||||
title: "Error",
|
||||
message:
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: "An error occurred while unstaking",
|
||||
onClose: () => {
|
||||
setInfoModalProps({ open: false });
|
||||
},
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[address, nymClient],
|
||||
);
|
||||
|
||||
const handleActionSelect = useCallback(
|
||||
(action: string, nodeId: number | undefined) => {
|
||||
switch (action) {
|
||||
case "unstake":
|
||||
handleUnstake(nodeId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[handleUnstake],
|
||||
);
|
||||
|
||||
const columns: MRT_ColumnDef<DelegationWithNodeDetails>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "node",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Node</ColumnHeading>,
|
||||
accessorKey: "bondInformation.node.identity_key",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">
|
||||
{row.original.node?.bondInformation.node_id || "-"}
|
||||
</Typography>
|
||||
<Typography variant="body5">
|
||||
{row.original.node?.bondInformation.node.identity_key || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "location",
|
||||
header: "Location",
|
||||
accessorKey: "location.country_name",
|
||||
Header: <ColumnHeading>Location</ColumnHeading>,
|
||||
Cell: ({ row }) =>
|
||||
row.original.node?.location.two_letter_iso_country_code ? (
|
||||
<Tooltip title={row.original.node.location.country_name}>
|
||||
<Box>
|
||||
<CountryFlag
|
||||
countryCode={
|
||||
row.original.node.location.two_letter_iso_country_code
|
||||
}
|
||||
countryName={
|
||||
row.original.node.location.two_letter_iso_country_code
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "stake",
|
||||
header: "Staked amount",
|
||||
accessorKey: "delegation.amount.amount",
|
||||
Header: <ColumnHeading>Stake</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{formatBigNum(+row.original.delegation.amount.amount / 1_000_000)}{" "}
|
||||
NYM
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "stakeSaturation",
|
||||
header: "Stake saturation",
|
||||
accessorKey: "stakeSaturation",
|
||||
Header: <ColumnHeading>Stake saturation</ColumnHeading>,
|
||||
Cell: () => <Typography variant="body4">Unavailable</Typography>,
|
||||
},
|
||||
{
|
||||
id: "profitMarginPercentage",
|
||||
header: "Profit margin",
|
||||
accessorKey: "profitMarginPercentage",
|
||||
Header: <ColumnHeading>Profit margin</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{row.original.node?.profitMarginPercentage || 0 * 100}%
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
header: "Action",
|
||||
Header: <ColumnHeading>Action</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<StakeActions
|
||||
nodeId={row.original.node?.bondInformation.node_id}
|
||||
onActionSelect={(action) => {
|
||||
handleActionSelect(
|
||||
action,
|
||||
row.original.node?.bondInformation.node_id,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[handleActionSelect],
|
||||
);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data: delegations,
|
||||
enableRowSelection: false,
|
||||
enableColumnOrdering: false,
|
||||
enableColumnActions: false,
|
||||
enableFullScreenToggle: false,
|
||||
enableHiding: false,
|
||||
paginationDisplayMode: "pages",
|
||||
muiPaginationProps: {
|
||||
showRowsPerPage: false,
|
||||
SelectProps: {
|
||||
sx: {
|
||||
fontFamily: "labGrotesqueMono",
|
||||
fontSize: "14px",
|
||||
},
|
||||
},
|
||||
color: "primary",
|
||||
shape: "circular",
|
||||
},
|
||||
sortingFns: {
|
||||
Favorite: () => {
|
||||
// TODO implement sorting by favorite
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: { right: ["Action", "Favorite"] },
|
||||
},
|
||||
|
||||
muiColumnActionsButtonProps: {
|
||||
sx: {
|
||||
color: "red",
|
||||
},
|
||||
size: "small",
|
||||
},
|
||||
muiTablePaperProps: {
|
||||
elevation: 0,
|
||||
},
|
||||
muiTableHeadRowProps: {
|
||||
sx: {
|
||||
bgcolor: "background.paper",
|
||||
},
|
||||
},
|
||||
|
||||
muiTableBodyCellProps: {
|
||||
sx: {
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
muiTableBodyRowProps: ({ row }) => ({
|
||||
onClick: () => {
|
||||
router.push(`/nym-node/${row.original.node?.nodeId || "not-found"}`);
|
||||
},
|
||||
hover: true,
|
||||
sx: {
|
||||
":nth-child(odd)": {
|
||||
bgcolor: "#F3F7FB !important",
|
||||
},
|
||||
":nth-child(even)": {
|
||||
bgcolor: "white !important",
|
||||
},
|
||||
cursor: "pointer",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!nymClient || !address) {
|
||||
return (
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Typography variant="body4">
|
||||
Please connect your wallet to view your stake
|
||||
</Typography>
|
||||
<ConnectWallet hideAddressAndBalance />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading && <Loading />}
|
||||
<InfoModal {...infoModalProps} />
|
||||
<MaterialReactTable table={table} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StakeTable;
|
||||
@@ -0,0 +1,31 @@
|
||||
import getNymNodes from "@/actions/getNymNodes";
|
||||
import type NymNode from "@/app/api/types";
|
||||
import StakeTable from "./StakeTable";
|
||||
|
||||
const mappedNymNodes = (nodes: NymNode[]) =>
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
nodeId: node.node_id,
|
||||
bondInformation: node.bond_information,
|
||||
location: node.location,
|
||||
profitMarginPercentage:
|
||||
+node.rewarding_details.cost_params.profit_margin_percent * 100,
|
||||
description: node.description,
|
||||
};
|
||||
});
|
||||
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
|
||||
export type MappedNymNode = MappedNymNodes[0];
|
||||
|
||||
const StakeTableWithAction = async () => {
|
||||
try {
|
||||
const nodes = await getNymNodes();
|
||||
const data = mappedNymNodes(nodes);
|
||||
return <StakeTable nodes={data} />;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default StakeTableWithAction;
|
||||
@@ -0,0 +1,18 @@
|
||||
import NextEpochTime from "@/components/epochtime/EpochTime";
|
||||
import Grid2 from "@mui/material/Grid2";
|
||||
import SubHeaderRowActions from "./SubHeaderRowActions";
|
||||
|
||||
const SubHeaderRow = () => {
|
||||
return (
|
||||
<Grid2 container spacing={3} alignItems={"center"}>
|
||||
<Grid2 size={{ xs: 12, sm: "grow" }}>
|
||||
<NextEpochTime />
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12, sm: "auto" }}>
|
||||
<SubHeaderRowActions />
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubHeaderRow;
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useNymClient } from "@/hooks/useNymClient";
|
||||
import { Button, Stack } from "@mui/material";
|
||||
|
||||
const SubHeaderRowActions = () => {
|
||||
const { address } = useNymClient();
|
||||
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={3} justifyContent={"end"}>
|
||||
<Button variant="outlined">Redeem all rewards</Button>
|
||||
<Button variant="contained">Stake NYM</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubHeaderRowActions;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
const TotalRewardsCard = () => {
|
||||
return (
|
||||
<ExplorerCard label="Total Rewards">
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
- NYM
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotalRewardsCard;
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { Typography } from "@mui/material";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
const TotalStakeCard = () => {
|
||||
return (
|
||||
<ExplorerCard label="Total Stake">
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: "pine.950", wordWrap: "break-word", maxWidth: "95%" }}
|
||||
>
|
||||
- NYM
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotalStakeCard;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { validateAmount } from "@/utils/currency";
|
||||
import { z } from "zod";
|
||||
|
||||
const MIN_AMOUNT_TO_DELEGATE = "10";
|
||||
const fee = { gas: "1000000", amount: [{ amount: "1000000", denom: "unym" }] };
|
||||
|
||||
const stakingSchema = z
|
||||
.object({
|
||||
nodeId: z.number(),
|
||||
balance: z.string().refine(
|
||||
async (val) => {
|
||||
const num = Number.parseFloat(val);
|
||||
return num > 0;
|
||||
},
|
||||
{
|
||||
message: "Balance must be greater than 0",
|
||||
},
|
||||
),
|
||||
amount: z.string().refine(
|
||||
async (val) => {
|
||||
let isValid = false;
|
||||
|
||||
isValid = await validateAmount(val, MIN_AMOUNT_TO_DELEGATE.toString());
|
||||
isValid =
|
||||
isValid &&
|
||||
Number.parseFloat(val) >= Number.parseFloat(MIN_AMOUNT_TO_DELEGATE);
|
||||
return isValid;
|
||||
},
|
||||
{
|
||||
message: "Amount must be greater than 10 NYM",
|
||||
},
|
||||
),
|
||||
})
|
||||
.extend({})
|
||||
.refine(
|
||||
(data) => {
|
||||
const balance = Number.parseFloat(data.balance);
|
||||
const amount = Number.parseFloat(data.amount);
|
||||
console.log(balance);
|
||||
return balance - amount >= 0;
|
||||
},
|
||||
{
|
||||
message: "Not enough funds",
|
||||
},
|
||||
);
|
||||
|
||||
export default stakingSchema;
|
||||
export { MIN_AMOUNT_TO_DELEGATE, fee };
|
||||
@@ -1,29 +0,0 @@
|
||||
import { StarOutlineRounded, StarRounded } from "@mui/icons-material/";
|
||||
import { Rating } from "@mui/material";
|
||||
|
||||
export const StarRating = ({
|
||||
value,
|
||||
defaultValue,
|
||||
max = 5,
|
||||
size = "medium",
|
||||
}: {
|
||||
value: number;
|
||||
defaultValue?: number;
|
||||
max?: number;
|
||||
size?: "small" | "medium" | "large";
|
||||
}) => {
|
||||
return (
|
||||
<Rating
|
||||
size={size}
|
||||
sx={{ color: "accent.main" }}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
max={max}
|
||||
readOnly
|
||||
icon={<StarRounded fontSize={size} />}
|
||||
emptyIcon={
|
||||
<StarOutlineRounded fontSize={size} sx={{ color: "accent.main" }} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface TwoSidedSwitchProps {
|
||||
leftLabel: string; // Label for the left side
|
||||
rightLabel: string; // Label for the right side
|
||||
onSwitch?: (side: "left" | "right") => void; // Callback when switched
|
||||
}
|
||||
|
||||
const TwoSidedSwitch: React.FC<TwoSidedSwitchProps> = ({
|
||||
leftLabel,
|
||||
rightLabel,
|
||||
onSwitch,
|
||||
}) => {
|
||||
const [selectedSide, setSelectedSide] = useState<"left" | "right">("left");
|
||||
|
||||
const handleSwitch = (side: "left" | "right") => {
|
||||
setSelectedSide(side);
|
||||
if (onSwitch) onSwitch(side);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderRadius: "20px",
|
||||
overflow: "hidden",
|
||||
width: "200px",
|
||||
height: "40px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => handleSwitch("left")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "left" ? "black" : "transparent",
|
||||
color: selectedSide === "left" ? "white" : "black",
|
||||
border: "1px dashed black",
|
||||
borderRight: "none",
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
// "&:hover": {
|
||||
// backgroundColor: selectedSide === "left" ? "black" : "lightgray",
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{leftLabel}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSwitch("right")}
|
||||
sx={{
|
||||
flex: 1,
|
||||
backgroundColor: selectedSide === "right" ? "black" : "transparent",
|
||||
color: selectedSide === "right" ? "white" : "black",
|
||||
border: "1px dashed black",
|
||||
borderLeft: "none",
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
// "&:hover": {
|
||||
// backgroundColor: selectedSide === "right" ? "black" : "lightgray",
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{rightLabel}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoSidedSwitch;
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { Button, type ButtonProps, IconButton, Stack } from "@mui/material";
|
||||
import Cross from "../icons/Cross";
|
||||
import { WalletAddress } from "./WalletAddress";
|
||||
import { WalletBalance } from "./WalletBalance";
|
||||
|
||||
interface ButtonPropsWithOnClick extends ButtonProps {
|
||||
hideAddressAndBalance?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const ConnectWallet = ({ ...buttonProps }: ButtonPropsWithOnClick) => {
|
||||
const { connect, disconnect, address, isWalletConnected } =
|
||||
useChain(COSMOS_KIT_USE_CHAIN);
|
||||
|
||||
const handleConnectWallet = async () => {
|
||||
buttonProps.onClick?.();
|
||||
await connect();
|
||||
};
|
||||
|
||||
const handleDisconnectWallet = async () => {
|
||||
await disconnect();
|
||||
};
|
||||
|
||||
if (isWalletConnected && !buttonProps.hideAddressAndBalance) {
|
||||
return (
|
||||
<Stack direction="row" spacing={3}>
|
||||
<WalletBalance />
|
||||
<WalletAddress address={address} />
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await handleDisconnectWallet();
|
||||
}}
|
||||
>
|
||||
<Cross />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
fullWidth={buttonProps.fullWidth}
|
||||
variant="contained"
|
||||
size={buttonProps.size}
|
||||
onClick={handleConnectWallet}
|
||||
>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectWallet;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Elips } from "@/components/icons/Elips";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
export const trimAddress = (address = "", trimBy = 6) =>
|
||||
`${address.slice(0, trimBy)}...${address.slice(-trimBy)}`;
|
||||
|
||||
export const WalletAddress = ({ address }: { address?: string }) => {
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayAddress = trimAddress(address, 7);
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<Elips />
|
||||
<Typography variant="h5" fontWeight={400}>
|
||||
{displayAddress}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Token } from "@/components/icons/Token";
|
||||
import useGetWalletBalance from "@/hooks/useGetWalletBalance";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
export const WalletBalance = () => {
|
||||
const { formattedBalance } = useGetWalletBalance();
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Token />
|
||||
<Typography variant="h5" fontWeight={400}>
|
||||
{formattedBalance} NYM
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export const COSMOS_KIT_USE_CHAIN =
|
||||
process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || "sandbox";
|
||||
|
||||
export const NYM_MIXNET_CONTRACT =
|
||||
process.env.NYM_MIXNET_CONTRACT ||
|
||||
"n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr";
|
||||
@@ -0,0 +1,46 @@
|
||||
import { COSMOS_KIT_USE_CHAIN } from "@/config";
|
||||
import { unymToNym } from "@/utils/currency";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
const useGetWalletBalance = () => {
|
||||
const [balance, setBalance] = useState<string>("0");
|
||||
const [formattedBalance, setFormattedBalance] = useState<string>("-");
|
||||
const { getCosmWasmClient, address } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
|
||||
const getNYMBalance = useCallback(
|
||||
async (address: string) => {
|
||||
const account = await getCosmWasmClient();
|
||||
const uNYMBalance = await account.getBalance(address, "unym");
|
||||
const NYMBalance = unymToNym(uNYMBalance.amount);
|
||||
if (!NYMBalance) {
|
||||
return undefined;
|
||||
}
|
||||
const formattedBalance = Intl.NumberFormat().format(+NYMBalance);
|
||||
return {
|
||||
NYMBalance,
|
||||
formattedBalance,
|
||||
};
|
||||
},
|
||||
[getCosmWasmClient],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
getNYMBalance(address)
|
||||
.then((balance) => {
|
||||
setFormattedBalance(balance?.formattedBalance || "-");
|
||||
setBalance(balance?.NYMBalance || "0");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to get balance", e);
|
||||
});
|
||||
}, [address, getNYMBalance]);
|
||||
|
||||
return { balance, formattedBalance };
|
||||
};
|
||||
|
||||
export default useGetWalletBalance;
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "@/config";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import { contracts } from "@nymproject/contract-clients";
|
||||
import type {
|
||||
MixnetClient,
|
||||
MixnetQueryClient,
|
||||
} from "@nymproject/contract-clients/Mixnet.client";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useNymClient = () => {
|
||||
const [nymClient, setNymClient] = useState<MixnetClient>();
|
||||
const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>();
|
||||
|
||||
const { address, getCosmWasmClient, getSigningCosmWasmClient } =
|
||||
useChain(COSMOS_KIT_USE_CHAIN);
|
||||
|
||||
useEffect(() => {
|
||||
if (address) {
|
||||
const init = async () => {
|
||||
const cosmWasmSigningClient = await getSigningCosmWasmClient();
|
||||
const cosmWasmClient = await getCosmWasmClient();
|
||||
|
||||
const client = new contracts.Mixnet.MixnetClient(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cosmWasmSigningClient as any,
|
||||
address,
|
||||
NYM_MIXNET_CONTRACT,
|
||||
);
|
||||
|
||||
const queryClient = new contracts.Mixnet.MixnetQueryClient(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cosmWasmClient as any,
|
||||
NYM_MIXNET_CONTRACT,
|
||||
);
|
||||
|
||||
setNymClient(client);
|
||||
setNymQueryClient(queryClient);
|
||||
};
|
||||
|
||||
init();
|
||||
}
|
||||
}, [address, getCosmWasmClient, getSigningCosmWasmClient]);
|
||||
|
||||
return { nymClient, nymQueryClient, address };
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import type { AssetList, Chain } from "@chain-registry/types";
|
||||
import { wallets as keplr } from "@cosmos-kit/keplr-extension";
|
||||
import { ChainProvider } from "@cosmos-kit/react";
|
||||
import { assets, chains } from "chain-registry";
|
||||
import React from "react";
|
||||
|
||||
const nymSandbox: Chain = {
|
||||
chain_type: "cosmos",
|
||||
chain_name: "sandbox",
|
||||
chain_id: "sandbox",
|
||||
bech32_prefix: "n",
|
||||
network_type: "devnet",
|
||||
pretty_name: "Nym Sandbox",
|
||||
status: "live",
|
||||
slip44: 118,
|
||||
apis: {
|
||||
rpc: [
|
||||
{
|
||||
address: "https://rpc.sandbox.nymtech.net",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const nymSandboxAssets = {
|
||||
chain_name: "sandbox",
|
||||
assets: [
|
||||
{
|
||||
name: "Nym",
|
||||
base: "unym",
|
||||
symbol: "NYM",
|
||||
display: "NYM",
|
||||
denom_units: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const CosmosKitProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
// Only use the nyx chains
|
||||
const chainsWithNyx = React.useMemo(() => {
|
||||
const nyx = chains.find((chain) => chain.chain_id === "nyx");
|
||||
|
||||
return nyx ? [nymSandbox, nyx] : [nymSandbox];
|
||||
}, []);
|
||||
|
||||
// Only use the nyx assets
|
||||
const assetsWithNyx = React.useMemo(() => {
|
||||
const nyx = assets.find((asset) => asset.chain_name === "nyx");
|
||||
|
||||
return nyx ? [nymSandboxAssets, nyx] : [nymSandboxAssets];
|
||||
}, []) as AssetList[];
|
||||
|
||||
return (
|
||||
<ChainProvider
|
||||
chains={chainsWithNyx}
|
||||
assetLists={assetsWithNyx}
|
||||
wallets={[...keplr]}
|
||||
>
|
||||
{children}
|
||||
</ChainProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CosmosKitProvider;
|
||||
@@ -1,7 +1,12 @@
|
||||
import CosmosKitProvider from "./CosmosKitProvider";
|
||||
import ThemeProvider from "./ThemeProvider";
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
return <ThemeProvider>{children}</ThemeProvider>;
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<CosmosKitProvider>{children}</CosmosKitProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Providers;
|
||||
|
||||
@@ -168,11 +168,7 @@ const getTheme = (mode: "light" | "dark"): ThemeOptions => {
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: "32px",
|
||||
},
|
||||
},
|
||||
styleOverrides: {},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { DecCoin } from "@nymproject/types";
|
||||
import Big from "big.js";
|
||||
|
||||
export const isValidRawCoin = (rawAmount: string): boolean => {
|
||||
const amountFloat = Number.parseFloat(rawAmount);
|
||||
|
||||
// if value is a decimal it cannot have more than 6 decimal places
|
||||
if (amountFloat % 1 > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/naming-convention
|
||||
const [_, numsAfterDecimal] = rawAmount.split(".");
|
||||
|
||||
if (+numsAfterDecimal.length > 6) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// it cannot be larger than the total supply
|
||||
if (amountFloat > 1_000_000_000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// it can't be lower than one micro coin
|
||||
if (amountFloat < 0.000001) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const unymToNym = (unym: string | number | Big, dp = 4) => {
|
||||
try {
|
||||
const nym = Big(unym).div(1_000_000).toFixed(dp);
|
||||
return nym;
|
||||
} catch (e: unknown) {
|
||||
console.warn(`${unym} not a valid decimal number: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateAmount = async (
|
||||
majorAmountAsString: DecCoin["amount"],
|
||||
minimumAmountAsString: DecCoin["amount"],
|
||||
): Promise<boolean> => {
|
||||
// tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc
|
||||
if (!Number(majorAmountAsString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidRawCoin(majorAmountAsString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const majorValueFloat = Number.parseInt(majorAmountAsString, Number(10));
|
||||
|
||||
return majorValueFloat >= Number.parseInt(minimumAmountAsString, Number(10));
|
||||
};
|
||||
@@ -20,7 +20,8 @@
|
||||
],
|
||||
"paths": {
|
||||
"@/public/*": ["./public/*"],
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@assets/*": ["../assets/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useChain } from '@cosmos-kit/react';
|
||||
import { contracts } from '@nymproject/contract-clients';
|
||||
import { MixnetClient, MixnetQueryClient } from '@nymproject/contract-clients/Mixnet.client';
|
||||
import type { MixnetClient, MixnetQueryClient } from '@nymproject/contract-clients/Mixnet.client';
|
||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from '@src/api/constants';
|
||||
|
||||
export const useNymClient = (address?: string) => {
|
||||
@@ -15,8 +15,9 @@ export const useNymClient = (address?: string) => {
|
||||
const init = async () => {
|
||||
const cosmWasmSigningClient = await getSigningCosmWasmClient();
|
||||
const cosmWasmClient = await getCosmWasmClient();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const client = new contracts.Mixnet.MixnetClient(cosmWasmSigningClient as any, address, NYM_MIXNET_CONTRACT);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const queryClient = new contracts.Mixnet.MixnetQueryClient(cosmWasmClient as any, NYM_MIXNET_CONTRACT);
|
||||
|
||||
setNymClient(client);
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"notistack": "^2.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"qrcode.react": "^4.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
@@ -125,4 +125,4 @@
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"private": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AppContext } from 'src/context';
|
||||
import { Box, Stack, SxProps } from '@mui/material';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { ModalListItem } from '../Modals/ModalListItem';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
@@ -57,7 +57,7 @@ export const ReceiveModal = ({
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
{clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />}
|
||||
{clientDetails && <QRCodeSVG data-testid="qr-code" value={clientDetails?.client_address} />}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user