Clean up
remove old explorer references
This commit is contained in:
@@ -5,7 +5,6 @@ on:
|
||||
paths:
|
||||
- 'clients/**'
|
||||
- 'common/**'
|
||||
- 'explorer-api/**'
|
||||
- 'gateway/**'
|
||||
- 'integrations/**'
|
||||
- 'nym-api/**'
|
||||
|
||||
@@ -19,7 +19,11 @@ jobs:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: arc-ubuntu-22.04
|
||||
matrix:
|
||||
include:
|
||||
- os: arc-ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
@@ -66,7 +70,6 @@ jobs:
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
target/release/explorer-api
|
||||
target/release/nym-client
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
@@ -82,7 +85,6 @@ jobs:
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/explorer-api
|
||||
target/release/nym-client
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
|
||||
@@ -23,7 +23,6 @@ fn main() {
|
||||
"REWARDING_VALIDATOR_ADDRESS",
|
||||
"NYM_API",
|
||||
"NYXD_WS",
|
||||
"EXPLORER_API",
|
||||
"NYM_VPN_API",
|
||||
];
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772
|
||||
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
|
||||
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
|
||||
@@ -123,7 +122,6 @@ pub fn export_to_env() {
|
||||
set_var_to_default(var_names::NYXD, NYXD_URL);
|
||||
set_var_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_to_default(var_names::EXPLORER_API, EXPLORER_API);
|
||||
set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
|
||||
}
|
||||
@@ -165,6 +163,5 @@ pub fn export_to_env_if_not_set() {
|
||||
set_var_conditionally_to_default(var_names::NYXD, NYXD_URL);
|
||||
set_var_conditionally_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API);
|
||||
set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ pub struct NymNetworkDetails {
|
||||
pub chain_details: ChainDetails,
|
||||
pub endpoints: Vec<ValidatorDetails>,
|
||||
pub contracts: NymContracts,
|
||||
pub explorer_api: Option<String>,
|
||||
pub nym_vpn_api_url: Option<String>,
|
||||
}
|
||||
|
||||
@@ -65,7 +64,6 @@ impl NymNetworkDetails {
|
||||
},
|
||||
endpoints: Default::default(),
|
||||
contracts: Default::default(),
|
||||
explorer_api: Default::default(),
|
||||
nym_vpn_api_url: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -124,7 +122,6 @@ impl NymNetworkDetails {
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
|
||||
.with_explorer_api(get_optional_env(var_names::EXPLORER_API))
|
||||
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
|
||||
}
|
||||
|
||||
@@ -152,7 +149,6 @@ impl NymNetworkDetails {
|
||||
mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
),
|
||||
},
|
||||
explorer_api: parse_optional_str(mainnet::EXPLORER_API),
|
||||
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
|
||||
}
|
||||
}
|
||||
@@ -193,7 +189,6 @@ impl NymNetworkDetails {
|
||||
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
||||
set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address);
|
||||
|
||||
set_optional_var(var_names::EXPLORER_API, self.explorer_api);
|
||||
set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url);
|
||||
}
|
||||
|
||||
@@ -297,12 +292,6 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_explorer_api<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.explorer_api = endpoint.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.nym_vpn_api_url = endpoint.map(Into::into);
|
||||
|
||||
@@ -22,7 +22,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
pub const NYM_API: &str = "NYM_API";
|
||||
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
|
||||
pub const EXPLORER_API: &str = "EXPLORER_API";
|
||||
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
|
||||
pub const NYM_VPN_API: &str = "NYM_VPN_API";
|
||||
pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER";
|
||||
|
||||
@@ -18,7 +18,6 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07
|
||||
MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9
|
||||
|
||||
EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/
|
||||
NYXD=https://rpc.canary-validator.performance.nymte.ch
|
||||
NYM_API=https://canary-api.performance.nymte.ch/api/
|
||||
NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket
|
||||
|
||||
@@ -23,5 +23,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
|
||||
NYXD="https://rpc.nymtech.net"
|
||||
NYM_API="http://127.0.0.1:8000"
|
||||
NYXD_WS="wss://rpc.nymtech.net/websocket"
|
||||
EXPLORER_API="https://explorer.nymtech.net/api/"
|
||||
NYM_VPN_API="https://nymvpn.com/api"
|
||||
|
||||
@@ -24,5 +24,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS=https://mainnet-stats.nymte.ch:8090
|
||||
NYXD=https://rpc.nymtech.net
|
||||
NYM_API=https://validator.nymtech.net/api/
|
||||
NYXD_WS=wss://rpc.nymtech.net/websocket
|
||||
EXPLORER_API=https://explorer.nymtech.net/api/
|
||||
NYM_VPN_API=https://nymvpn.com/api/
|
||||
|
||||
@@ -18,7 +18,6 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq683
|
||||
VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj
|
||||
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
|
||||
|
||||
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/
|
||||
NYXD=https://rpc.qa-validator.qa.nymte.ch
|
||||
NYXD_WS=wss://rpc.qa-validator.qa.nymte.ch/websocket
|
||||
NYM_API=https://qa-nym-api.qa.nymte.ch/api/
|
||||
|
||||
@@ -19,7 +19,6 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865
|
||||
ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9
|
||||
|
||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
||||
EXPLORER_API=https://sandbox-explorer.nymtech.net/api/
|
||||
NYXD=https://rpc.sandbox.nymtech.net
|
||||
NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket
|
||||
NYM_API=https://sandbox-nym-api1.nymtech.net/api/
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals"]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,36 +0,0 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Navbar } from './components/Nav/Navbar'
|
||||
import { Providers } from './providers'
|
||||
|
||||
const App = ({ children }: { children: React.ReactNode }) => (
|
||||
<Providers>
|
||||
<Navbar>{children}</Navbar>
|
||||
</Providers>
|
||||
)
|
||||
|
||||
export { App }
|
||||
@@ -1,407 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {Alert, AlertTitle, Box, Button, Chip, CircularProgress, Grid, Tooltip, Typography} from '@mui/material'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useMainContext } from '@/app/context/main'
|
||||
import { Title } from '@/app/components/Title'
|
||||
import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from "material-react-table";
|
||||
import { useMemo } from "react";
|
||||
import { humanReadableCurrencyToString } from "@/app/utils/currency";
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import { PieChart } from '@mui/x-charts/PieChart';
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useIsMobile } from "@/app/hooks";
|
||||
import { StyledLink } from "@/app/components";
|
||||
|
||||
const AccumulatedRewards = ({account}: { account?: any}) => {
|
||||
const columns = useMemo<
|
||||
MRT_ColumnDef<any>[]
|
||||
>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'accumulated-rewards-data',
|
||||
header: 'Accumulated Rewards Data',
|
||||
columns: [
|
||||
{
|
||||
id: 'node_id',
|
||||
accessorKey: 'node_id',
|
||||
header: 'Node ID',
|
||||
size: 150,
|
||||
Cell: ({ row }) => (<StyledLink
|
||||
color="text.primary"
|
||||
to={`/network-components/nodes/${row.original.node_id}`}>
|
||||
{row.original.node_id}
|
||||
</StyledLink>),
|
||||
},
|
||||
{
|
||||
id: 'node_still_fully_bonded',
|
||||
accessorKey: 'node_still_fully_bonded',
|
||||
header: 'Node still bonded?',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<>{row.original.node_still_fully_bonded ? <CheckCircleOutlineIcon/> :
|
||||
<Typography fontSize="inherit" alignItems="center" display="flex" sx={{ color: theme => theme.palette.warning.main }}>
|
||||
<WarningAmberIcon sx={{ mr: 1 }}/>
|
||||
Unbonded
|
||||
</Typography>}</>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'amount_staked',
|
||||
accessorKey: 'amount_staked',
|
||||
header: 'Amount',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<>{humanReadableCurrencyToString(row.original.amount_staked)}</>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'rewards',
|
||||
accessorKey: 'rewards',
|
||||
header: 'Rewards',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<Typography fontSize="inherit" color="success.main">{humanReadableCurrencyToString(row.original.rewards)}</Typography>
|
||||
)
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data: account?.accumulated_rewards || [],
|
||||
enableFullScreenToggle: false,
|
||||
})
|
||||
|
||||
return (<MaterialReactTable table={table} />);
|
||||
}
|
||||
|
||||
const DelegationHistory = ({account}: { account?: any}) => {
|
||||
const columns = useMemo<
|
||||
MRT_ColumnDef<any>[]
|
||||
>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'delegation-history-data',
|
||||
header: 'Delegation History',
|
||||
columns: [
|
||||
{
|
||||
id: 'node_id',
|
||||
accessorKey: 'node_id',
|
||||
header: 'Node ID',
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
id: 'delegated',
|
||||
accessorKey: 'delegated',
|
||||
header: 'Amount',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<>{humanReadableCurrencyToString(row.original.delegated)}</>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'height',
|
||||
accessorKey: 'height',
|
||||
header: 'Delegated at height',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<>{row.original.height}</>
|
||||
)
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data: account?.delegations || [],
|
||||
enableFullScreenToggle: false,
|
||||
})
|
||||
|
||||
return (<MaterialReactTable table={table} />);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows account details
|
||||
*/
|
||||
const PageAccountWithState = ({ account }: {
|
||||
account?: any;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const pieChartData = React.useMemo(() => {
|
||||
if(!account) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
|
||||
const nymBalance = Number.parseFloat(account.balances.find((b: any) => b.denom === "unym")?.amount || "0") / 1e6;
|
||||
|
||||
if(nymBalance > 0) {
|
||||
parts.push({label: "Spendable", value: nymBalance, color: theme.palette.primary.main});
|
||||
}
|
||||
|
||||
if(account.vesting_account) {
|
||||
if (`${account.vesting_account.locked?.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.vesting_account.locked.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Vesting locked",
|
||||
value,
|
||||
color: 'red'
|
||||
});
|
||||
}
|
||||
}
|
||||
if (`${account.vesting_account.spendable?.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.vesting_account.spendable.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Vesting spendable",
|
||||
value,
|
||||
color: theme.palette.primary.light
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (account.claimable_rewards &&`${account.claimable_rewards.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.claimable_rewards.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Claimable delegation rewards",
|
||||
value,
|
||||
color: theme.palette.success.light
|
||||
});
|
||||
}
|
||||
}
|
||||
if (account.operator_rewards && `${account.operator_rewards.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.operator_rewards.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Claimable operator rewards",
|
||||
value,
|
||||
color: theme.palette.success.dark
|
||||
});
|
||||
}
|
||||
}
|
||||
if (account.total_delegations && `${account.total_delegations.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.total_delegations.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Total delegations",
|
||||
value,
|
||||
color: '#888'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}, [account]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
<Box overflow="scroll">
|
||||
<Title text={`Account ${account.address}`} />
|
||||
</Box>
|
||||
|
||||
<Box mt={4} sx={{ maxWidth: "600px" }}>
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: pieChartData,
|
||||
innerRadius: 40,
|
||||
outerRadius: 80,
|
||||
cy: isMobile ? 200 : undefined,
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
slotProps={isMobile ? {
|
||||
legend: { position: { vertical: "top", horizontal: "right" } }
|
||||
} : undefined}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box mt={4}>
|
||||
<TableContainer component={Paper} sx={{ maxWidth: "400px" }}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow sx={{ color: theme => theme.palette.primary.main }}>
|
||||
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
|
||||
<strong>Spendable Balance</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{account.balances.map((b: any) => (<strong key={`balance-${b.denom}`}>{humanReadableCurrencyToString(b)}<br/></strong>))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row">
|
||||
Total delegations
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{humanReadableCurrencyToString(account.total_delegations)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{account.claimable_rewards && <TableRow sx={{ color: theme => theme.palette.success.light }}>
|
||||
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
|
||||
Claimable delegation rewards
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.claimable_rewards)}
|
||||
</TableCell>
|
||||
</TableRow>}
|
||||
{account.operator_rewards && `${account.operator_rewards.amount}` !== "0" && <TableRow sx={{ color: theme => theme.palette.success.light }}>
|
||||
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
|
||||
Claimable operator rewards
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.operator_rewards)}
|
||||
</TableCell>
|
||||
</TableRow>}
|
||||
{account.vesting_account && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" colSpan={2}>
|
||||
Vesting account
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{`${account.vesting_account.locked.amount}` !== "0" &&
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
|
||||
Locked
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.vesting_account.locked)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{`${account.vesting_account.vested.amount}` !== "0" &&
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
|
||||
Vested
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.vesting_account.vested)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{`${account.vesting_account.vesting.amount}` !== "0" &&
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
|
||||
Vesting
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.vesting_account.vesting)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{`${account.vesting_account.spendable.amount}` !== "0" &&
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
|
||||
Spendable
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.vesting_account.spendable)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
|
||||
<h3>Total value</h3>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
<h3>{humanReadableCurrencyToString(account.total_value)}</h3>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<AccumulatedRewards account={account}/>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DelegationHistory account={account}/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard component to handle loading and not found states
|
||||
*/
|
||||
const PageAccountDetailGuard = ({ account } : { account: string }) => {
|
||||
const [accountDetails, setAccountDetails] = React.useState<any>();
|
||||
const [isLoading, setLoading] = React.useState<boolean>(true);
|
||||
const [error, setError] = React.useState<string>();
|
||||
const { fetchAccountById } = useMainContext()
|
||||
const { id } = useParams()
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
if(typeof(id) === "string") {
|
||||
try {
|
||||
const res = await fetchAccountById(account);
|
||||
setAccountDetails(res);
|
||||
} catch(e: any) {
|
||||
setError(e.message);
|
||||
}
|
||||
finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [id])
|
||||
|
||||
if (isLoading) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Account not found</AlertTitle>
|
||||
Sorry, we could not find the account <code>{id || ''}</code>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return <PageAccountWithState account={accountDetails} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the account details based on the `id` in the address URL
|
||||
*/
|
||||
const PageAccountDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
if (!id || typeof id !== 'string') {
|
||||
return (
|
||||
<Alert severity="error">Oh no! Could not find that account</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageAccountDetailGuard account={id} />
|
||||
)
|
||||
}
|
||||
|
||||
export default PageAccountDetail
|
||||
@@ -1,39 +0,0 @@
|
||||
// master APIs
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_EXPLORER_API_URL || 'https://explorer.nymtech.net/api/v1';
|
||||
export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https://validator.nymtech.net';
|
||||
|
||||
export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net';
|
||||
|
||||
export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net';
|
||||
export const BLOCK_EXPLORER_BASE_URL = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru';
|
||||
|
||||
// specific API routes
|
||||
export const OVERVIEW_API = `${API_BASE_URL}/overview`;
|
||||
export const MIXNODE_PING = `${API_BASE_URL}/ping`;
|
||||
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
|
||||
export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
|
||||
export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`;
|
||||
export const BLOCK_API = `${NYX_RPC_BASE_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
||||
export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this
|
||||
export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`;
|
||||
export const TEMP_UNSTABLE_NYM_NODES = `${API_BASE_URL}/tmp/unstable/nym-nodes`;
|
||||
export const TEMP_UNSTABLE_ACCOUNT = `${API_BASE_URL}/tmp/unstable/account`;
|
||||
export const NYM_API_NODE_UPTIME = `${NYM_API_BASE_URL}/api/v1/nym-nodes/uptime-history`;
|
||||
export const NYM_API_NODE_PERFORMANCE = `${NYM_API_BASE_URL}/api/v1/nym-nodes/performance-history`;
|
||||
|
||||
export const LEGACY_MIXNODES_API = `${API_BASE_URL}/tmp/unstable/legacy-mixnode-bonds`;
|
||||
export const LEGACY_GATEWAYS_API = `${API_BASE_URL}/tmp/unstable/legacy-gateway-bonds`;
|
||||
|
||||
// errors
|
||||
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
|
||||
|
||||
export const NYM_WEBSITE = 'https://nymtech.net';
|
||||
|
||||
export const EXPLORER_FOR_ACCOUNTS = ''; // set to empty to use this Nym Explorer and NOT an external one
|
||||
|
||||
export const NYM_MIXNET_CONTRACT =
|
||||
process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr';
|
||||
export const COSMOS_KIT_USE_CHAIN = process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || 'sandbox';
|
||||
export const WALLET_CONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || '';
|
||||
@@ -1,217 +0,0 @@
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import {
|
||||
API_BASE_URL,
|
||||
BLOCK_API,
|
||||
COUNTRY_DATA_API,
|
||||
UPTIME_STORY_API_GATEWAY,
|
||||
MIXNODE_API,
|
||||
MIXNODE_PING,
|
||||
MIXNODES_API,
|
||||
OVERVIEW_API,
|
||||
UPTIME_STORY_API,
|
||||
VALIDATORS_API,
|
||||
SERVICE_PROVIDERS,
|
||||
TEMP_UNSTABLE_NYM_NODES,
|
||||
NYM_API_NODE_UPTIME,
|
||||
NYM_API_NODE_PERFORMANCE,
|
||||
TEMP_UNSTABLE_ACCOUNT,
|
||||
LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API,
|
||||
} from './constants';
|
||||
|
||||
import {
|
||||
CountryDataResponse,
|
||||
DelegationsResponse,
|
||||
UniqDelegationsResponse,
|
||||
GatewayReportResponse,
|
||||
UptimeStoryResponse,
|
||||
MixNodeDescriptionResponse,
|
||||
MixNodeResponse,
|
||||
MixNodeResponseItem,
|
||||
MixnodeStatus,
|
||||
MixNodeEconomicDynamicsStatsResponse,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
SummaryOverviewResponse,
|
||||
ValidatorsResponse,
|
||||
Environment,
|
||||
GatewayBondAnnotated,
|
||||
GatewayBond,
|
||||
DirectoryServiceProvider,
|
||||
LocatedGateway,
|
||||
} from '../typeDefs/explorer-api';
|
||||
|
||||
function getFromCache(key: string) {
|
||||
const ts = Number(localStorage.getItem('ts'));
|
||||
const hasExpired = Date.now() - ts > 5000;
|
||||
const curr = localStorage.getItem(key);
|
||||
if (curr && !hasExpired) {
|
||||
return JSON.parse(curr);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function storeInCache(key: string, data: any) {
|
||||
localStorage.setItem(key, data);
|
||||
localStorage.setItem('ts', Date.now().toString());
|
||||
}
|
||||
|
||||
export class Api {
|
||||
static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => {
|
||||
const cache = getFromCache('overview-summary');
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
const res = await fetch(`${OVERVIEW_API}/summary`);
|
||||
const json: SummaryOverviewResponse = await res.json();
|
||||
|
||||
if (json.nymnodes?.roles) {
|
||||
json.mixnodes.count += json.nymnodes.roles.mixnode;
|
||||
json.gateways.count += json.nymnodes.roles.entry;
|
||||
json.gateways.count += Math.max(json.nymnodes.roles.exit_ipr, json.nymnodes.roles.exit_nr);
|
||||
}
|
||||
|
||||
storeInCache('overview-summary', JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodes = async (): Promise<MixNodeResponse> => {
|
||||
const cachedMixnodes = getFromCache('mixnodes');
|
||||
if (cachedMixnodes) {
|
||||
return cachedMixnodes;
|
||||
}
|
||||
|
||||
const res = await fetch(LEGACY_MIXNODES_API);
|
||||
const json = await res.json();
|
||||
storeInCache('mixnodes', JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => {
|
||||
const cachedMixnodes = getFromCache(`mixnodes-${status}`);
|
||||
if (cachedMixnodes) {
|
||||
return cachedMixnodes;
|
||||
}
|
||||
const res = await fetch(`${MIXNODES_API}/active-set/${status}`);
|
||||
const json = await res.json();
|
||||
storeInCache(`mixnodes-${status}`, JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => {
|
||||
const response = await fetch(`${MIXNODE_API}/${id}`);
|
||||
|
||||
// when the mixnode is not found, returned undefined
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
static fetchGateways = async (): Promise<LocatedGateway[]> => {
|
||||
// const res = await fetch(GATEWAYS_API);
|
||||
// const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
|
||||
// const res2 = await fetch(GATEWAYS_EXPLORER_API);
|
||||
// const locatedGateways: LocatedGateway[] = await res2.json();
|
||||
// const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner');
|
||||
// return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
|
||||
// ...gateway_bond,
|
||||
// node_performance,
|
||||
// location: locatedGatewaysByOwner[gateway_bond.owner]?.location,
|
||||
// }));
|
||||
|
||||
const res = await fetch(LEGACY_GATEWAYS_API);
|
||||
const locatedGateways: LocatedGateway[] = await res.json();
|
||||
return locatedGateways;
|
||||
};
|
||||
|
||||
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json();
|
||||
|
||||
static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json();
|
||||
|
||||
static fetchValidators = async (): Promise<ValidatorsResponse> => {
|
||||
const res = await fetch(VALIDATORS_API);
|
||||
const json = await res.json();
|
||||
return json.result;
|
||||
};
|
||||
|
||||
static fetchBlock = async (): Promise<number> => {
|
||||
const res = await fetch(BLOCK_API);
|
||||
const json = await res.json();
|
||||
const { height } = json.result.block.header;
|
||||
return height;
|
||||
};
|
||||
|
||||
static fetchCountryData = async (): Promise<CountryDataResponse> => {
|
||||
const result: CountryDataResponse = {};
|
||||
const res = await fetch(COUNTRY_DATA_API);
|
||||
const json = await res.json();
|
||||
Object.keys(json).forEach((ISO3) => {
|
||||
result[ISO3] = { ISO3, nodes: json[ISO3] };
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/delegations`)).json();
|
||||
|
||||
static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json();
|
||||
|
||||
static fetchStatsById = async (id: string): Promise<StatsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/stats`)).json();
|
||||
|
||||
static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
|
||||
|
||||
static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json();
|
||||
|
||||
static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json();
|
||||
|
||||
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
|
||||
|
||||
static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => {
|
||||
const res = await fetch(SERVICE_PROVIDERS);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchNodes = async () => {
|
||||
const res = await fetch(TEMP_UNSTABLE_NYM_NODES);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
static fetchNodeById = async (id: number) => {
|
||||
const res = await fetch(`${TEMP_UNSTABLE_NYM_NODES}/${id}`);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
static fetchNymNodeUptimeHistoryById = async (id: number | string) => {
|
||||
const res = await fetch(`${NYM_API_NODE_UPTIME}/${id}`)
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
static fetchNymNodePerformanceById = async (id: number | string) => {
|
||||
const res = await fetch(`${NYM_API_NODE_PERFORMANCE}/${id}`)
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
static fetchAccountById = async (id: string) => {
|
||||
const res = await fetch(`${TEMP_UNSTABLE_ACCOUNT}/${id}`);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
export const getEnvironment = (): Environment => {
|
||||
const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env;
|
||||
return matchEnv('sandbox') || matchEnv('qa') || 'mainnet';
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
||||
import { Typography } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
|
||||
export const ComponentError: FCWithChildren<{ text: string }> = ({ text }) => (
|
||||
<Typography
|
||||
sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }}
|
||||
variant="body1"
|
||||
data-testid="delegation-total-amount"
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Card, CardHeader, CardContent, Typography } from '@mui/material'
|
||||
import React, { ReactEventHandler } from 'react'
|
||||
|
||||
type ContentCardProps = {
|
||||
title?: React.ReactNode
|
||||
subtitle?: string
|
||||
Icon?: React.ReactNode
|
||||
Action?: React.ReactNode
|
||||
errorMsg?: string
|
||||
onClick?: ReactEventHandler
|
||||
}
|
||||
|
||||
export const ContentCard: FCWithChildren<ContentCardProps> = ({
|
||||
title,
|
||||
Icon,
|
||||
Action,
|
||||
subtitle,
|
||||
errorMsg,
|
||||
children,
|
||||
onClick,
|
||||
}) => (
|
||||
<Card onClick={onClick} sx={{ height: '100%' }}>
|
||||
{title && (
|
||||
<CardHeader
|
||||
title={title || ''}
|
||||
avatar={Icon}
|
||||
action={Action}
|
||||
subheader={subtitle}
|
||||
/>
|
||||
)}
|
||||
{children && <CardContent>{children}</CardContent>}
|
||||
{errorMsg && (
|
||||
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
|
||||
{errorMsg}
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip'
|
||||
|
||||
export const CustomColumnHeading: FCWithChildren<{
|
||||
headingTitle: string
|
||||
tooltipInfo?: string
|
||||
}> = ({ headingTitle, tooltipInfo }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box alignItems="center" display="flex">
|
||||
{tooltipInfo && (
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={headingTitle}
|
||||
placement="top-start"
|
||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
)}
|
||||
<Typography variant="body2" fontWeight={600} data-testid={headingTitle}>
|
||||
{headingTitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Button,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
open: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose?: () => void;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subTitle?: React.ReactNode | string;
|
||||
confirmButton: React.ReactNode | string;
|
||||
disabled?: boolean;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
open,
|
||||
onConfirm,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subTitle,
|
||||
confirmButton,
|
||||
disabled,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ConfirmationModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
{title}
|
||||
{subTitle &&
|
||||
(typeof subTitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} color="grey">
|
||||
{subTitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subTitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
const ConfirmButton =
|
||||
typeof confirmButton === 'string' ? (
|
||||
<Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}>
|
||||
<Typography variant="button" fontSize="large">
|
||||
{confirmButton}
|
||||
</Typography>
|
||||
</Button>
|
||||
) : (
|
||||
confirmButton
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Button, IconButton } from '@mui/material'
|
||||
import { SxProps } from '@mui/system'
|
||||
import { useIsMobile } from '@/app/hooks'
|
||||
import { DelegateIcon } from '@/app/icons/DelevateSVG'
|
||||
|
||||
export const DelegateIconButton: FCWithChildren<{
|
||||
size?: 'small' | 'medium'
|
||||
disabled?: boolean
|
||||
tooltip?: React.ReactNode
|
||||
sx?: SxProps
|
||||
onDelegate: () => void
|
||||
}> = ({ onDelegate, sx, disabled, size = 'medium' }) => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const handleOnDelegate = () => {
|
||||
onDelegate()
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<IconButton size="small" disabled={disabled} onClick={handleOnDelegate}>
|
||||
<DelegateIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
onClick={handleOnDelegate}
|
||||
sx={sx}
|
||||
>
|
||||
Delegate
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Box, SxProps } from '@mui/material'
|
||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'
|
||||
import { CurrencyDenom, DecCoin } from '@nymproject/types'
|
||||
import { useWalletContext } from '@/app/context/wallet'
|
||||
import { urls } from '@/app/utils'
|
||||
import { useDelegationsContext } from '@/app/context/delegations'
|
||||
import { validateAmount } from '@/app/utils/currency'
|
||||
import { SimpleModal } from './SimpleModal'
|
||||
import { ModalListItem } from './ModalListItem'
|
||||
import { DelegationModalProps } from './DelegationModal'
|
||||
|
||||
const MIN_AMOUNT_TO_DELEGATE = 10
|
||||
|
||||
type Props = {
|
||||
mixId: number
|
||||
identityKey: string
|
||||
header?: string
|
||||
buttonText?: string
|
||||
rewardInterval?: string
|
||||
estimatedReward?: number
|
||||
profitMarginPercentage?: string | null
|
||||
nodeUptimePercentage?: number | null
|
||||
denom: CurrencyDenom
|
||||
sx?: SxProps
|
||||
backdropProps?: object
|
||||
onClose: () => void
|
||||
onOk?: (delegationModalProps: DelegationModalProps) => void
|
||||
}
|
||||
|
||||
export const DelegateModal = ({
|
||||
mixId,
|
||||
identityKey,
|
||||
onClose,
|
||||
onOk,
|
||||
denom,
|
||||
sx,
|
||||
}: Props) => {
|
||||
const [amount, setAmount] = useState<DecCoin | undefined>({
|
||||
amount: '10',
|
||||
denom: 'nym',
|
||||
})
|
||||
const [isValidated, setValidated] = useState<boolean>(false)
|
||||
const [errorAmount, setErrorAmount] = useState<string | undefined>()
|
||||
|
||||
const { address, balance } = useWalletContext()
|
||||
const { handleDelegate } = useDelegationsContext()
|
||||
|
||||
const validate = async () => {
|
||||
let newValidatedValue = true
|
||||
let errorAmountMessage
|
||||
|
||||
if (amount && !(await validateAmount(amount.amount, '0'))) {
|
||||
newValidatedValue = false
|
||||
errorAmountMessage = 'Please enter a valid amount'
|
||||
}
|
||||
|
||||
if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) {
|
||||
errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}`
|
||||
newValidatedValue = false
|
||||
}
|
||||
|
||||
if (!amount?.amount.length) {
|
||||
newValidatedValue = false
|
||||
}
|
||||
|
||||
if (amount && balance.data && +balance.data - +amount.amount <= 0) {
|
||||
errorAmountMessage = 'Not enough funds'
|
||||
newValidatedValue = false
|
||||
}
|
||||
|
||||
setErrorAmount(errorAmountMessage)
|
||||
setValidated(newValidatedValue)
|
||||
}
|
||||
|
||||
const delegateToMixnode = async ({
|
||||
delegationMixId,
|
||||
delegationAmount,
|
||||
}: {
|
||||
delegationMixId: number
|
||||
delegationAmount: string
|
||||
}) => {
|
||||
try {
|
||||
const tx = await handleDelegate(delegationMixId, delegationAmount)
|
||||
return tx
|
||||
} catch (e) {
|
||||
console.error('Failed to delegate to mixnode', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (mixId && amount && onOk) {
|
||||
onOk({
|
||||
status: 'loading',
|
||||
})
|
||||
try {
|
||||
if (!address) {
|
||||
throw new Error('Please connect your wallet')
|
||||
}
|
||||
|
||||
const tx = await delegateToMixnode({
|
||||
delegationMixId: mixId,
|
||||
delegationAmount: amount.amount,
|
||||
})
|
||||
|
||||
if (!tx) {
|
||||
throw new Error('Failed to delegate')
|
||||
}
|
||||
|
||||
onOk({
|
||||
status: 'success',
|
||||
message: 'Delegation can take up to one hour to process',
|
||||
transactions: [
|
||||
{
|
||||
url: `${urls('MAINNET').blockExplorer}/transaction/${
|
||||
tx.transactionHash
|
||||
}`,
|
||||
hash: tx.transactionHash,
|
||||
},
|
||||
],
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to delegate', e)
|
||||
onOk({
|
||||
status: 'error',
|
||||
message: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAmountChanged = (newAmount: DecCoin) => {
|
||||
setAmount(newAmount)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
validate()
|
||||
}, [amount, identityKey, mixId])
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
open
|
||||
onClose={onClose}
|
||||
onOk={handleConfirm}
|
||||
header="Delegate"
|
||||
okLabel="Delegate"
|
||||
okDisabled={!isValidated}
|
||||
sx={sx}
|
||||
>
|
||||
<Box sx={{ mt: 3 }} gap={2}>
|
||||
<IdentityKeyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Node identity key"
|
||||
onChanged={() => undefined}
|
||||
initialValue={identityKey}
|
||||
readOnly
|
||||
showTickOnValid={false}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}>
|
||||
<CurrencyFormField
|
||||
showCoinMark={false}
|
||||
required
|
||||
fullWidth
|
||||
autoFocus
|
||||
label="Amount"
|
||||
initialValue={amount?.amount || '10'}
|
||||
onChanged={handleAmountChanged}
|
||||
denom={denom}
|
||||
validationError={errorAmount}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<ModalListItem
|
||||
label="Account balance"
|
||||
value={`${balance.data} NYM`}
|
||||
divider
|
||||
fontWeight={600}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<ModalListItem label="Est. fee for this transaction will be calculated in your connected wallet" />
|
||||
</SimpleModal>
|
||||
)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Typography, SxProps, Stack } from '@mui/material'
|
||||
import { Link } from '@nymproject/react/link/Link'
|
||||
import { LoadingModal } from './LoadingModal'
|
||||
import { ConfirmationModal } from './ConfirmationModal'
|
||||
import { ErrorModal } from './ErrorModal'
|
||||
|
||||
export type DelegationModalProps = {
|
||||
status: 'loading' | 'success' | 'error' | 'info'
|
||||
message?: string
|
||||
transactions?: {
|
||||
url: string
|
||||
hash: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export const DelegationModal: FCWithChildren<
|
||||
DelegationModalProps & {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
sx?: SxProps
|
||||
backdropProps?: object
|
||||
children?: React.ReactNode
|
||||
}
|
||||
> = ({
|
||||
status,
|
||||
message,
|
||||
transactions,
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
sx,
|
||||
backdropProps,
|
||||
}) => {
|
||||
if (status === 'loading')
|
||||
return <LoadingModal sx={sx} backdropProps={backdropProps} />
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<ErrorModal message={message} sx={sx} open={open} onClose={onClose}>
|
||||
{children}
|
||||
</ErrorModal>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'info') {
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open={open}
|
||||
title="Connect wallet"
|
||||
confirmButton="OK"
|
||||
onConfirm={onClose}
|
||||
>
|
||||
<Typography>{message}</Typography>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open={open}
|
||||
onConfirm={onClose || (() => {})}
|
||||
title="Transaction successful"
|
||||
confirmButton="Done"
|
||||
>
|
||||
<Stack alignItems="center" spacing={2} mb={0}>
|
||||
{message && <Typography>{message}</Typography>}
|
||||
{transactions?.length === 1 && (
|
||||
<Link
|
||||
href={transactions[0].url}
|
||||
target="_blank"
|
||||
sx={{ ml: 1 }}
|
||||
text="View on blockchain"
|
||||
noIcon
|
||||
/>
|
||||
)}
|
||||
{transactions && transactions.length > 1 && (
|
||||
<Stack alignItems="center" spacing={1}>
|
||||
<Typography>View the transactions on blockchain:</Typography>
|
||||
{transactions.map(({ url, hash }) => (
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
sx={{ ml: 1 }}
|
||||
text={hash.slice(0, 6)}
|
||||
key={hash}
|
||||
noIcon
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Modal, SxProps, Typography } from '@mui/material';
|
||||
import { modalStyle } from './SimpleModal';
|
||||
|
||||
export const ErrorModal: FCWithChildren<{
|
||||
open: boolean;
|
||||
title?: string;
|
||||
message?: string;
|
||||
sx?: SxProps;
|
||||
backdropProps?: object;
|
||||
onClose: () => void;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ children, open, title, message, sx, backdropProps, onClose }) => (
|
||||
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
|
||||
<Box sx={{ ...modalStyle(), ...sx }} textAlign="center">
|
||||
<Typography color={(theme) => theme.palette.error.main} mb={1}>
|
||||
{title || 'Oh no! Something went wrong...'}
|
||||
</Typography>
|
||||
<Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}>
|
||||
{message}
|
||||
</Typography>
|
||||
{children}
|
||||
<Button variant="contained" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
|
||||
import { modalStyle } from './SimpleModal';
|
||||
|
||||
export const LoadingModal: FCWithChildren<{
|
||||
text?: string;
|
||||
sx?: SxProps;
|
||||
backdropProps?: object;
|
||||
}> = ({ sx, text = 'Please wait...' }) => (
|
||||
<Modal open>
|
||||
<Box sx={{ ...modalStyle(), ...sx }} textAlign="center">
|
||||
<Stack spacing={4} direction="row" alignItems="center">
|
||||
<CircularProgress />
|
||||
<Typography sx={{ color: 'text.primary' }}>{text}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, SxProps } from '@mui/material';
|
||||
|
||||
export const ModalDivider: FCWithChildren<{
|
||||
sx?: SxProps;
|
||||
}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material';
|
||||
import { ModalDivider } from './ModalDivider';
|
||||
|
||||
export const ModalListItem: FCWithChildren<{
|
||||
label: string;
|
||||
divider?: boolean;
|
||||
hidden?: boolean;
|
||||
fontWeight?: TypographyProps['fontWeight'];
|
||||
fontSize?: TypographyProps['fontSize'];
|
||||
light?: boolean;
|
||||
value?: React.ReactNode;
|
||||
sxValue?: SxProps;
|
||||
}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => (
|
||||
<Box sx={{ display: hidden ? 'none' : 'block' }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
{value && (
|
||||
<Typography
|
||||
fontSize="smaller"
|
||||
fontWeight={fontWeight}
|
||||
sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
{divider && <ModalDivider />}
|
||||
</Box>
|
||||
);
|
||||
@@ -1,152 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import ErrorOutline from '@mui/icons-material/ErrorOutline'
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
|
||||
export const modalStyle = (width: number | string = 600) => ({
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
width,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
borderRadius: '16px',
|
||||
p: 4,
|
||||
})
|
||||
|
||||
export const StyledBackButton = ({
|
||||
onBack,
|
||||
label,
|
||||
fullWidth,
|
||||
sx,
|
||||
}: {
|
||||
onBack: () => void
|
||||
label?: string
|
||||
fullWidth?: boolean
|
||||
sx?: SxProps
|
||||
}) => (
|
||||
<Button
|
||||
disableFocusRipple
|
||||
size="large"
|
||||
fullWidth={fullWidth}
|
||||
variant="outlined"
|
||||
onClick={onBack}
|
||||
sx={sx}
|
||||
>
|
||||
{label || <ArrowBackIosNewIcon fontSize="small" />}
|
||||
</Button>
|
||||
)
|
||||
|
||||
export const SimpleModal: FCWithChildren<{
|
||||
open: boolean
|
||||
hideCloseIcon?: boolean
|
||||
displayErrorIcon?: boolean
|
||||
displayInfoIcon?: boolean
|
||||
headerStyles?: SxProps
|
||||
subHeaderStyles?: SxProps
|
||||
buttonFullWidth?: boolean
|
||||
onClose?: () => void
|
||||
onOk?: () => Promise<void>
|
||||
onBack?: () => void
|
||||
header: string | React.ReactNode
|
||||
subHeader?: string
|
||||
okLabel: string
|
||||
backLabel?: string
|
||||
backButtonFullWidth?: boolean
|
||||
okDisabled?: boolean
|
||||
sx?: SxProps
|
||||
children?: React.ReactNode
|
||||
}> = ({
|
||||
open,
|
||||
hideCloseIcon,
|
||||
displayErrorIcon,
|
||||
displayInfoIcon,
|
||||
headerStyles,
|
||||
buttonFullWidth,
|
||||
onClose,
|
||||
okDisabled,
|
||||
onOk,
|
||||
onBack,
|
||||
header,
|
||||
subHeader,
|
||||
okLabel,
|
||||
backLabel,
|
||||
backButtonFullWidth,
|
||||
sx,
|
||||
children,
|
||||
}) => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={{ ...modalStyle(isMobile ? '90%' : 600), ...sx }}>
|
||||
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
|
||||
{displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />}
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
{typeof header === 'string' ? (
|
||||
<Typography
|
||||
fontSize={20}
|
||||
fontWeight={600}
|
||||
sx={{ color: 'text.primary', ...headerStyles }}
|
||||
>
|
||||
{header}
|
||||
</Typography>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
mt={subHeader ? 0.5 : 0}
|
||||
mb={3}
|
||||
fontSize={12}
|
||||
color={(theme) => theme.palette.text.secondary}
|
||||
>
|
||||
{subHeader}
|
||||
</Typography>
|
||||
|
||||
{children}
|
||||
|
||||
{(onOk || onBack) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
width: buttonFullWidth ? '100%' : null,
|
||||
}}
|
||||
>
|
||||
{onBack && (
|
||||
<StyledBackButton
|
||||
onBack={onBack}
|
||||
label={backLabel}
|
||||
fullWidth={backButtonFullWidth}
|
||||
/>
|
||||
)}
|
||||
{onOk && (
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
onClick={onOk}
|
||||
disabled={okDisabled}
|
||||
>
|
||||
{okLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export * from './ConfirmationModal';
|
||||
export * from './DelegateIconButton';
|
||||
export * from './DelegationModal';
|
||||
export * from './DelegateModal';
|
||||
export * from './ErrorModal';
|
||||
export * from './LoadingModal';
|
||||
export * from './ModalDivider';
|
||||
export * from './ModalListItem';
|
||||
export * from './SimpleModal';
|
||||
export * from './styles';
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
export const backDropStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return {
|
||||
style: {
|
||||
left: mode === 'light' ? '0' : '50%',
|
||||
width: '50%',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const modalStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return { left: mode === 'light' ? '25%' : '75%' };
|
||||
};
|
||||
|
||||
export const dialogStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return { left: mode === 'light' ? '-50%' : '50%' };
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Link,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCellProps,
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip'
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'
|
||||
import { Box } from '@mui/system'
|
||||
import { unymToNym } from '@/app/utils/currency'
|
||||
import { GatewayEnrichedRowType } from './Gateways/Gateways'
|
||||
import { MixnodeRowType } from './MixNodes'
|
||||
import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar'
|
||||
import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants";
|
||||
|
||||
export type ColumnsType = {
|
||||
field: string
|
||||
title: string
|
||||
headerAlign?: TableCellProps['align']
|
||||
width?: string | number
|
||||
tooltipInfo?: string
|
||||
}
|
||||
|
||||
export interface UniversalTableProps<T = any> {
|
||||
tableName: string
|
||||
columnsData: ColumnsType[]
|
||||
rows: T[]
|
||||
}
|
||||
|
||||
function formatCellValues(val: string | number, field: string) {
|
||||
if (field === 'identity_key' && typeof val === 'string') {
|
||||
return (
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
<CopyToClipboard
|
||||
sx={{ mr: 1, mt: 0.5, fontSize: '18px' }}
|
||||
value={val}
|
||||
tooltip={`Copy identity key ${val} to clipboard`}
|
||||
/>
|
||||
<span>{val}</span>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (field === 'bond') {
|
||||
return unymToNym(val, 6)
|
||||
}
|
||||
|
||||
if (field === 'owner') {
|
||||
return (
|
||||
<Link
|
||||
underline="none"
|
||||
color="inherit"
|
||||
target="_blank"
|
||||
href={`${EXPLORER_FOR_ACCOUNTS}/account/${val}`}
|
||||
>
|
||||
{val}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
if (field === 'stake_saturation') {
|
||||
return <StakeSaturationProgressBar value={Number(val)} threshold={100} />
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
export const DetailTable: FCWithChildren<{
|
||||
tableName: string
|
||||
columnsData: ColumnsType[]
|
||||
rows: MixnodeRowType[] | GatewayEnrichedRowType[] | any[]
|
||||
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 1080 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, width, tooltipInfo }) => (
|
||||
<TableCell
|
||||
key={field}
|
||||
sx={{ fontSize: 14, fontWeight: 600, width }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{tooltipInfo && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={field}
|
||||
placement="top-start"
|
||||
textColor={
|
||||
theme.palette.nym.networkExplorer.tooltip.color
|
||||
}
|
||||
bgColor={
|
||||
theme.palette.nym.networkExplorer.tooltip.background
|
||||
}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{title}
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((eachRow) => (
|
||||
<TableRow
|
||||
key={eachRow.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
{columnsData?.map((data, index) => (
|
||||
<TableCell
|
||||
key={data.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 14,
|
||||
}}
|
||||
data-testid={`${data.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(
|
||||
eachRow[columnsData[index].field],
|
||||
columnsData[index].field
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogTitle,
|
||||
Slider,
|
||||
Typography,
|
||||
Box,
|
||||
Snackbar,
|
||||
Slide,
|
||||
Alert,
|
||||
} from '@mui/material'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useMainContext } from '@/app/context/main'
|
||||
import {
|
||||
MixnodeStatusWithAll,
|
||||
toMixnodeStatus,
|
||||
} from '@/app/typeDefs/explorer-api'
|
||||
import { EnumFilterKey, TFilterItem, TFilters } from '@/app/typeDefs/filters'
|
||||
import { Api } from '@/app/api'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { formatOnSave, generateFilterSchema } from './filterSchema'
|
||||
import FiltersButton from './FiltersButton'
|
||||
|
||||
const FilterItem = ({
|
||||
label,
|
||||
id,
|
||||
tooltipInfo,
|
||||
value,
|
||||
isSmooth,
|
||||
marks,
|
||||
scale,
|
||||
min,
|
||||
max,
|
||||
onChange,
|
||||
}: TFilterItem & {
|
||||
onChange: (id: EnumFilterKey, newValue: number[]) => void
|
||||
}) => (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography gutterBottom>{label}</Typography>
|
||||
<Typography fontSize={12}>{tooltipInfo}</Typography>
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={(e: Event, newValue: number | number[]) =>
|
||||
onChange(id, newValue as number[])
|
||||
}
|
||||
valueLabelDisplay={isSmooth ? 'auto' : 'off'}
|
||||
marks={marks}
|
||||
step={isSmooth ? 1 : null}
|
||||
scale={scale}
|
||||
min={min}
|
||||
max={max}
|
||||
valueLabelFormat={(val: number) =>
|
||||
val === 100 && id === 'stakeSaturation' ? '>100' : val
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
||||
export const Filters = () => {
|
||||
const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext()
|
||||
const { status } = useParams<{
|
||||
status: 'active' | 'standby' | 'inactive' | 'all'
|
||||
}>()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
const [isFiltered, setIsFiltered] = useState(false)
|
||||
const [filters, setFilters] = React.useState<TFilters>()
|
||||
const [upperSaturationValue, setUpperSaturationValue] =
|
||||
React.useState<number>(100)
|
||||
|
||||
const baseFilters = useRef<TFilters>()
|
||||
const prevFilters = useRef<TFilters>()
|
||||
|
||||
const handleToggleShowFilters = () => setShowFilters(!showFilters)
|
||||
|
||||
const initialiseFilters = useCallback(async () => {
|
||||
const allMixnodes = await Api.fetchMixnodes()
|
||||
if (allMixnodes) {
|
||||
setUpperSaturationValue(
|
||||
Math.round(
|
||||
Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1
|
||||
)
|
||||
)
|
||||
const initFilters = generateFilterSchema()
|
||||
baseFilters.current = initFilters
|
||||
prevFilters.current = initFilters
|
||||
setFilters(initFilters)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleOnChange = (id: EnumFilterKey, newValue: number[]) => {
|
||||
if (id === 'stakeSaturation' && newValue[1] === 100) {
|
||||
newValue.splice(1, 1, upperSaturationValue)
|
||||
}
|
||||
setFilters((ftrs) => {
|
||||
if (ftrs)
|
||||
return {
|
||||
...ftrs,
|
||||
[id]: {
|
||||
...ftrs[id],
|
||||
value: newValue,
|
||||
},
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
const handleOnSave = async () => {
|
||||
setShowFilters(false)
|
||||
await filterMixnodes(formatOnSave(filters!), status)
|
||||
setIsFiltered(true)
|
||||
prevFilters.current = filters
|
||||
}
|
||||
|
||||
const handleOnCancel = () => {
|
||||
setShowFilters(false)
|
||||
setFilters(prevFilters.current)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters(baseFilters.current)
|
||||
setIsFiltered(false)
|
||||
prevFilters.current = baseFilters.current
|
||||
}
|
||||
|
||||
const onClearFilters = async () => {
|
||||
await fetchMixnodes(toMixnodeStatus(MixnodeStatusWithAll[status]))
|
||||
resetFilters()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initialiseFilters()
|
||||
}, [initialiseFilters])
|
||||
|
||||
useEffect(() => {
|
||||
resetFilters()
|
||||
}, [status])
|
||||
|
||||
if (!filters) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar
|
||||
open={isFiltered}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
message="Filters applied"
|
||||
TransitionComponent={Slide}
|
||||
transitionDuration={250}
|
||||
>
|
||||
<Alert
|
||||
severity="info"
|
||||
variant={isMobile ? 'standard' : 'outlined'}
|
||||
sx={{ color: (t) => t.palette.info.light }}
|
||||
action={
|
||||
<Button size="small" onClick={onClearFilters}>
|
||||
CLEAR FILTERS
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{mixnodes?.data?.length} mixnodes matched your criteria
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<FiltersButton onClick={handleToggleShowFilters} fullWidth />
|
||||
<Dialog
|
||||
open={showFilters}
|
||||
onClose={handleToggleShowFilters}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Mixnode filters</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{Object.values(filters).map((v) => (
|
||||
<FilterItem {...v} key={v.id} onChange={handleOnChange} />
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button size="large" onClick={handleOnCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" size="large" onClick={handleOnSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton } from '@mui/material';
|
||||
import { Tune } from '@mui/icons-material';
|
||||
|
||||
type FiltersButtonProps = {
|
||||
iconOnly?: boolean;
|
||||
fullWidth?: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const FiltersButton = ({ iconOnly, fullWidth, onClick }: FiltersButtonProps) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<IconButton onClick={onClick} color="primary">
|
||||
<Tune />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
fullWidth={fullWidth}
|
||||
size="large"
|
||||
variant="contained"
|
||||
endIcon={<Tune />}
|
||||
onClick={onClick}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Filters
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default FiltersButton;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { EnumFilterKey, TFilters } from '../../typeDefs/filters';
|
||||
|
||||
export const generateFilterSchema = () => ({
|
||||
profitMargin: {
|
||||
label: 'Profit margin (%)',
|
||||
id: EnumFilterKey.profitMargin,
|
||||
value: [0, 100],
|
||||
isSmooth: true,
|
||||
marks: [
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '30', value: 30 },
|
||||
{ label: '40', value: 40 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '60', value: 60 },
|
||||
{ label: '70', value: 70 },
|
||||
{ label: '80', value: 80 },
|
||||
{ label: '90', value: 90 },
|
||||
{ label: '100', value: 100 },
|
||||
],
|
||||
tooltipInfo:
|
||||
'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators',
|
||||
},
|
||||
stakeSaturation: {
|
||||
label: 'Stake saturation (%)',
|
||||
id: EnumFilterKey.stakeSaturation,
|
||||
value: [0, 100],
|
||||
isSmooth: true,
|
||||
marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({
|
||||
value: value < 100 ? value : 100,
|
||||
label: value < 100 ? value : '>100',
|
||||
})),
|
||||
tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards",
|
||||
},
|
||||
routingScore: {
|
||||
label: 'Routing score (%)',
|
||||
id: EnumFilterKey.routingScore,
|
||||
value: [0, 100],
|
||||
isSmooth: true,
|
||||
marks: [
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '30', value: 30 },
|
||||
{ label: '40', value: 40 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '60', value: 60 },
|
||||
{ label: '70', value: 70 },
|
||||
{ label: '80', value: 80 },
|
||||
{ label: '90', value: 90 },
|
||||
{ label: '100', value: 100 },
|
||||
],
|
||||
tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards',
|
||||
},
|
||||
});
|
||||
|
||||
const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
|
||||
const lowerValue = value_1 / 100;
|
||||
const upperValue = value_2 / 100;
|
||||
|
||||
return [lowerValue, upperValue];
|
||||
};
|
||||
|
||||
export const formatOnSave = (filters: TFilters) => ({
|
||||
routingScore: filters.routingScore.value,
|
||||
profitMargin: filters.profitMargin.value,
|
||||
stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value),
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import React from 'react'
|
||||
import Box from '@mui/material/Box'
|
||||
import MuiLink from '@mui/material/Link'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useIsMobile } from '../hooks/useIsMobile'
|
||||
import { NymVpnIcon } from '../icons/NymVpn'
|
||||
import { Socials } from './Socials'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const Footer: FCWithChildren = () => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
mt: 3,
|
||||
pt: 3,
|
||||
pb: 3,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box marginRight={1}>
|
||||
<Link href="http://nymvpn.com" target="_blank">
|
||||
<NymVpnIcon />
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Socials isFooter />
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
textAlign: isMobile ? 'center' : 'end',
|
||||
color: 'nym.muted.onDarkBg',
|
||||
}}
|
||||
>
|
||||
© {new Date().getFullYear()} Nym Technologies SA, all rights reserved
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import {GatewayResponse, GatewayBond, GatewayReportResponse, LocatedGateway} from '@/app/typeDefs/explorer-api';
|
||||
import { toPercentInteger } from '@/app/utils';
|
||||
|
||||
export type GatewayRowType = {
|
||||
id: string;
|
||||
owner: string;
|
||||
identity_key: string;
|
||||
bond: number;
|
||||
host: string;
|
||||
location: string;
|
||||
version: string;
|
||||
// node_performance: number;
|
||||
};
|
||||
|
||||
export type GatewayEnrichedRowType = GatewayRowType & {
|
||||
routingScore: string;
|
||||
avgUptime: string;
|
||||
clientsPort: number;
|
||||
mixPort: number;
|
||||
};
|
||||
|
||||
export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] {
|
||||
return !arrayOfGateways
|
||||
? []
|
||||
: arrayOfGateways.map((gw) => ({
|
||||
id: gw.owner,
|
||||
owner: gw.owner,
|
||||
identity_key: gw.gateway.identity_key || '',
|
||||
location: gw.location?.country_name.toUpperCase() || '',
|
||||
bond: gw.pledge_amount.amount || 0,
|
||||
host: gw.gateway.host || '',
|
||||
version: gw.gateway.version || '',
|
||||
// node_performance: toPercentInteger(gw.node_performance.last_24h),
|
||||
}));
|
||||
}
|
||||
|
||||
export function gatewayEnrichedToGridRow(gateway: LocatedGateway, report: GatewayReportResponse): GatewayEnrichedRowType {
|
||||
return {
|
||||
id: gateway.owner,
|
||||
owner: gateway.owner,
|
||||
identity_key: gateway.gateway.identity_key || '',
|
||||
location: gateway.location?.country_name.toUpperCase() || '',
|
||||
bond: gateway.pledge_amount.amount || 0,
|
||||
host: gateway.gateway.host || '',
|
||||
version: gateway.gateway.version || '',
|
||||
clientsPort: gateway.gateway.clients_port || 0,
|
||||
mixPort: gateway.gateway.mix_port || 0,
|
||||
routingScore: `${report.most_recent}%`,
|
||||
avgUptime: `${report.last_day || report.last_hour}%`,
|
||||
// node_performance: toPercentInteger(gateway.node_performance.most_recent),
|
||||
};
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react'
|
||||
import { FormControl, MenuItem, Select } from '@mui/material'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
|
||||
export enum VersionSelectOptions {
|
||||
latestVersion = 'Latest versions',
|
||||
olderVersions = 'Older versions',
|
||||
all = 'All',
|
||||
}
|
||||
export const VersionDisplaySelector = ({
|
||||
selected,
|
||||
handleChange,
|
||||
}: {
|
||||
selected: VersionSelectOptions
|
||||
handleChange: (option: VersionSelectOptions) => void
|
||||
}) => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<FormControl size="small">
|
||||
<Select
|
||||
value={selected}
|
||||
onChange={(e) => handleChange(e.target.value as VersionSelectOptions)}
|
||||
labelId="simple-select-label"
|
||||
id="simple-select"
|
||||
sx={{
|
||||
marginRight: isMobile ? 0 : 2,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
value={VersionSelectOptions.latestVersion}
|
||||
data-testid="show-gateway-latest-version"
|
||||
>
|
||||
{VersionSelectOptions.latestVersion}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={VersionSelectOptions.olderVersions}
|
||||
data-testid="show-gateway-old-versions"
|
||||
>
|
||||
{VersionSelectOptions.olderVersions}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={VersionSelectOptions.all}
|
||||
data-testid="show-gateway-all-versions"
|
||||
>
|
||||
{VersionSelectOptions.all}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline';
|
||||
import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined';
|
||||
import { MixnodeStatus } from '../typeDefs/explorer-api';
|
||||
|
||||
export const Icons = {
|
||||
Mixnodes: {
|
||||
Status: {
|
||||
Active: CheckCircleOutlineIcon,
|
||||
Standby: PauseCircleOutlineIcon,
|
||||
Inactive: CircleOutlinedIcon,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getMixNodeIcon = (value: any) => {
|
||||
if (value && typeof value === 'string') {
|
||||
switch (value) {
|
||||
case MixnodeStatus.active:
|
||||
return Icons.Mixnodes.Status.Active;
|
||||
case MixnodeStatus.standby:
|
||||
return Icons.Mixnodes.Status.Standby;
|
||||
default:
|
||||
return Icons.Mixnodes.Status.Inactive;
|
||||
}
|
||||
}
|
||||
return Icons.Mixnodes.Status.Inactive;
|
||||
};
|
||||
@@ -1,213 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Alert, Box, CircularProgress, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import Table from '@mui/material/Table'
|
||||
import TableBody from '@mui/material/TableBody'
|
||||
import TableCell from '@mui/material/TableCell'
|
||||
import TableContainer from '@mui/material/TableContainer'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import { ExpandMore } from '@mui/icons-material'
|
||||
import { currencyToString } from '@/app/utils/currency'
|
||||
import { useMixnodeContext } from '@/app/context/mixnode'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
|
||||
export const BondBreakdownTable: FCWithChildren = () => {
|
||||
const { mixNode, delegations, uniqDelegations } = useMixnodeContext()
|
||||
const [showDelegations, toggleShowDelegations] =
|
||||
React.useState<boolean>(false)
|
||||
|
||||
const [bonds, setBonds] = React.useState({
|
||||
delegations: '0',
|
||||
pledges: '0',
|
||||
bondsTotal: '0',
|
||||
hasLoaded: false,
|
||||
})
|
||||
const theme = useTheme()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mixNode?.data) {
|
||||
// delegations
|
||||
const decimalisedDelegations = currencyToString({
|
||||
amount: mixNode.data.total_delegation.amount.toString(),
|
||||
denom: mixNode.data.total_delegation.denom,
|
||||
})
|
||||
|
||||
// pledges
|
||||
const decimalisedPledges = currencyToString({
|
||||
amount: mixNode.data.pledge_amount.amount.toString(),
|
||||
denom: mixNode.data.pledge_amount.denom,
|
||||
})
|
||||
|
||||
// bonds total (del + pledges)
|
||||
const pledgesSum = Number(mixNode.data.pledge_amount.amount)
|
||||
const delegationsSum = Number(mixNode.data.total_delegation.amount)
|
||||
const bondsTotal = currencyToString({
|
||||
amount: (pledgesSum + delegationsSum).toString(),
|
||||
})
|
||||
|
||||
setBonds({
|
||||
delegations: decimalisedDelegations,
|
||||
pledges: decimalisedPledges,
|
||||
bondsTotal,
|
||||
hasLoaded: true,
|
||||
})
|
||||
}
|
||||
}, [mixNode])
|
||||
|
||||
const expandDelegations = () => {
|
||||
if (delegations?.data && delegations.data.length > 0) {
|
||||
toggleShowDelegations(!showDelegations)
|
||||
}
|
||||
}
|
||||
const calcBondPercentage = (num: number) => {
|
||||
if (mixNode?.data) {
|
||||
const rawDelegationAmount = Number(mixNode.data.total_delegation.amount)
|
||||
const rawPledgeAmount = Number(mixNode.data.pledge_amount.amount)
|
||||
const rawTotalBondsAmount = rawDelegationAmount + rawPledgeAmount
|
||||
return ((num * 100) / rawTotalBondsAmount).toFixed(1)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (mixNode?.isLoading || delegations?.isLoading) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
if (mixNode?.error) {
|
||||
return <Alert severity="error">Mixnode not found</Alert>
|
||||
}
|
||||
if (delegations?.error) {
|
||||
return <Alert severity="error">Unable to get delegations for mixnode</Alert>
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals">
|
||||
<TableBody>
|
||||
<TableRow sx={isMobile ? { minWidth: '70vw' } : null}>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
width: '150px',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Stake total
|
||||
</TableCell>
|
||||
<TableCell align="left" data-testid="bond-total-amount">
|
||||
{bonds.bondsTotal}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell align="left">Bond</TableCell>
|
||||
<TableCell align="left" data-testid="pledge-total-amount">
|
||||
{bonds.pledges}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell onClick={expandDelegations} align="left">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Delegation total {'\u00A0'}
|
||||
{delegations?.data && delegations?.data?.length > 0 && (
|
||||
<ExpandMore />
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell align="left" data-testid="delegation-total-amount">
|
||||
{bonds.delegations}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{showDelegations && (
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: 400,
|
||||
overflowY: 'scroll',
|
||||
p: 2,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
width: '100%',
|
||||
p: 2,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
data-testid="delegations-total-amount"
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Delegations
|
||||
</Typography>
|
||||
</Box>
|
||||
<Table stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Delegators
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Amount
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: theme.palette.background.paper,
|
||||
width: '200px',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Share of stake
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{uniqDelegations?.data?.map(({ owner, amount: { amount } }) => (
|
||||
<TableRow key={owner}>
|
||||
<TableCell sx={isMobile ? { width: 190 } : null} align="left">
|
||||
{owner}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{currencyToString({ amount: amount.toString() })}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{calcBondPercentage(amount)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Box, Button, Grid, Typography, useTheme } from '@mui/material'
|
||||
import Identicon from 'react-identicons'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { MixNodeDescriptionResponse } from '@/app/typeDefs/explorer-api'
|
||||
import { getMixNodeStatusText, MixNodeStatus } from './Status'
|
||||
import { MixnodeRowType } from '.'
|
||||
|
||||
interface MixNodeDetailProps {
|
||||
mixNodeRow: MixnodeRowType
|
||||
mixnodeDescription: MixNodeDescriptionResponse
|
||||
}
|
||||
|
||||
export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({
|
||||
mixNodeRow,
|
||||
mixnodeDescription,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const palette = [theme.palette.text.primary]
|
||||
const isMobile = useIsMobile()
|
||||
const statusText = React.useMemo(
|
||||
() => getMixNodeStatusText(mixNodeRow.status),
|
||||
[mixNodeRow.status]
|
||||
)
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection={isMobile ? 'column' : 'row'}
|
||||
width="100%"
|
||||
>
|
||||
<Box
|
||||
width={72}
|
||||
height={72}
|
||||
sx={{
|
||||
minWidth: 72,
|
||||
minHeight: 72,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.palette.text.primary,
|
||||
borderStyle: 'solid',
|
||||
borderRadius: '50%',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Identicon
|
||||
size={43}
|
||||
string={mixNodeRow.identity_key}
|
||||
palette={palette}
|
||||
/>
|
||||
</Box>
|
||||
<Box ml={isMobile ? 0 : 2} mt={isMobile ? 2 : 0}>
|
||||
<Typography fontSize={21}>{mixnodeDescription.name}</Typography>
|
||||
<Typography>
|
||||
{(mixnodeDescription.description || '').slice(0, 1000)}
|
||||
</Typography>
|
||||
<Button
|
||||
component="a"
|
||||
variant="text"
|
||||
sx={{
|
||||
mt: isMobile ? 2 : 4,
|
||||
borderRadius: '30px',
|
||||
fontWeight: 600,
|
||||
padding: 0,
|
||||
}}
|
||||
href={mixnodeDescription.link}
|
||||
target="_blank"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
maxWidth="250px"
|
||||
>
|
||||
{mixnodeDescription.link}
|
||||
</Typography>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
md={6}
|
||||
display="flex"
|
||||
justifyContent={isMobile ? 'start' : 'end'}
|
||||
mt={isMobile ? 3 : undefined}
|
||||
>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography
|
||||
fontWeight="600"
|
||||
alignSelf={isMobile ? 'start' : 'self-end'}
|
||||
>
|
||||
Node status:
|
||||
</Typography>
|
||||
<Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}>
|
||||
<MixNodeStatus status={mixNodeRow.status} />
|
||||
</Box>
|
||||
<Typography
|
||||
mt={1}
|
||||
alignSelf={isMobile ? 'start' : 'self-end'}
|
||||
color={theme.palette.text.secondary}
|
||||
fontSize="smaller"
|
||||
>
|
||||
This node is {statusText} in this epoch
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ColumnsType } from '../../DetailTable';
|
||||
|
||||
export const EconomicsInfoColumns: ColumnsType[] = [
|
||||
{
|
||||
field: 'estimatedTotalReward',
|
||||
title: 'Estimated Total Reward',
|
||||
width: '15%',
|
||||
tooltipInfo:
|
||||
'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.',
|
||||
},
|
||||
{
|
||||
field: 'estimatedOperatorReward',
|
||||
title: 'Estimated Operator Reward',
|
||||
width: '15%',
|
||||
tooltipInfo:
|
||||
"Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.",
|
||||
},
|
||||
{
|
||||
field: 'selectionChance',
|
||||
title: 'Active Set Probability',
|
||||
width: '12.5%',
|
||||
tooltipInfo:
|
||||
'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.',
|
||||
},
|
||||
{
|
||||
field: 'profitMargin',
|
||||
title: 'Profit Margin',
|
||||
width: '12.5%',
|
||||
tooltipInfo:
|
||||
'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.',
|
||||
},
|
||||
{
|
||||
field: 'operatingCost',
|
||||
title: 'Operating Cost',
|
||||
width: '10%',
|
||||
tooltipInfo:
|
||||
'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.',
|
||||
},
|
||||
{
|
||||
field: 'nodePerformance',
|
||||
title: 'Routing Score',
|
||||
width: '10%',
|
||||
tooltipInfo:
|
||||
"Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.",
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
tooltipInfo: "Mixnode's average routing score in the last 24 hour",
|
||||
},
|
||||
];
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { EconomicsProgress } from './EconomicsProgress';
|
||||
|
||||
export default {
|
||||
title: 'Mix Node Detail/Economics/ProgressBar',
|
||||
component: EconomicsProgress,
|
||||
} as ComponentMeta<typeof EconomicsProgress>;
|
||||
|
||||
const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />;
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {};
|
||||
|
||||
export const OverThreshold = Template.bind({});
|
||||
OverThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 120,
|
||||
};
|
||||
|
||||
export const UnderThreshold = Template.bind({});
|
||||
UnderThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 80,
|
||||
};
|
||||
|
||||
export const OnThreshold = Template.bind({});
|
||||
OnThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 100,
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box } from '@mui/system';
|
||||
|
||||
const parseToNumber = (value: number | undefined | string) =>
|
||||
typeof value === 'string' ? parseInt(value || '', 10) : value || 0;
|
||||
|
||||
export const EconomicsProgress: FCWithChildren<
|
||||
LinearProgressProps & {
|
||||
threshold?: number;
|
||||
color: string;
|
||||
}
|
||||
> = ({ threshold, color, ...props }) => {
|
||||
const theme = useTheme();
|
||||
const { value } = props;
|
||||
|
||||
const valueNumber: number = parseToNumber(value);
|
||||
const thresholdNumber: number = parseToNumber(threshold);
|
||||
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 6 / 10,
|
||||
color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee,
|
||||
}}
|
||||
>
|
||||
<LinearProgress
|
||||
{...props}
|
||||
variant="determinate"
|
||||
color={color}
|
||||
value={percentageToDisplay}
|
||||
sx={{ width: '100%', borderRadius: '5px' }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,107 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { DelegatorsInfoTable } from './Table';
|
||||
import { EconomicsInfoColumns } from './Columns';
|
||||
import { EconomicsInfoRowWithIndex } from './types';
|
||||
|
||||
export default {
|
||||
title: 'Mix Node Detail/Economics',
|
||||
component: DelegatorsInfoTable,
|
||||
} as ComponentMeta<typeof DelegatorsInfoTable>;
|
||||
|
||||
const row: EconomicsInfoRowWithIndex = {
|
||||
id: 1,
|
||||
selectionChance: {
|
||||
value: 'High',
|
||||
},
|
||||
|
||||
estimatedOperatorReward: {
|
||||
value: '80000.123456 NYM',
|
||||
},
|
||||
estimatedTotalReward: {
|
||||
value: '80000.123456 NYM',
|
||||
},
|
||||
profitMargin: {
|
||||
value: '10 %',
|
||||
},
|
||||
operatingCost: {
|
||||
value: '11121 NYM',
|
||||
},
|
||||
avgUptime: {
|
||||
value: '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: '-',
|
||||
},
|
||||
};
|
||||
|
||||
const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = {
|
||||
...row,
|
||||
selectionChance: {
|
||||
value: 'Good',
|
||||
},
|
||||
};
|
||||
|
||||
const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = {
|
||||
...row,
|
||||
selectionChance: {
|
||||
value: 'Low',
|
||||
},
|
||||
};
|
||||
|
||||
const emptyRow: EconomicsInfoRowWithIndex = {
|
||||
id: 1,
|
||||
selectionChance: {
|
||||
value: '-',
|
||||
progressBarValue: 0,
|
||||
},
|
||||
|
||||
estimatedOperatorReward: {
|
||||
value: '-',
|
||||
},
|
||||
estimatedTotalReward: {
|
||||
value: '-',
|
||||
},
|
||||
profitMargin: {
|
||||
value: '-',
|
||||
},
|
||||
operatingCost: {
|
||||
value: '-',
|
||||
},
|
||||
avgUptime: {
|
||||
value: '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: '-',
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />;
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
rows: [emptyRow],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceHigh = Template.bind({});
|
||||
selectionChanceHigh.args = {
|
||||
rows: [row],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceGood = Template.bind({});
|
||||
selectionChanceGood.args = {
|
||||
rows: [rowGoodProbabilitySelection],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceLow = Template.bind({});
|
||||
selectionChanceLow.args = {
|
||||
rows: [rowLowProbabilitySelection],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { currencyToString, unymToNym } from '@/app/utils/currency';
|
||||
import { useMixnodeContext } from '@/app/context/mixnode';
|
||||
import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '@/app/typeDefs/explorer-api';
|
||||
import { toPercentIntegerString } from '@/app/utils';
|
||||
import { EconomicsInfoRowWithIndex } from './types';
|
||||
|
||||
const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) =>
|
||||
economicDynamicsStats?.data?.active_set_inclusion_probability || '-';
|
||||
|
||||
export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
||||
const { economicDynamicsStats, mixNode } = useMixnodeContext();
|
||||
|
||||
const estimatedNodeRewards =
|
||||
currencyToString({
|
||||
amount: economicDynamicsStats?.data?.estimated_total_node_reward.toString() || '',
|
||||
}) || '-';
|
||||
const estimatedOperatorRewards =
|
||||
currencyToString({
|
||||
amount: economicDynamicsStats?.data?.estimated_operator_reward.toString() || '',
|
||||
}) || '-';
|
||||
const profitMargin = mixNode?.data?.profit_margin_percent
|
||||
? toPercentIntegerString(mixNode?.data?.profit_margin_percent)
|
||||
: '-';
|
||||
const avgUptime = mixNode?.data?.node_performance
|
||||
? toPercentIntegerString(mixNode?.data?.node_performance.last_24h)
|
||||
: '-';
|
||||
const nodePerformance = mixNode?.data?.node_performance
|
||||
? toPercentIntegerString(mixNode?.data?.node_performance.most_recent)
|
||||
: '-';
|
||||
|
||||
const opCost = mixNode?.data?.operating_cost;
|
||||
|
||||
return {
|
||||
id: 1,
|
||||
estimatedTotalReward: {
|
||||
value: estimatedNodeRewards,
|
||||
},
|
||||
estimatedOperatorReward: {
|
||||
value: estimatedOperatorRewards,
|
||||
},
|
||||
selectionChance: {
|
||||
value: selectionChance(economicDynamicsStats),
|
||||
},
|
||||
profitMargin: {
|
||||
value: profitMargin ? `${profitMargin} %` : '-',
|
||||
},
|
||||
operatingCost: {
|
||||
value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-',
|
||||
},
|
||||
avgUptime: {
|
||||
value: avgUptime ? `${avgUptime} %` : '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: nodePerformance,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { EconomicsProgress } from './EconomicsProgress'
|
||||
|
||||
export const StakeSaturationProgressBar = ({
|
||||
value,
|
||||
threshold,
|
||||
}: {
|
||||
value: number
|
||||
threshold: number
|
||||
}) => {
|
||||
const isTablet = useIsMobile('lg')
|
||||
const percentageColor = value > (threshold || 100) ? 'warning' : 'inherit'
|
||||
const textColor =
|
||||
percentageColor === 'warning' ? 'warning.main' : 'nym.wallet.fee'
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: isTablet ? 'column' : 'row',
|
||||
}}
|
||||
id="field"
|
||||
color={percentageColor}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
mr: isTablet ? 0 : 1,
|
||||
mb: isTablet ? 1 : 0,
|
||||
fontWeight: '600',
|
||||
fontSize: '12px',
|
||||
color: textColor,
|
||||
}}
|
||||
id="stake-saturation-progress-bar"
|
||||
>
|
||||
{value}%
|
||||
</Typography>
|
||||
<EconomicsProgress
|
||||
value={value}
|
||||
threshold={threshold}
|
||||
color={percentageColor}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { Box } from '@mui/system'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip'
|
||||
import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types'
|
||||
import { UniversalTableProps } from '@/app/components/DetailTable'
|
||||
import { textColour } from '@/app/utils'
|
||||
|
||||
const formatCellValues = (value: EconomicsRowsType, field: string) => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
|
||||
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
|
||||
{value.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
|
||||
export const DelegatorsInfoTable: FCWithChildren<
|
||||
UniversalTableProps<EconomicsInfoRowWithIndex>
|
||||
> = ({ tableName, columnsData, rows }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, tooltipInfo, width }) => (
|
||||
<TableCell
|
||||
key={field}
|
||||
sx={{ fontSize: 14, fontWeight: 600, width }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{tooltipInfo && (
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={field}
|
||||
placement="top-start"
|
||||
textColor={
|
||||
theme.palette.nym.networkExplorer.tooltip.color
|
||||
}
|
||||
bgColor={
|
||||
theme.palette.nym.networkExplorer.tooltip.background
|
||||
}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
)}
|
||||
{title}
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows?.map((eachRow) => (
|
||||
<TableRow
|
||||
key={eachRow.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
{columnsData?.map((_, index: number) => {
|
||||
const { field } = columnsData[index]
|
||||
const value: EconomicsRowsType = (eachRow as any)[field]
|
||||
return (
|
||||
<TableCell
|
||||
key={_.title}
|
||||
sx={{
|
||||
color: textColour(value, field, theme),
|
||||
}}
|
||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(value, columnsData[index].field)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { DelegatorsInfoTable } from './Table';
|
||||
export { EconomicsInfoColumns } from './Columns';
|
||||
export { EconomicsInfoRows } from './Rows';
|
||||
@@ -1,20 +0,0 @@
|
||||
export type EconomicsRowsType = {
|
||||
progressBarValue?: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type TEconomicsInfoProperties =
|
||||
| 'estimatedTotalReward'
|
||||
| 'estimatedOperatorReward'
|
||||
| 'estimatedOperatorReward'
|
||||
| 'selectionChance'
|
||||
| 'profitMargin'
|
||||
| 'avgUptime'
|
||||
| 'nodePerformance'
|
||||
| 'operatingCost';
|
||||
|
||||
export type EconomicsInfoRow = {
|
||||
[k in TEconomicsInfoProperties]: EconomicsRowsType;
|
||||
};
|
||||
|
||||
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
|
||||
@@ -1,36 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Typography } from '@mui/material'
|
||||
import { getMixNodeIcon } from '@/app/components/Icons'
|
||||
import { MixnodeStatus } from '@/app/typeDefs/explorer-api'
|
||||
import { useGetMixNodeStatusColor } from '@/app/hooks/useGetMixnodeStatusColor'
|
||||
|
||||
interface MixNodeStatusProps {
|
||||
status: MixnodeStatus
|
||||
}
|
||||
// TODO: should be done with i18n
|
||||
export const getMixNodeStatusText = (status: MixnodeStatus) => {
|
||||
switch (status) {
|
||||
case MixnodeStatus.active:
|
||||
return 'active'
|
||||
case MixnodeStatus.standby:
|
||||
return 'on standby'
|
||||
default:
|
||||
return 'inactive'
|
||||
}
|
||||
}
|
||||
|
||||
export const MixNodeStatus: FCWithChildren<MixNodeStatusProps> = ({
|
||||
status,
|
||||
}) => {
|
||||
const Icon = React.useMemo(() => getMixNodeIcon(status), [status])
|
||||
const color = useGetMixNodeStatusColor(status)
|
||||
|
||||
return (
|
||||
<Typography color={color} display="flex" alignItems="center">
|
||||
<Icon />
|
||||
<Typography ml={1} component="span" color="inherit">
|
||||
{`${status[0].toUpperCase()}${status.slice(1)}`}
|
||||
</Typography>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { MenuItem } from '@mui/material'
|
||||
import Select from '@mui/material/Select'
|
||||
import { SelectChangeEvent } from '@mui/material/Select/SelectInput'
|
||||
import { SxProps } from '@mui/system'
|
||||
import {
|
||||
MixnodeStatus,
|
||||
MixnodeStatusWithAll,
|
||||
} from '@/app/typeDefs/explorer-api'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { MixNodeStatus } from './Status'
|
||||
|
||||
// TODO: replace with i18n
|
||||
const ALL_NODES = 'All nodes'
|
||||
|
||||
interface MixNodeStatusDropdownProps {
|
||||
status?: MixnodeStatusWithAll
|
||||
sx?: SxProps
|
||||
onSelectionChanged?: (status?: MixnodeStatusWithAll) => void
|
||||
}
|
||||
|
||||
export const MixNodeStatusDropdown: FCWithChildren<
|
||||
MixNodeStatusDropdownProps
|
||||
> = ({ status, onSelectionChanged, sx }) => {
|
||||
const isMobile = useIsMobile()
|
||||
const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>(
|
||||
status || MixnodeStatusWithAll.all
|
||||
)
|
||||
const onChange = React.useCallback(
|
||||
(event: SelectChangeEvent) => {
|
||||
setStatusValue(event.target.value as MixnodeStatusWithAll)
|
||||
if (onSelectionChanged) {
|
||||
onSelectionChanged(event.target.value as MixnodeStatusWithAll)
|
||||
}
|
||||
},
|
||||
[onSelectionChanged]
|
||||
)
|
||||
|
||||
return (
|
||||
<Select
|
||||
labelId="mixnodeStatusSelect_label"
|
||||
id="mixnodeStatusSelect"
|
||||
value={statusValue}
|
||||
onChange={onChange}
|
||||
renderValue={(value) => {
|
||||
switch (value) {
|
||||
case 'active':
|
||||
case 'standby':
|
||||
case 'inactive':
|
||||
return <MixNodeStatus status={value as unknown as MixnodeStatus} />
|
||||
default:
|
||||
return ALL_NODES
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: isMobile ? '50%' : 200,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
value={MixnodeStatus.active}
|
||||
data-testid="mixnodeStatusSelectOption_active"
|
||||
>
|
||||
<MixNodeStatus status={MixnodeStatus.active} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={MixnodeStatus.standby}
|
||||
data-testid="mixnodeStatusSelectOption_standby"
|
||||
>
|
||||
<MixNodeStatus status={MixnodeStatus.standby} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={MixnodeStatus.inactive}
|
||||
data-testid="mixnodeStatusSelectOption_inactive"
|
||||
>
|
||||
<MixNodeStatus status={MixnodeStatus.inactive} />
|
||||
</MenuItem>
|
||||
<MenuItem value={'all'} data-testid="mixnodeStatusSelectOption_allNodes">
|
||||
{ALL_NODES}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Status';
|
||||
export * from './StatusDropdown';
|
||||
export * from './mappings';
|
||||
@@ -1,57 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api';
|
||||
import { toPercentInteger, toPercentIntegerString } from '@/app/utils';
|
||||
import { unymToNym } from '@/app/utils/currency';
|
||||
|
||||
export type MixnodeRowType = {
|
||||
mix_id: number;
|
||||
id: string;
|
||||
status: MixnodeStatus;
|
||||
owner: string;
|
||||
location: string;
|
||||
identity_key: string;
|
||||
bond: number;
|
||||
self_percentage: string;
|
||||
pledge_amount: number;
|
||||
host: string;
|
||||
layer: string;
|
||||
profit_percentage: number;
|
||||
avg_uptime: string;
|
||||
stake_saturation: React.ReactNode;
|
||||
operating_cost: number;
|
||||
node_performance: number;
|
||||
blacklisted: boolean;
|
||||
};
|
||||
|
||||
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
|
||||
return (arrayOfMixnodes || []).map(mixNodeResponseItemToMixnodeRowType);
|
||||
}
|
||||
|
||||
export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): MixnodeRowType {
|
||||
const pledge = Number(item.pledge_amount.amount) || 0;
|
||||
const delegations = Number(item.total_delegation.amount) || 0;
|
||||
const totalBond = pledge + delegations;
|
||||
const selfPercentage = ((pledge * 100) / totalBond).toFixed(2);
|
||||
const profitPercentage = toPercentInteger(item.profit_margin_percent) || 0;
|
||||
const uncappedSaturation = typeof item.uncapped_saturation === 'number' ? item.uncapped_saturation * 100 : 0;
|
||||
|
||||
return {
|
||||
mix_id: item.mix_id,
|
||||
id: item.owner,
|
||||
status: item.status,
|
||||
owner: item.owner,
|
||||
identity_key: item.mix_node.identity_key || '',
|
||||
bond: totalBond || 0,
|
||||
location: item?.location?.country_name || '',
|
||||
self_percentage: selfPercentage,
|
||||
pledge_amount: pledge,
|
||||
host: item?.mix_node?.host || '',
|
||||
layer: item?.layer || '',
|
||||
profit_percentage: profitPercentage,
|
||||
avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`,
|
||||
stake_saturation: Number(uncappedSaturation.toFixed(2)),
|
||||
operating_cost: Number(unymToNym(item.operating_cost?.amount, 6)) || 0,
|
||||
node_performance: toPercentInteger(item.node_performance.most_recent),
|
||||
blacklisted: item.blacklisted,
|
||||
};
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ExpandLess, ExpandMore, Menu } from '@mui/icons-material'
|
||||
import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles'
|
||||
import { Link as MuiLink } from '@mui/material'
|
||||
import Button from '@mui/material/Button'
|
||||
import Box from '@mui/material/Box'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import MuiDrawer from '@mui/material/Drawer'
|
||||
import AppBar from '@mui/material/AppBar'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import List from '@mui/material/List'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import ListItemButton from '@mui/material/ListItemButton'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import { NYM_WEBSITE } from '@/app/api/constants'
|
||||
import { useMainContext } from '@/app/context/main'
|
||||
import { MobileDrawerClose } from '@/app/icons/MobileDrawerClose'
|
||||
import { NavOptionType, originalNavOptions } from '@/app/context/nav'
|
||||
import { ReleaseAlert } from '@/app/components/ReleaseAlert'
|
||||
import { DarkLightSwitchDesktop } from '@/app/components/Switch'
|
||||
import { Footer } from '@/app/components/Footer'
|
||||
import { ConnectKeplrWallet } from '@/app/components/Wallet/ConnectKeplrWallet'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import {SearchToolbar} from "@/app/components/Nav/Search";
|
||||
|
||||
const drawerWidth = 255
|
||||
const bannerHeight = 80
|
||||
|
||||
const openedMixin = (theme: Theme): CSSObject => ({
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
})
|
||||
|
||||
const closedMixin = (theme: Theme): CSSObject => ({
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
width: `calc(${theme.spacing(7)} + 1px)`,
|
||||
})
|
||||
|
||||
const DrawerHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: theme.spacing(0, 1),
|
||||
height: 64,
|
||||
}))
|
||||
|
||||
const Drawer = styled(MuiDrawer, {
|
||||
shouldForwardProp: (prop) => prop !== 'open',
|
||||
})(({ theme, open }) => ({
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
...(open && {
|
||||
...openedMixin(theme),
|
||||
'& .MuiDrawer-paper': openedMixin(theme),
|
||||
}),
|
||||
...(!open && {
|
||||
...closedMixin(theme),
|
||||
'& .MuiDrawer-paper': closedMixin(theme),
|
||||
}),
|
||||
}))
|
||||
|
||||
type ExpandableButtonType = {
|
||||
title: string
|
||||
url: string
|
||||
isActive?: boolean
|
||||
Icon?: React.ReactNode
|
||||
nested?: NavOptionType[]
|
||||
isChild?: boolean
|
||||
isMobile: boolean
|
||||
drawIsTempOpen: boolean
|
||||
drawIsFixed: boolean
|
||||
isExternalLink?: boolean
|
||||
openDrawer: () => void
|
||||
closeDrawer?: () => void
|
||||
fixDrawerClose?: () => void
|
||||
}
|
||||
|
||||
export const ExpandableButton: FCWithChildren<ExpandableButtonType> = ({
|
||||
title,
|
||||
url,
|
||||
drawIsTempOpen,
|
||||
drawIsFixed,
|
||||
Icon,
|
||||
nested,
|
||||
isMobile,
|
||||
isChild,
|
||||
isExternalLink,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
fixDrawerClose,
|
||||
}) => {
|
||||
const { palette } = useTheme()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
const handleClick = () => {
|
||||
if (title === 'Network Components') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isExternalLink) {
|
||||
window.open(url, '_blank')
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!isExternalLink) {
|
||||
router.push(url, {})
|
||||
}
|
||||
|
||||
if (closeDrawer) {
|
||||
closeDrawer()
|
||||
}
|
||||
}
|
||||
const selectedStyle = {
|
||||
background: palette.nym.networkExplorer.nav.selected.main,
|
||||
borderRight: `3px solid ${palette.nym.highlight}`,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
sx={{
|
||||
borderBottom: isChild ? 'none' : '1px solid rgba(255, 255, 255, 0.1)',
|
||||
...(pathname === url
|
||||
? selectedStyle
|
||||
: {
|
||||
background: palette.nym.networkExplorer.nav.background,
|
||||
borderRight: 'none',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={() => handleClick()}
|
||||
sx={{
|
||||
pt: 2,
|
||||
pb: 2,
|
||||
background: isChild
|
||||
? palette.nym.networkExplorer.nav.selected.nested
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: '39px' }}>{Icon}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={title}
|
||||
sx={{
|
||||
color: palette.nym.networkExplorer.nav.text,
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{nested?.map((each) => (
|
||||
<ExpandableButton
|
||||
url={each.url}
|
||||
key={each.title}
|
||||
title={each.title}
|
||||
openDrawer={openDrawer}
|
||||
drawIsTempOpen={drawIsTempOpen}
|
||||
closeDrawer={closeDrawer}
|
||||
drawIsFixed={drawIsFixed}
|
||||
fixDrawerClose={fixDrawerClose}
|
||||
isMobile={isMobile}
|
||||
isChild
|
||||
isExternalLink={each.isExternal}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const Nav: FCWithChildren = ({ children }) => {
|
||||
const { environment } = useMainContext()
|
||||
const [drawerIsOpen, setDrawerToOpen] = React.useState(false)
|
||||
const [fixedOpen, setFixedOpen] = React.useState(false)
|
||||
// Set maintenance banner to false by default to don't display it
|
||||
const [openMaintenance, setOpenMaintenance] = React.useState(false)
|
||||
const theme = useTheme()
|
||||
|
||||
const explorerName = environment
|
||||
? `${environment} Explorer`
|
||||
: 'Mainnet Explorer'
|
||||
|
||||
const switchNetworkText =
|
||||
environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet'
|
||||
const switchNetworkLink =
|
||||
environment === 'mainnet'
|
||||
? 'https://sandbox-explorer.nymtech.net'
|
||||
: 'https://explorer.nymtech.net'
|
||||
|
||||
const fixDrawerOpen = () => {
|
||||
setFixedOpen(true)
|
||||
setDrawerToOpen(true)
|
||||
}
|
||||
|
||||
const fixDrawerClose = () => {
|
||||
setFixedOpen(false)
|
||||
setDrawerToOpen(false)
|
||||
}
|
||||
|
||||
const tempDrawerOpen = () => {
|
||||
if (!fixedOpen) {
|
||||
setDrawerToOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const tempDrawerClose = () => {
|
||||
if (!fixedOpen) {
|
||||
setDrawerToOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<AppBar
|
||||
sx={{
|
||||
background: theme.palette.nym.networkExplorer.topNav.appBar,
|
||||
borderRadius: 0,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
disableGutters
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
ml: 0.5,
|
||||
}}
|
||||
>
|
||||
<IconButton component="a" href={NYM_WEBSITE} target="_blank">
|
||||
{/* <NymLogo /> */}
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
sx={{
|
||||
color: theme.palette.nym.networkExplorer.nav.text,
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<MuiLink
|
||||
href="/"
|
||||
underline="none"
|
||||
color="inherit"
|
||||
textTransform="capitalize"
|
||||
>
|
||||
{explorerName}
|
||||
</MuiLink>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
href={switchNetworkLink}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
width: 150,
|
||||
ml: 4,
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{switchNetworkText}
|
||||
</Button>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
mr: 2,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<SearchToolbar/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
pr: 0,
|
||||
pl: 2,
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mr: 1 }}>
|
||||
<ConnectKeplrWallet />
|
||||
</Box>
|
||||
<DarkLightSwitchDesktop defaultChecked />
|
||||
</Box>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
open={true}
|
||||
PaperProps={{
|
||||
style: {
|
||||
background: theme.palette.nym.networkExplorer.nav.background,
|
||||
borderRadius: 0,
|
||||
top: openMaintenance ? bannerHeight : 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DrawerHeader
|
||||
sx={{
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
justifyContent: 'flex-start',
|
||||
paddingLeft: 0,
|
||||
display: 'none',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={drawerIsOpen ? fixDrawerClose : fixDrawerOpen}
|
||||
sx={{
|
||||
padding: 1,
|
||||
ml: 1,
|
||||
color: theme.palette.nym.networkExplorer.nav.text,
|
||||
}}
|
||||
>
|
||||
{drawerIsOpen ? <MobileDrawerClose /> : <Menu />}
|
||||
</IconButton>
|
||||
</DrawerHeader>
|
||||
|
||||
<List
|
||||
sx={{ pb: 0 }}
|
||||
onMouseEnter={tempDrawerOpen}
|
||||
onMouseLeave={tempDrawerClose}
|
||||
>
|
||||
{originalNavOptions.map((props) => (
|
||||
<ExpandableButton
|
||||
key={props.url}
|
||||
closeDrawer={tempDrawerClose}
|
||||
drawIsTempOpen={drawerIsOpen}
|
||||
drawIsFixed={fixedOpen}
|
||||
fixDrawerClose={fixDrawerClose}
|
||||
openDrawer={tempDrawerOpen}
|
||||
isMobile={false}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Drawer>
|
||||
<Box
|
||||
style={{ width: `calc(100% - ${drawerWidth}px` }}
|
||||
sx={{ py: 5, px: 6, mt: 7 }}
|
||||
>
|
||||
<ReleaseAlert />
|
||||
{children}
|
||||
<Footer />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import {
|
||||
AppBar,
|
||||
Box,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
Toolbar,
|
||||
} from '@mui/material'
|
||||
import { Menu } from '@mui/icons-material'
|
||||
import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { MobileDrawerClose } from '@/app/icons/MobileDrawerClose'
|
||||
import { Footer } from '../Footer'
|
||||
import { ExpandableButton } from './DesktopNav'
|
||||
import { ConnectKeplrWallet } from '../Wallet/ConnectKeplrWallet'
|
||||
import { NetworkTitle } from '../NetworkTitle'
|
||||
import { originalNavOptions } from '@/app/context/nav'
|
||||
import { ReleaseAlert } from '@/app/components/ReleaseAlert'
|
||||
import {SearchToolbar} from "@/app/components/Nav/Search";
|
||||
|
||||
export const MobileNav: FCWithChildren = ({ children }) => {
|
||||
const theme = useTheme()
|
||||
const [drawerOpen, setDrawerOpen] = React.useState(false)
|
||||
// Set maintenance banner to false by default to don't display it
|
||||
const [openMaintenance, setOpenMaintenance] = React.useState(false)
|
||||
const isSmallMobile = useIsMobile(400)
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setDrawerOpen(!drawerOpen)
|
||||
}
|
||||
|
||||
const openDrawer = () => {
|
||||
setDrawerOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<AppBar
|
||||
sx={{
|
||||
background: theme.palette.nym.networkExplorer.topNav.appBar,
|
||||
borderRadius: 0,
|
||||
}}
|
||||
>
|
||||
<MaintenanceBanner
|
||||
open={openMaintenance}
|
||||
onClick={() => setOpenMaintenance(false)}
|
||||
/>
|
||||
<Toolbar
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={toggleDrawer}>
|
||||
<Menu sx={{ color: 'primary.contrastText' }} />
|
||||
</IconButton>
|
||||
{!isSmallMobile && <NetworkTitle />}
|
||||
</Box>
|
||||
<Box sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
}}>
|
||||
<Box mr={0.5}>
|
||||
<SearchToolbar/>
|
||||
</Box>
|
||||
<ConnectKeplrWallet />
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer
|
||||
anchor="left"
|
||||
open={drawerOpen}
|
||||
onClose={toggleDrawer}
|
||||
PaperProps={{
|
||||
style: {
|
||||
background: theme.palette.nym.networkExplorer.nav.background,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box role="presentation">
|
||||
<List sx={{ pt: 0, pb: 0 }}>
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
sx={{
|
||||
height: 64,
|
||||
background: theme.palette.nym.networkExplorer.nav.background,
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={toggleDrawer}
|
||||
sx={{
|
||||
pt: 2,
|
||||
pb: 2,
|
||||
background: theme.palette.nym.networkExplorer.nav.background,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<MobileDrawerClose />
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{originalNavOptions.map((props) => (
|
||||
<ExpandableButton
|
||||
key={props.url}
|
||||
title={props.title}
|
||||
openDrawer={openDrawer}
|
||||
url={props.url}
|
||||
drawIsTempOpen={false}
|
||||
drawIsFixed={false}
|
||||
Icon={props.Icon}
|
||||
nested={props.nested}
|
||||
closeDrawer={toggleDrawer}
|
||||
isMobile
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Drawer>
|
||||
|
||||
<Box sx={{ width: '100%', p: 4, mt: 7 }}>
|
||||
<ReleaseAlert />
|
||||
{children}
|
||||
<Footer />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { useIsMobile } from '@/app/hooks'
|
||||
import { MobileNav } from './MobileNav'
|
||||
import { Nav } from './DesktopNav'
|
||||
|
||||
const Navbar = ({ children }: { children: React.ReactNode }) => {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
if (isMobile) {
|
||||
return <MobileNav>{children}</MobileNav>
|
||||
}
|
||||
|
||||
return <Nav>{children}</Nav>
|
||||
}
|
||||
|
||||
export { Navbar }
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from "react";
|
||||
import { styled, alpha } from '@mui/material/styles';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import InputBase from '@mui/material/InputBase';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
const Search = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.common.white, 0.25),
|
||||
},
|
||||
marginLeft: 0,
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
marginLeft: theme.spacing(1),
|
||||
width: 'auto',
|
||||
},
|
||||
}));
|
||||
|
||||
const SearchIconWrapper = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(0, 2),
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
||||
color: 'inherit',
|
||||
width: '100%',
|
||||
'& .MuiInputBase-input': {
|
||||
padding: theme.spacing(1, 1, 1, 0),
|
||||
// vertical padding + font size from searchIcon
|
||||
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: '30ch',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const SearchToolbar = () => {
|
||||
const [search, setSearch] = React.useState<string>();
|
||||
const router = useRouter();
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if(search?.trim().length) {
|
||||
router.push(`/account/${search.trim()}`);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Search>
|
||||
<SearchIconWrapper>
|
||||
<SearchIcon />
|
||||
</SearchIconWrapper>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<StyledInputBase
|
||||
placeholder="Search for account id…"
|
||||
inputProps={{ 'aria-label': 'search' }}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Search>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Button, Typography, Link as MuiLink } from '@mui/material'
|
||||
import { useMainContext } from '@/app/context/main'
|
||||
|
||||
type NetworkTitleProps = {
|
||||
showToggleNetwork?: boolean
|
||||
}
|
||||
|
||||
const NetworkTitle = ({ showToggleNetwork }: NetworkTitleProps) => {
|
||||
const { environment } = useMainContext()
|
||||
|
||||
const explorerName =
|
||||
`${
|
||||
environment && environment.charAt(0).toUpperCase() + environment.slice(1)
|
||||
} Explorer` || 'Mainnet Explorer'
|
||||
|
||||
const switchNetworkText =
|
||||
environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet'
|
||||
const switchNetworkLink =
|
||||
environment === 'mainnet'
|
||||
? 'https://sandbox-explorer.nymtech.net'
|
||||
: 'https://explorer.nymtech.net'
|
||||
return (
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
sx={{
|
||||
color: 'nym.networkExplorer.nav.text',
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<MuiLink href="/" underline="none" color="inherit" fontWeight={700}>
|
||||
{explorerName}
|
||||
</MuiLink>
|
||||
|
||||
{showToggleNetwork && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
href={switchNetworkLink}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
width: 114,
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
ml: 1,
|
||||
}}
|
||||
>
|
||||
{switchNetworkText}
|
||||
</Button>
|
||||
)}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
export { NetworkTitle }
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Alert, Box, Typography } from '@mui/material';
|
||||
|
||||
export const ReleaseAlert = () => (
|
||||
<Alert severity="warning" sx={{ mb: 3, fontSize: 'medium', width: '100%' }}>
|
||||
<Box>
|
||||
<Typography>You are now viewing the legacy Nym mixnet explorer. Explorer 2.0 is coming soon.</Typography>
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Box, IconButton } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { TelegramIcon } from '../icons/socials/TelegramIcon'
|
||||
import { GitHubIcon } from '../icons/socials/GitHubIcon'
|
||||
import { TwitterIcon } from '../icons/socials/TwitterIcon'
|
||||
import { DiscordIcon } from '../icons/socials/DiscordIcon'
|
||||
|
||||
// socials
|
||||
export const TELEGRAM_LINK = 'https://nymtech.net/go/telegram';
|
||||
export const TWITTER_LINK = 'https://nymtech.net/go/x';
|
||||
export const GITHUB_LINK = 'https://nymtech.net/go/github';
|
||||
export const DISCORD_LINK = 'https://nymtech.net/go/discord';
|
||||
|
||||
export const Socials: FCWithChildren<{ isFooter?: boolean }> = ({
|
||||
isFooter = false,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const color = isFooter
|
||||
? theme.palette.nym.networkExplorer.footer.socialIcons
|
||||
: theme.palette.nym.networkExplorer.topNav.socialIcons
|
||||
return (
|
||||
<Box>
|
||||
<IconButton
|
||||
component="a"
|
||||
href={TELEGRAM_LINK}
|
||||
target="_blank"
|
||||
data-testid="telegram"
|
||||
>
|
||||
<TelegramIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
component="a"
|
||||
href={DISCORD_LINK}
|
||||
target="_blank"
|
||||
data-testid="discord"
|
||||
>
|
||||
<DiscordIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
component="a"
|
||||
href={TWITTER_LINK}
|
||||
target="_blank"
|
||||
data-testid="twitter"
|
||||
>
|
||||
<TwitterIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
component="a"
|
||||
href={GITHUB_LINK}
|
||||
target="_blank"
|
||||
data-testid="github"
|
||||
>
|
||||
<GitHubIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Box, Card, CardContent, IconButton, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import EastIcon from '@mui/icons-material/East'
|
||||
|
||||
interface StatsCardProps {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
count?: string | number
|
||||
errorMsg?: Error | string
|
||||
onClick?: () => void
|
||||
color?: string
|
||||
}
|
||||
export const StatsCard: FCWithChildren<StatsCardProps> = ({
|
||||
icon,
|
||||
title,
|
||||
count,
|
||||
onClick,
|
||||
errorMsg,
|
||||
color: colorProp,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const color = colorProp || theme.palette.text.primary
|
||||
return (
|
||||
<Card onClick={onClick} sx={{ height: '100%' }}>
|
||||
<CardContent
|
||||
sx={{
|
||||
padding: 1.5,
|
||||
paddingLeft: 3,
|
||||
'&:last-child': {
|
||||
paddingBottom: 1.5,
|
||||
},
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" color={color}>
|
||||
<Box display="flex">
|
||||
{icon}
|
||||
<Typography
|
||||
ml={3}
|
||||
mr={0.75}
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
data-testid={`${title}-amount`}
|
||||
>
|
||||
{count === undefined || count === null ? '' : count}
|
||||
</Typography>
|
||||
<Typography
|
||||
mr={1}
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
data-testid={title}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton color="inherit" sx={{ fontSize: '16px' }}>
|
||||
<EastIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
{errorMsg && (
|
||||
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
|
||||
{typeof errorMsg === 'string'
|
||||
? errorMsg
|
||||
: errorMsg.message || 'Oh no! An error occurred'}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Link as MuiLink, SxProps, Typography } from '@mui/material'
|
||||
import Link from 'next/link'
|
||||
|
||||
type StyledLinkProps = {
|
||||
to: string
|
||||
children: React.ReactNode
|
||||
target?: React.HTMLAttributeAnchorTarget
|
||||
dataTestId?: string
|
||||
color?: string
|
||||
sx?: SxProps
|
||||
}
|
||||
|
||||
const StyledLink = ({
|
||||
to,
|
||||
children,
|
||||
dataTestId,
|
||||
target,
|
||||
color,
|
||||
sx,
|
||||
}: StyledLinkProps) => (
|
||||
<Link
|
||||
href={to}
|
||||
target={target}
|
||||
data-testid={dataTestId}
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<Typography component="a" sx={{ ...sx }} color={color}>
|
||||
{children}
|
||||
</Typography>
|
||||
</Link>
|
||||
)
|
||||
|
||||
export default StyledLink
|
||||
@@ -1,70 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { Button } from '@mui/material';
|
||||
import { useMainContext } from '../context/main';
|
||||
import { LightSwitchSVG } from '../icons/LightSwitchSVG';
|
||||
|
||||
export const DarkLightSwitch = styled(Switch)(({ theme }) => ({
|
||||
width: 55,
|
||||
height: 34,
|
||||
padding: 7,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
margin: 1,
|
||||
padding: 2,
|
||||
transform: 'translateX(4px)',
|
||||
'&.Mui-checked': {
|
||||
color: '#fff',
|
||||
transform: 'translateX(22px)',
|
||||
'& .MuiSwitch-thumb:before': {
|
||||
backgroundImage:
|
||||
'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')',
|
||||
},
|
||||
'& + .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
},
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
backgroundColor: theme.palette.nym.networkExplorer.nav.text,
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginTop: '2px',
|
||||
'&:before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
backgroundImage:
|
||||
'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')',
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
borderRadius: 20 / 2,
|
||||
},
|
||||
}));
|
||||
|
||||
export const DarkLightSwitchMobile: FCWithChildren = () => {
|
||||
const { toggleMode } = useMainContext();
|
||||
return (
|
||||
<Button onClick={() => toggleMode()} data-testid="switch-button" sx={{ p: 0, minWidth: 0 }}>
|
||||
<LightSwitchSVG />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const DarkLightSwitchDesktop: FCWithChildren<{ defaultChecked: boolean }> = ({ defaultChecked }) => {
|
||||
const { toggleMode } = useMainContext();
|
||||
return (
|
||||
<Button sx={{ paddingLeft: 0 }} onClick={() => toggleMode()} data-testid="switch-button">
|
||||
<DarkLightSwitch defaultChecked={defaultChecked} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { Box, SelectChangeEvent } from '@mui/material'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { Filters } from './Filters/Filters'
|
||||
|
||||
const fieldsHeight = '42.25px'
|
||||
|
||||
type TableToolBarProps = {
|
||||
childrenBefore?: React.ReactNode
|
||||
childrenAfter?: React.ReactNode
|
||||
}
|
||||
|
||||
export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
|
||||
childrenBefore,
|
||||
childrenAfter,
|
||||
}) => {
|
||||
const isMobile = useIsMobile()
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column-reverse' : 'row',
|
||||
alignItems: 'middle',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
height: fieldsHeight,
|
||||
}}
|
||||
>
|
||||
{childrenBefore}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'end',
|
||||
gap: 1,
|
||||
marginTop: isMobile ? 2 : 0,
|
||||
}}
|
||||
>
|
||||
{childrenAfter}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
export const Title: FCWithChildren<{ text: string }> = ({ text }) => (
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
}}
|
||||
data-testid={text}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material';
|
||||
|
||||
type ValueType<T> = T[keyof T];
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
id: string;
|
||||
placement?: ValueType<Pick<TooltipProps, 'placement'>>;
|
||||
tooltipSx?: TooltipComponentsPropsOverrides;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => (
|
||||
<MUITooltip
|
||||
title={text}
|
||||
id={id}
|
||||
placement={placement || 'top-start'}
|
||||
componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
maxWidth: 200,
|
||||
background: (t) => t.palette.nym.networkExplorer.tooltip.background,
|
||||
color: (t) => t.palette.nym.networkExplorer.tooltip.color,
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: (t) => t.palette.nym.networkExplorer.tooltip.background,
|
||||
},
|
||||
},
|
||||
...tooltipSx,
|
||||
},
|
||||
}}
|
||||
arrow
|
||||
>
|
||||
{children as ReactElement<any, any>}
|
||||
</MUITooltip>
|
||||
);
|
||||
@@ -1,78 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { CircularProgress, Typography } from '@mui/material';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
|
||||
interface TableProps {
|
||||
title?: string;
|
||||
icons?: boolean[];
|
||||
keys: string[];
|
||||
values: number[];
|
||||
marginBottom?: boolean;
|
||||
error?: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const TwoColSmallTable: FCWithChildren<TableProps> = ({
|
||||
loading,
|
||||
title,
|
||||
icons,
|
||||
keys,
|
||||
values,
|
||||
marginBottom,
|
||||
error,
|
||||
}) => (
|
||||
<>
|
||||
{title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>}
|
||||
|
||||
<TableContainer component={Paper} sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}>
|
||||
<Table aria-label="two col small table">
|
||||
<TableBody>
|
||||
{keys.map((each: string, i: number) => (
|
||||
<TableRow key={each}>
|
||||
{icons && <TableCell>{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}</TableCell>}
|
||||
<TableCell sx={error ? { opacity: 0.4 } : null} data-testid={each.replace(/ /g, '')}>
|
||||
{each}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={error ? { opacity: 0.4 } : null}
|
||||
align="right"
|
||||
data-testid={`${each.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{values[i]}
|
||||
</TableCell>
|
||||
{error && (
|
||||
<TableCell align="right" sx={{ opacity: 0.4 }}>
|
||||
{values[i]}
|
||||
</TableCell>
|
||||
)}
|
||||
{!error && loading && (
|
||||
<TableCell align="right">
|
||||
<CircularProgress />
|
||||
</TableCell>
|
||||
)}
|
||||
{error && !icons && (
|
||||
<TableCell sx={{ opacity: 0.2 }} align="right">
|
||||
<ErrorIcon />
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
TwoColSmallTable.defaultProps = {
|
||||
title: undefined,
|
||||
icons: undefined,
|
||||
marginBottom: false,
|
||||
error: undefined,
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import {
|
||||
DataGrid,
|
||||
GridColDef,
|
||||
GridEventListener,
|
||||
useGridApiContext,
|
||||
} from '@mui/x-data-grid'
|
||||
import Pagination from '@mui/material/Pagination'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
})
|
||||
|
||||
const CustomPagination = () => {
|
||||
const apiRef = useGridApiContext()
|
||||
const classes = useStyles()
|
||||
console.log(apiRef.current.state)
|
||||
|
||||
return (
|
||||
<Pagination
|
||||
className={classes.root}
|
||||
sx={{ mt: 2 }}
|
||||
color="primary"
|
||||
count={apiRef.current.state.pagination.paginationModel.pageSize}
|
||||
page={apiRef.current.state.pagination.paginationModel.page + 1}
|
||||
onChange={(_, value) => apiRef.current.setPage(value - 1)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type DataGridProps = {
|
||||
columns: GridColDef[]
|
||||
pagination?: true | undefined
|
||||
pageSize?: string | undefined
|
||||
rows: any
|
||||
loading?: boolean
|
||||
initialState?: GridInitialStateCommunity
|
||||
onRowClick?: GridEventListener<'rowClick'> | undefined
|
||||
}
|
||||
export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({
|
||||
rows,
|
||||
columns,
|
||||
loading,
|
||||
pagination,
|
||||
pageSize,
|
||||
initialState,
|
||||
onRowClick,
|
||||
}) => {
|
||||
if (loading) return <LinearProgress />
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
onRowClick={onRowClick}
|
||||
pagination={pagination}
|
||||
rows={rows}
|
||||
slots={{
|
||||
pagination: CustomPagination,
|
||||
}}
|
||||
columns={columns}
|
||||
autoHeight
|
||||
hideFooter={!pagination}
|
||||
initialState={initialState}
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
sx={{
|
||||
'*::-webkit-scrollbar': {
|
||||
width: '1em',
|
||||
},
|
||||
'*::-webkit-scrollbar-track': {
|
||||
background: (t) => t.palette.nym.networkExplorer.scroll.backgroud,
|
||||
outline: (t) =>
|
||||
`1px solid ${t.palette.nym.networkExplorer.scroll.border}`,
|
||||
boxShadow: 'auto',
|
||||
borderRadius: 'auto',
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color,
|
||||
borderRadius: '20px',
|
||||
width: '.4em',
|
||||
border: (t) =>
|
||||
`3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`,
|
||||
shadow: 'auto',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { CircularProgress, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Chart } from 'react-google-charts';
|
||||
import { format } from 'date-fns';
|
||||
import { ApiState, UptimeStoryResponse } from '../typeDefs/explorer-api';
|
||||
|
||||
interface ChartProps {
|
||||
title?: string;
|
||||
xLabel: string;
|
||||
yLabel?: string;
|
||||
uptimeStory: ApiState<UptimeStoryResponse>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
type FormattedDateRecord = [string, number];
|
||||
type FormattedChartHeadings = string[];
|
||||
type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord];
|
||||
|
||||
export const UptimeChart: FCWithChildren<ChartProps> = ({ title, xLabel, yLabel, uptimeStory, loading }) => {
|
||||
const [formattedChartData, setFormattedChartData] = React.useState<FormattedChartData>();
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
React.useEffect(() => {
|
||||
if (uptimeStory.data?.history) {
|
||||
const allFormattedChartData: FormattedChartData = [['Date', 'Score']];
|
||||
uptimeStory.data.history.forEach((eachDate) => {
|
||||
const formattedDateUptimeRecord: FormattedDateRecord = [
|
||||
format(new Date(eachDate.date), 'MMM dd'),
|
||||
eachDate.uptime,
|
||||
];
|
||||
allFormattedChartData.push(formattedDateUptimeRecord);
|
||||
});
|
||||
setFormattedChartData(allFormattedChartData);
|
||||
} else {
|
||||
const emptyData: any = [
|
||||
['Date', 'Score'],
|
||||
['Jul 27', 10],
|
||||
];
|
||||
setFormattedChartData(emptyData);
|
||||
}
|
||||
}, [uptimeStory]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && <Typography>{title}</Typography>}
|
||||
{loading && <CircularProgress />}
|
||||
|
||||
{!loading && uptimeStory && (
|
||||
<Chart
|
||||
style={{ minHeight: 480 }}
|
||||
chartType="LineChart"
|
||||
loader={<p>...</p>}
|
||||
data={
|
||||
uptimeStory.data
|
||||
? formattedChartData
|
||||
: [
|
||||
['Date', 'Routing Score'],
|
||||
[format(new Date(Date.now()), 'MMM dd'), 0],
|
||||
]
|
||||
}
|
||||
options={{
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? theme.palette.nym.networkExplorer.background.tertiary : undefined,
|
||||
color: uptimeStory.error ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 1)',
|
||||
colors: ['#FB7A21'],
|
||||
legend: {
|
||||
textStyle: {
|
||||
color,
|
||||
opacity: uptimeStory.error ? 0.4 : 1,
|
||||
},
|
||||
},
|
||||
|
||||
intervals: { style: 'sticks' },
|
||||
hAxis: {
|
||||
// horizontal / date
|
||||
title: xLabel,
|
||||
titleTextStyle: {
|
||||
color,
|
||||
},
|
||||
textStyle: {
|
||||
color,
|
||||
// fontSize: 11
|
||||
},
|
||||
gridlines: {
|
||||
count: -1,
|
||||
},
|
||||
},
|
||||
vAxis: {
|
||||
// vertical / % Routing Score
|
||||
viewWindow: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
title: yLabel,
|
||||
titleTextStyle: {
|
||||
color,
|
||||
opacity: uptimeStory.error ? 0.4 : 1,
|
||||
},
|
||||
textStyle: {
|
||||
color,
|
||||
fontSize: 11,
|
||||
opacity: uptimeStory.error ? 0.4 : 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
UptimeChart.defaultProps = {
|
||||
title: undefined,
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Button, IconButton, Stack, CircularProgress } from '@mui/material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import { useIsMobile } from '@/app/hooks/useIsMobile'
|
||||
import { useWalletContext } from '@/app/context/wallet'
|
||||
import { WalletAddress, WalletBalance } from '@/app/components/Wallet'
|
||||
|
||||
export const ConnectKeplrWallet = () => {
|
||||
const {
|
||||
connectWallet,
|
||||
disconnectWallet,
|
||||
isWalletConnected,
|
||||
isWalletConnecting,
|
||||
} = useWalletContext()
|
||||
const isMobile = useIsMobile(1200)
|
||||
|
||||
if (!connectWallet || !disconnectWallet) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isWalletConnected) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<WalletBalance />
|
||||
<WalletAddress />
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await disconnectWallet()
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" sx={{ color: 'white' }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => connectWallet()}
|
||||
disabled={isWalletConnecting}
|
||||
endIcon={
|
||||
isWalletConnecting && <CircularProgress size={14} color="inherit" />
|
||||
}
|
||||
>
|
||||
Connect {isMobile ? '' : ' Wallet'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { ElipsSVG } from '@/app/icons/ElipsSVG'
|
||||
import { trimAddress } from '@/app/utils'
|
||||
import { useWalletContext } from '@/app/context/wallet'
|
||||
|
||||
export const WalletAddress = () => {
|
||||
const { address } = useWalletContext()
|
||||
|
||||
const displayAddress = trimAddress(address, 7)
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" gap={0.5}>
|
||||
<ElipsSVG />
|
||||
<Typography variant="body1" fontWeight={600}>
|
||||
{displayAddress}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { useWalletContext } from '@/app/context/wallet'
|
||||
import { useIsMobile } from '@/app/hooks'
|
||||
import { TokenSVG } from '@/app/icons/TokenSVG'
|
||||
|
||||
export const WalletBalance = () => {
|
||||
const { balance } = useWalletContext()
|
||||
const isMobile = useIsMobile(1200)
|
||||
|
||||
const showBalance = !isMobile && balance.status === 'success'
|
||||
|
||||
if (!showBalance) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<TokenSVG />
|
||||
<Typography variant="body1" fontWeight={600}>
|
||||
{balance.data} NYM
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './WalletBalance';
|
||||
export * from './WalletAddress';
|
||||
@@ -1,129 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { scaleLinear } from 'd3-scale'
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
Geography,
|
||||
Marker,
|
||||
ZoomableGroup,
|
||||
} from 'react-simple-maps'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
import { CircularProgress } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { ApiState, CountryDataResponse } from '../typeDefs/explorer-api'
|
||||
import MAP_TOPOJSON from '../assets/world-110m.json'
|
||||
|
||||
type MapProps = {
|
||||
userLocation?: [number, number]
|
||||
countryData?: ApiState<CountryDataResponse>
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export const WorldMap: FCWithChildren<MapProps> = ({
|
||||
countryData,
|
||||
userLocation,
|
||||
loading,
|
||||
}) => {
|
||||
const { palette } = useTheme()
|
||||
|
||||
const colorScale = React.useMemo(() => {
|
||||
if (countryData?.data) {
|
||||
const heighestNumberOfNodes = Math.max(
|
||||
...Object.values(countryData.data).map((country) => country.nodes)
|
||||
)
|
||||
return scaleLinear<string, string>()
|
||||
.domain([
|
||||
0,
|
||||
1,
|
||||
heighestNumberOfNodes / 4,
|
||||
heighestNumberOfNodes / 2,
|
||||
heighestNumberOfNodes,
|
||||
])
|
||||
.range(palette.nym.networkExplorer.map.fills)
|
||||
.unknown(palette.nym.networkExplorer.map.fills[0])
|
||||
}
|
||||
return () => palette.nym.networkExplorer.map.fills[0]
|
||||
}, [countryData, palette])
|
||||
|
||||
const [tooltipContent, setTooltipContent] = React.useState<string | null>(
|
||||
null
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ComposableMap
|
||||
data-tip=""
|
||||
style={{
|
||||
backgroundColor: palette.nym.networkExplorer.background.tertiary,
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
}}
|
||||
viewBox="0, 50, 800, 350"
|
||||
projection="geoMercator"
|
||||
projectionConfig={{
|
||||
scale: userLocation ? 200 : 100,
|
||||
center: userLocation,
|
||||
}}
|
||||
>
|
||||
<ZoomableGroup>
|
||||
<Geographies geography={MAP_TOPOJSON}>
|
||||
{({ geographies }) =>
|
||||
geographies.map((geo) => {
|
||||
const d = (countryData?.data || {})[geo.properties.ISO_A3]
|
||||
return (
|
||||
<Geography
|
||||
key={geo.rsmKey}
|
||||
geography={geo}
|
||||
fill={colorScale(d?.nodes || 0)}
|
||||
stroke={palette.nym.networkExplorer.map.stroke}
|
||||
strokeWidth={0.2}
|
||||
onMouseEnter={() => {
|
||||
const { NAME_LONG } = geo.properties
|
||||
if (!userLocation) {
|
||||
setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`)
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltipContent('')
|
||||
}}
|
||||
style={{
|
||||
hover:
|
||||
!userLocation && countryData
|
||||
? {
|
||||
fill: palette.nym.highlight,
|
||||
outline: 'white',
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
|
||||
{userLocation && (
|
||||
<Marker coordinates={userLocation}>
|
||||
<g
|
||||
fill="grey"
|
||||
stroke="#FF5533"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
transform="translate(-12, -10)"
|
||||
>
|
||||
<circle cx="12" cy="10" r="5" />
|
||||
</g>
|
||||
</Marker>
|
||||
)}
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
<ReactTooltip>{tooltipContent}</ReactTooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export * from './CustomColumnHeading';
|
||||
export * from './Title';
|
||||
export * from './Universal-DataGrid';
|
||||
export * from './Tooltip';
|
||||
export { default as StyledLink } from './StyledLink';
|
||||
export * from './Delegations';
|
||||
export * from './MixNodes';
|
||||
export * from './TableToolbar';
|
||||
export * from './Icons';
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { ChainProvider } from '@cosmos-kit/react'
|
||||
import { wallets as keplr } from '@cosmos-kit/keplr-extension'
|
||||
import { assets, chains } from 'chain-registry'
|
||||
import { Chain, AssetList } from '@chain-registry/types'
|
||||
import { VALIDATOR_BASE_URL } from '@/app/api/constants'
|
||||
|
||||
const nymSandbox: Chain = {
|
||||
chain_name: 'sandbox',
|
||||
chain_id: 'sandbox',
|
||||
bech32_prefix: 'n',
|
||||
network_type: 'devnet',
|
||||
pretty_name: 'Nym Sandbox',
|
||||
status: 'active',
|
||||
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 chainsFixedUp = React.useMemo(() => {
|
||||
const nyx = chains.find((chain) => chain.chain_id === 'nyx')
|
||||
|
||||
return nyx ? [nymSandbox, nyx] : [nymSandbox]
|
||||
}, [chains])
|
||||
|
||||
// Only use the nyx assets
|
||||
const assetsFixedUp = React.useMemo(() => {
|
||||
const nyx = assets.find((asset) => asset.chain_name === 'nyx')
|
||||
|
||||
return nyx ? [nymSandboxAssets, nyx] : [nymSandboxAssets]
|
||||
}, [assets]) as AssetList[]
|
||||
|
||||
return (
|
||||
<ChainProvider
|
||||
chains={chainsFixedUp}
|
||||
assetLists={assetsFixedUp}
|
||||
wallets={[...keplr]}
|
||||
endpointOptions={{
|
||||
endpoints: {
|
||||
nyx: {
|
||||
rpc: [VALIDATOR_BASE_URL],
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ChainProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default CosmosKitProvider
|
||||
@@ -1,252 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
Delegation,
|
||||
PendingEpochEvent,
|
||||
PendingEpochEventKind,
|
||||
} from '@nymproject/contract-clients/Mixnet.types'
|
||||
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'
|
||||
import { useWalletContext } from './wallet'
|
||||
import { useMainContext } from './main'
|
||||
|
||||
const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] }
|
||||
|
||||
export type PendingEvent = ReturnType<typeof getEventsByAddress>
|
||||
|
||||
export type DelegationWithRewards = Delegation & {
|
||||
rewards: string
|
||||
identityKey: string
|
||||
pending: PendingEvent
|
||||
}
|
||||
|
||||
const getEventsByAddress = (kind: PendingEpochEventKind, address: String) => {
|
||||
if ('delegate' in kind && kind.delegate.owner === address) {
|
||||
return {
|
||||
kind: 'delegate' as const,
|
||||
mixId: kind.delegate.mix_id,
|
||||
amount: kind.delegate.amount,
|
||||
}
|
||||
}
|
||||
|
||||
if ('undelegate' in kind && kind.undelegate.owner === address) {
|
||||
return {
|
||||
kind: 'undelegate' as const,
|
||||
mixId: kind.undelegate.mix_id,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
interface DelegationsState {
|
||||
delegations?: DelegationWithRewards[]
|
||||
handleGetDelegations: () => Promise<void>
|
||||
handleDelegate: (
|
||||
mixId: number,
|
||||
amount: string
|
||||
) => Promise<ExecuteResult | undefined>
|
||||
handleUndelegate: (mixId: number) => Promise<ExecuteResult | undefined>
|
||||
}
|
||||
|
||||
export const DelegationsContext = createContext<DelegationsState>({
|
||||
delegations: undefined,
|
||||
handleGetDelegations: async () => {
|
||||
throw new Error('Please connect your wallet')
|
||||
},
|
||||
handleDelegate: async () => {
|
||||
throw new Error('Please connect your wallet')
|
||||
},
|
||||
handleUndelegate: async () => {
|
||||
throw new Error('Please connect your wallet')
|
||||
},
|
||||
})
|
||||
|
||||
export const DelegationsProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [delegations, setDelegations] = useState<DelegationWithRewards[]>()
|
||||
const { address, nymQueryClient, nymClient } = useWalletContext()
|
||||
const { fetchMixnodes } = useMainContext()
|
||||
|
||||
const handleGetPendingEvents = async () => {
|
||||
if (!nymQueryClient) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const response = await nymQueryClient.getPendingEpochEvents({})
|
||||
const pendingEvents: PendingEvent[] = []
|
||||
|
||||
response.events.forEach((e: PendingEpochEvent) => {
|
||||
const event = getEventsByAddress(e.event.kind, address)
|
||||
if (event) {
|
||||
pendingEvents.push(event)
|
||||
}
|
||||
})
|
||||
|
||||
return pendingEvents
|
||||
}
|
||||
|
||||
const handleGetDelegationRewards = async (mixId: number) => {
|
||||
if (!nymQueryClient) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const response = await nymQueryClient.getPendingDelegatorReward({
|
||||
address,
|
||||
mixId,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
const handleGetDelegations = useCallback(async () => {
|
||||
if (!nymQueryClient) {
|
||||
setDelegations(undefined)
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
setDelegations(undefined)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Get all mixnodes - Required to get the identity key for each delegation
|
||||
const mixnodes = await fetchMixnodes()
|
||||
|
||||
// Get delegations
|
||||
const delegationsResponse = await nymQueryClient.getDelegatorDelegations({
|
||||
delegator: address,
|
||||
})
|
||||
|
||||
// Get rewards for each delegation
|
||||
const rewardsResponse = await Promise.all(
|
||||
delegationsResponse.delegations.map((d: Delegation) =>
|
||||
handleGetDelegationRewards(d.mix_id)
|
||||
)
|
||||
)
|
||||
|
||||
// Get all pending events
|
||||
const pendingEvents = await handleGetPendingEvents()
|
||||
|
||||
const delegationsWithRewards: DelegationWithRewards[] = []
|
||||
|
||||
// Merge delegations with rewards and pending events
|
||||
delegationsResponse.delegations.forEach((d: Delegation, index: number) => {
|
||||
delegationsWithRewards.push({
|
||||
...d,
|
||||
pending: pendingEvents?.find((e: PendingEvent) =>
|
||||
e?.mixId === d.mix_id ? e.kind : undefined
|
||||
),
|
||||
identityKey:
|
||||
mixnodes?.find((m) => m.mix_id === d.mix_id)?.mix_node.identity_key ||
|
||||
'',
|
||||
rewards: rewardsResponse[index]?.amount_earned_detailed || '0',
|
||||
})
|
||||
})
|
||||
|
||||
// Add pending events that are not in the delegations list
|
||||
pendingEvents?.forEach((e) => {
|
||||
if (
|
||||
e &&
|
||||
!delegationsWithRewards.find(
|
||||
(d: DelegationWithRewards) => d.mix_id === e.mixId
|
||||
)
|
||||
) {
|
||||
delegationsWithRewards.push({
|
||||
mix_id: e.mixId,
|
||||
height: 0,
|
||||
cumulative_reward_ratio: '0',
|
||||
owner: address,
|
||||
amount: {
|
||||
amount: '0',
|
||||
denom: 'unym',
|
||||
},
|
||||
rewards: '0',
|
||||
identityKey:
|
||||
mixnodes?.find((m) => m.mix_id === e.mixId)?.mix_node
|
||||
.identity_key || '',
|
||||
pending: e,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setDelegations(delegationsWithRewards)
|
||||
|
||||
return undefined
|
||||
}, [address, nymQueryClient])
|
||||
|
||||
const handleDelegate = async (mixId: number, amount: string) => {
|
||||
if (!address) {
|
||||
throw new Error('Please connect your wallet')
|
||||
}
|
||||
|
||||
const amountToDelegate = (Number(amount) * 1000000).toString()
|
||||
const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }]
|
||||
try {
|
||||
const tx = await nymClient?.delegateToMixnode(
|
||||
{ mixId },
|
||||
fee,
|
||||
'Delegation from Nym Explorer',
|
||||
uNymFunds
|
||||
)
|
||||
|
||||
return tx as unknown as ExecuteResult
|
||||
} catch (e) {
|
||||
console.error('Failed to delegate to mixnode', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const handleUndelegate = async (mixId: number) => {
|
||||
const tx = await nymClient?.undelegateFromMixnode(
|
||||
{ mixId },
|
||||
fee,
|
||||
'Undelegation from Nym Explorer'
|
||||
)
|
||||
|
||||
return tx as unknown as ExecuteResult
|
||||
}
|
||||
|
||||
const contextValue: DelegationsState = useMemo(
|
||||
() => ({
|
||||
delegations,
|
||||
handleGetDelegations,
|
||||
handleDelegate,
|
||||
handleUndelegate,
|
||||
}),
|
||||
[delegations, handleGetDelegations]
|
||||
)
|
||||
|
||||
return (
|
||||
<DelegationsContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</DelegationsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDelegationsContext = () => {
|
||||
const context = useContext(DelegationsContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useDelegationsContext must be used within a DelegationsProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ApiState,
|
||||
GatewayReportResponse,
|
||||
UptimeStoryResponse,
|
||||
} from '@/app/typeDefs/explorer-api'
|
||||
import { Api } from '@/app/api'
|
||||
import { useApiState } from './hooks'
|
||||
|
||||
/**
|
||||
* This context provides the state for a single gateway by identity key.
|
||||
*/
|
||||
|
||||
interface GatewayState {
|
||||
uptimeReport?: ApiState<GatewayReportResponse>
|
||||
uptimeStory?: ApiState<UptimeStoryResponse>
|
||||
}
|
||||
|
||||
export const GatewayContext = React.createContext<GatewayState>({})
|
||||
|
||||
export const useGatewayContext = (): React.ContextType<typeof GatewayContext> =>
|
||||
React.useContext<GatewayState>(GatewayContext)
|
||||
|
||||
/**
|
||||
* Provides a state context for a gateway by identity
|
||||
* @param gatewayIdentityKey The identity key of the gateway
|
||||
*/
|
||||
export const GatewayContextProvider = ({
|
||||
gatewayIdentityKey,
|
||||
children,
|
||||
}: {
|
||||
gatewayIdentityKey: string
|
||||
children: JSX.Element
|
||||
}) => {
|
||||
const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] =
|
||||
useApiState<GatewayReportResponse>(
|
||||
gatewayIdentityKey,
|
||||
Api.fetchGatewayReportById,
|
||||
'Failed to fetch gateway uptime report by id'
|
||||
)
|
||||
|
||||
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] =
|
||||
useApiState<UptimeStoryResponse>(
|
||||
gatewayIdentityKey,
|
||||
Api.fetchGatewayUptimeStoryById,
|
||||
'Failed to fetch gateway uptime history'
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
// when the identity key changes, remove all previous data
|
||||
clearUptimeReportById()
|
||||
clearUptimeHistory()
|
||||
Promise.all([fetchUptimeReportById(), fetchUptimeHistory()])
|
||||
}, [gatewayIdentityKey])
|
||||
|
||||
const state = React.useMemo<GatewayState>(
|
||||
() => ({
|
||||
uptimeReport,
|
||||
uptimeStory,
|
||||
}),
|
||||
[uptimeReport, uptimeStory]
|
||||
)
|
||||
|
||||
return (
|
||||
<GatewayContext.Provider value={state}>{children}</GatewayContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react';
|
||||
import { ApiState } from '@/app/typeDefs/explorer-api';
|
||||
|
||||
/**
|
||||
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
|
||||
* @param id The id to fetch
|
||||
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
|
||||
* @param errorMessage A static error message, to use when no dynamic error message is returned
|
||||
*/
|
||||
export const useApiState = <T>(
|
||||
id: string,
|
||||
fn: (argId: string) => Promise<T>,
|
||||
errorMessage: string,
|
||||
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => {
|
||||
// stores the state
|
||||
const [value, setValue] = React.useState<ApiState<T>>();
|
||||
|
||||
// clear the value
|
||||
const clearValueFn = () => setValue(undefined);
|
||||
|
||||
// this provides a method to trigger the delegate to fetch data
|
||||
const wrappedFetchFn = React.useCallback(async () => {
|
||||
setValue({ isLoading: true });
|
||||
try {
|
||||
// keep previous state and set to loading
|
||||
setValue((prevState) => ({ ...prevState, isLoading: true }));
|
||||
|
||||
// delegate to user function to get data and set if successful
|
||||
const data = await fn(id);
|
||||
const newValue: ApiState<T> = {
|
||||
isLoading: false,
|
||||
data,
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
// return the caught error or create a new error with the static error message
|
||||
const newValue: ApiState<T> = {
|
||||
error: error instanceof Error ? error : new Error(errorMessage),
|
||||
isLoading: false,
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
}
|
||||
}, [setValue, fn, id, errorMessage]);
|
||||
return [value, wrappedFetchFn, clearValueFn];
|
||||
};
|
||||
@@ -1,297 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { PaletteMode } from '@mui/material'
|
||||
import {
|
||||
ApiState,
|
||||
BlockResponse,
|
||||
CountryDataResponse,
|
||||
GatewayResponse,
|
||||
MixNodeResponse,
|
||||
MixnodeStatus,
|
||||
SummaryOverviewResponse,
|
||||
ValidatorsResponse,
|
||||
Environment,
|
||||
DirectoryServiceProvider,
|
||||
} from '@/app/typeDefs/explorer-api'
|
||||
import { EnumFilterKey } from '@/app/typeDefs/filters'
|
||||
import { Api, getEnvironment } from '@/app/api'
|
||||
import { toPercentIntegerString } from '@/app/utils'
|
||||
import { NavOptionType, originalNavOptions } from './nav'
|
||||
|
||||
interface StateData {
|
||||
summaryOverview?: ApiState<SummaryOverviewResponse>
|
||||
block?: ApiState<BlockResponse>
|
||||
countryData?: ApiState<CountryDataResponse>
|
||||
gateways?: ApiState<GatewayResponse>
|
||||
globalError?: string | undefined
|
||||
mixnodes?: ApiState<MixNodeResponse>
|
||||
nodes?: ApiState<any>
|
||||
mode: PaletteMode
|
||||
validators?: ApiState<ValidatorsResponse>
|
||||
environment?: Environment
|
||||
serviceProviders?: ApiState<DirectoryServiceProvider[]>
|
||||
}
|
||||
|
||||
interface StateApi {
|
||||
fetchMixnodes: (
|
||||
status?: MixnodeStatus
|
||||
) => Promise<MixNodeResponse | undefined>
|
||||
filterMixnodes: (filters: any, status: any) => void
|
||||
fetchNodes: () => Promise<any>
|
||||
fetchNodeById: (id: number) => Promise<any>
|
||||
fetchAccountById: (accountAddr: string) => Promise<any>
|
||||
toggleMode: () => void
|
||||
}
|
||||
|
||||
type State = StateData & StateApi
|
||||
|
||||
export const MainContext = React.createContext<State>({
|
||||
mode: 'dark',
|
||||
toggleMode: () => undefined,
|
||||
filterMixnodes: () => null,
|
||||
fetchMixnodes: () => Promise.resolve(undefined),
|
||||
fetchNodes: async () => undefined,
|
||||
fetchNodeById: async () => undefined,
|
||||
fetchAccountById: async () => undefined,
|
||||
})
|
||||
|
||||
export const useMainContext = (): React.ContextType<typeof MainContext> =>
|
||||
React.useContext<State>(MainContext)
|
||||
|
||||
export const MainContextProvider: FCWithChildren = ({ children }) => {
|
||||
// network explorer environment
|
||||
const [environment, setEnvironment] = React.useState<Environment>()
|
||||
|
||||
// light/dark mode
|
||||
const [mode, setMode] = React.useState<PaletteMode>('dark')
|
||||
|
||||
// global / banner error messaging
|
||||
const [globalError] = React.useState<string>()
|
||||
|
||||
// various APIs for Overview page
|
||||
const [summaryOverview, setSummaryOverview] =
|
||||
React.useState<ApiState<SummaryOverviewResponse>>()
|
||||
const [nodes, setNodes] = React.useState<ApiState<any>>()
|
||||
const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>()
|
||||
const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>()
|
||||
const [validators, setValidators] =
|
||||
React.useState<ApiState<ValidatorsResponse>>()
|
||||
const [block, setBlock] = React.useState<ApiState<BlockResponse>>()
|
||||
const [countryData, setCountryData] =
|
||||
React.useState<ApiState<CountryDataResponse>>()
|
||||
const [serviceProviders, setServiceProviders] =
|
||||
React.useState<ApiState<DirectoryServiceProvider[]>>()
|
||||
|
||||
const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark'))
|
||||
|
||||
const fetchOverviewSummary = async () => {
|
||||
try {
|
||||
const data = await Api.fetchOverviewSummary()
|
||||
setSummaryOverview({ data, isLoading: false })
|
||||
} catch (error) {
|
||||
setSummaryOverview({
|
||||
error:
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error('Overview summary api fail'),
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const fetchMixnodes = async () => {
|
||||
let data
|
||||
setMixnodes((d) => ({ ...d, isLoading: true }))
|
||||
try {
|
||||
data = await Api.fetchMixnodes()
|
||||
setMixnodes({ data, isLoading: false })
|
||||
} catch (error) {
|
||||
setMixnodes({
|
||||
error: error instanceof Error ? error : new Error('Mixnode api fail'),
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
const filterMixnodes = async (
|
||||
filters: { [key in EnumFilterKey]: number[] },
|
||||
status?: MixnodeStatus
|
||||
) => {
|
||||
setMixnodes((d) => ({ ...d, isLoading: true }))
|
||||
const mxns = await Api.fetchMixnodes()
|
||||
|
||||
const filtered = mxns?.filter(
|
||||
(m) =>
|
||||
+m.profit_margin_percent >= filters.profitMargin[0] / 100 &&
|
||||
+m.profit_margin_percent <= filters.profitMargin[1] / 100 &&
|
||||
m.stake_saturation >= filters.stakeSaturation[0] &&
|
||||
m.stake_saturation <= filters.stakeSaturation[1] &&
|
||||
m.avg_uptime >= filters.routingScore[0] &&
|
||||
m.avg_uptime <= filters.routingScore[1]
|
||||
)
|
||||
|
||||
setMixnodes({ data: filtered, isLoading: false })
|
||||
}
|
||||
|
||||
const fetchGateways = async () => {
|
||||
setGateways((d) => ({ ...d, isLoading: true }))
|
||||
try {
|
||||
const data = await Api.fetchGateways()
|
||||
setGateways({ data, isLoading: false })
|
||||
} catch (error) {
|
||||
setGateways({
|
||||
error: error instanceof Error ? error : new Error('Gateways api fail'),
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
const fetchValidators = async () => {
|
||||
try {
|
||||
const data = await Api.fetchValidators()
|
||||
setValidators({ data, isLoading: false })
|
||||
} catch (error) {
|
||||
setValidators({
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Validators api fail'),
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
const fetchBlock = async () => {
|
||||
try {
|
||||
const data = await Api.fetchBlock()
|
||||
setBlock({ data, isLoading: false })
|
||||
} catch (error) {
|
||||
setBlock({
|
||||
error: error instanceof Error ? error : new Error('Block api fail'),
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
const fetchCountryData = async () => {
|
||||
setCountryData({ data: undefined, isLoading: true })
|
||||
try {
|
||||
const res = await Api.fetchCountryData()
|
||||
setCountryData({ data: res, isLoading: false })
|
||||
} catch (error) {
|
||||
setCountryData({
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Country Data api fail'),
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const fetchServiceProviders = async () => {
|
||||
setServiceProviders({ data: undefined, isLoading: true })
|
||||
try {
|
||||
const res = await Api.fetchServiceProviders()
|
||||
const resWithRoutingScorePercentage = res.map((item) => ({
|
||||
...item,
|
||||
routing_score: item.routing_score
|
||||
? `${toPercentIntegerString(item.routing_score.toString())}%`
|
||||
: item.routing_score,
|
||||
}))
|
||||
setServiceProviders({
|
||||
data: resWithRoutingScorePercentage,
|
||||
isLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
setServiceProviders({
|
||||
error:
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error('Service provider api fail'),
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNodes = async () => {
|
||||
setNodes({ data: undefined, isLoading: true })
|
||||
try {
|
||||
const res = await Api.fetchNodes();
|
||||
res.forEach((node: any) => node.total_stake =
|
||||
Math.round(Number.parseFloat(node.rewarding_details?.operator || "0")
|
||||
+ Number.parseFloat(node.rewarding_details?.delegates || "0"))
|
||||
);
|
||||
setNodes({
|
||||
data: res.sort((a: any, b: any) => b.total_stake - a.total_stake),
|
||||
isLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
setNodes({
|
||||
error:
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error('Service provider api fail'),
|
||||
isLoading: false,
|
||||
})
|
||||
} };
|
||||
|
||||
const fetchNodeById = async (id: number) => {
|
||||
const res = await Api.fetchNodeById(id);
|
||||
return res;
|
||||
};
|
||||
|
||||
const fetchAccountById = async (id: string) => {
|
||||
const res = await Api.fetchAccountById(id);
|
||||
return res;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (environment === 'mainnet') {
|
||||
fetchServiceProviders()
|
||||
}
|
||||
}, [environment])
|
||||
|
||||
React.useEffect(() => {
|
||||
setEnvironment(getEnvironment())
|
||||
Promise.all([
|
||||
fetchOverviewSummary(),
|
||||
fetchGateways(),
|
||||
fetchValidators(),
|
||||
fetchBlock(),
|
||||
fetchCountryData(),
|
||||
])
|
||||
}, [])
|
||||
|
||||
const state = React.useMemo<State>(
|
||||
() => ({
|
||||
environment,
|
||||
block,
|
||||
countryData,
|
||||
gateways,
|
||||
globalError,
|
||||
mixnodes,
|
||||
mode,
|
||||
nodes,
|
||||
summaryOverview,
|
||||
validators,
|
||||
serviceProviders,
|
||||
toggleMode,
|
||||
fetchMixnodes,
|
||||
filterMixnodes,
|
||||
fetchNodes,
|
||||
fetchNodeById,
|
||||
fetchAccountById,
|
||||
}),
|
||||
[
|
||||
environment,
|
||||
block,
|
||||
countryData,
|
||||
gateways,
|
||||
globalError,
|
||||
mixnodes,
|
||||
mode,
|
||||
nodes,
|
||||
summaryOverview,
|
||||
validators,
|
||||
serviceProviders,
|
||||
]
|
||||
)
|
||||
|
||||
return <MainContext.Provider value={state}>{children}</MainContext.Provider>
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ApiState,
|
||||
DelegationsResponse,
|
||||
UniqDelegationsResponse,
|
||||
MixNodeDescriptionResponse,
|
||||
MixNodeEconomicDynamicsStatsResponse,
|
||||
MixNodeResponseItem,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
UptimeStoryResponse,
|
||||
} from '../typeDefs/explorer-api'
|
||||
import { Api } from '../api'
|
||||
import { useApiState } from './hooks'
|
||||
import {
|
||||
mixNodeResponseItemToMixnodeRowType,
|
||||
MixnodeRowType,
|
||||
} from '../components/MixNodes'
|
||||
|
||||
/**
|
||||
* This context provides the state for a single mixnode by identity key.
|
||||
*/
|
||||
|
||||
interface MixnodeState {
|
||||
delegations?: ApiState<DelegationsResponse>
|
||||
uniqDelegations?: ApiState<UniqDelegationsResponse>
|
||||
description?: ApiState<MixNodeDescriptionResponse>
|
||||
economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse>
|
||||
mixNode?: ApiState<MixNodeResponseItem | undefined>
|
||||
mixNodeRow?: MixnodeRowType
|
||||
stats?: ApiState<StatsResponse>
|
||||
status?: ApiState<StatusResponse>
|
||||
uptimeStory?: ApiState<UptimeStoryResponse>
|
||||
}
|
||||
|
||||
export const MixnodeContext = React.createContext<MixnodeState>({})
|
||||
|
||||
export const useMixnodeContext = (): React.ContextType<typeof MixnodeContext> =>
|
||||
React.useContext<MixnodeState>(MixnodeContext)
|
||||
|
||||
interface MixnodeContextProviderProps {
|
||||
mixId: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a state context for a mixnode by identity
|
||||
* @param mixId The mixID of the mixnode
|
||||
*/
|
||||
export const MixnodeContextProvider: FCWithChildren<
|
||||
MixnodeContextProviderProps
|
||||
> = ({ mixId, children }) => {
|
||||
const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState<
|
||||
MixNodeResponseItem | undefined
|
||||
>(mixId, Api.fetchMixnodeByID, 'Failed to fetch mixnode by id')
|
||||
|
||||
const [mixNodeRow, setMixnodeRow] = React.useState<
|
||||
MixnodeRowType | undefined
|
||||
>()
|
||||
|
||||
const [delegations, fetchDelegations, clearDelegations] =
|
||||
useApiState<DelegationsResponse>(
|
||||
mixId,
|
||||
Api.fetchDelegationsById,
|
||||
'Failed to fetch delegations for mixnode'
|
||||
)
|
||||
|
||||
const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] =
|
||||
useApiState<UniqDelegationsResponse>(
|
||||
mixId,
|
||||
Api.fetchUniqDelegationsById,
|
||||
'Failed to fetch delegations for mixnode'
|
||||
)
|
||||
|
||||
const [status, fetchStatus, clearStatus] = useApiState<StatusResponse>(
|
||||
mixId,
|
||||
Api.fetchStatusById,
|
||||
'Failed to fetch mixnode status'
|
||||
)
|
||||
|
||||
const [stats, fetchStats, clearStats] = useApiState<StatsResponse>(
|
||||
mixId,
|
||||
Api.fetchStatsById,
|
||||
'Failed to fetch mixnode stats'
|
||||
)
|
||||
|
||||
const [description, fetchDescription, clearDescription] =
|
||||
useApiState<MixNodeDescriptionResponse>(
|
||||
mixId,
|
||||
Api.fetchMixnodeDescriptionById,
|
||||
'Failed to fetch mixnode description'
|
||||
)
|
||||
|
||||
const [
|
||||
economicDynamicsStats,
|
||||
fetchEconomicDynamicsStats,
|
||||
clearEconomicDynamicsStats,
|
||||
] = useApiState<MixNodeEconomicDynamicsStatsResponse>(
|
||||
mixId,
|
||||
Api.fetchMixnodeEconomicDynamicsStatsById,
|
||||
'Failed to fetch mixnode dynamics stats by id'
|
||||
)
|
||||
|
||||
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] =
|
||||
useApiState<UptimeStoryResponse>(
|
||||
mixId,
|
||||
Api.fetchUptimeStoryById,
|
||||
'Failed to fetch mixnode uptime history'
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
// when the identity key changes, remove all previous data
|
||||
clearMixnodeById()
|
||||
clearDelegations()
|
||||
clearUniqDelegations()
|
||||
clearStatus()
|
||||
clearStats()
|
||||
clearDescription()
|
||||
clearEconomicDynamicsStats()
|
||||
clearUptimeHistory()
|
||||
|
||||
// fetch the mixnode, then get all the other stuff
|
||||
fetchMixnodeById().then((value) => {
|
||||
if (!value.data || value.error) {
|
||||
setMixnodeRow(undefined)
|
||||
return
|
||||
}
|
||||
setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data))
|
||||
Promise.all([
|
||||
fetchDelegations(),
|
||||
fetchUniqDelegations(),
|
||||
fetchStatus(),
|
||||
fetchStats(),
|
||||
fetchDescription(),
|
||||
fetchEconomicDynamicsStats(),
|
||||
fetchUptimeHistory(),
|
||||
])
|
||||
})
|
||||
}, [mixId])
|
||||
|
||||
const state = React.useMemo<MixnodeState>(
|
||||
() => ({
|
||||
delegations,
|
||||
uniqDelegations,
|
||||
mixNode,
|
||||
mixNodeRow,
|
||||
description,
|
||||
economicDynamicsStats,
|
||||
stats,
|
||||
status,
|
||||
uptimeStory,
|
||||
}),
|
||||
[
|
||||
{
|
||||
delegations,
|
||||
uniqDelegations,
|
||||
mixNode,
|
||||
mixNodeRow,
|
||||
description,
|
||||
economicDynamicsStats,
|
||||
stats,
|
||||
status,
|
||||
uptimeStory,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { DelegateIcon } from '@/app/icons/DelevateSVG'
|
||||
import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants'
|
||||
import { OverviewSVG } from '@/app/icons/OverviewSVG'
|
||||
import { NodemapSVG } from '@/app/icons/NodemapSVG'
|
||||
import { NetworkComponentsSVG } from '@/app/icons/NetworksSVG'
|
||||
|
||||
export type NavOptionType = {
|
||||
url: string
|
||||
title: string
|
||||
Icon?: React.ReactNode
|
||||
nested?: NavOptionType[]
|
||||
isExpandedChild?: boolean
|
||||
isExternal?: boolean
|
||||
}
|
||||
|
||||
export const originalNavOptions: NavOptionType[] = [
|
||||
{
|
||||
url: '/',
|
||||
title: 'Overview',
|
||||
Icon: <OverviewSVG />,
|
||||
},
|
||||
{
|
||||
url: '/network-components',
|
||||
title: 'Network Components',
|
||||
Icon: <NetworkComponentsSVG />,
|
||||
nested: [
|
||||
{
|
||||
url: '/network-components/nodes',
|
||||
title: 'Nodes',
|
||||
},
|
||||
{
|
||||
url: '/network-components/mixnodes',
|
||||
title: 'Mixnodes (legacy)',
|
||||
},
|
||||
{
|
||||
url: '/network-components/gateways',
|
||||
title: 'Gateways (legacy)',
|
||||
},
|
||||
{
|
||||
url: `${BLOCK_EXPLORER_BASE_URL}/validators`,
|
||||
title: 'Validators',
|
||||
isExternal: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
url: '/nodemap',
|
||||
title: 'Nodemap',
|
||||
Icon: <NodemapSVG />,
|
||||
},
|
||||
{
|
||||
url: '/delegations',
|
||||
title: 'Delegations',
|
||||
Icon: <DelegateIcon sx={{ color: 'white' }} />,
|
||||
},
|
||||
]
|
||||
@@ -1,77 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ApiState,
|
||||
NymNodeReportResponse,
|
||||
UptimeStoryResponse,
|
||||
} from '@/app/typeDefs/explorer-api'
|
||||
import { Api } from '@/app/api'
|
||||
import { useApiState } from './hooks'
|
||||
|
||||
/**
|
||||
* This context provides the state for a single gateway by identity key.
|
||||
*/
|
||||
|
||||
interface NymNodeState {
|
||||
uptimeReport?: ApiState<NymNodeReportResponse>
|
||||
uptimeHistory?: ApiState<UptimeStoryResponse>
|
||||
}
|
||||
|
||||
export const NymNodeContext = React.createContext<NymNodeState>({})
|
||||
|
||||
export const useNymNodeContext = (): React.ContextType<typeof NymNodeContext> =>
|
||||
React.useContext<NymNodeState>(NymNodeContext)
|
||||
|
||||
/**
|
||||
* Provides a state context for a gateway by identity
|
||||
* @param gatewayIdentityKey The identity key of the gateway
|
||||
*/
|
||||
export const NymNodeContextProvider = ({
|
||||
nymNodeId,
|
||||
children,
|
||||
}: {
|
||||
nymNodeId: string
|
||||
children: JSX.Element
|
||||
}) => {
|
||||
const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] =
|
||||
useApiState<any>(
|
||||
nymNodeId,
|
||||
Api.fetchNymNodePerformanceById,
|
||||
'Failed to fetch gateway uptime report by id'
|
||||
)
|
||||
|
||||
const [uptimeHistory, fetchUptimeHistory, clearUptimeHistory] =
|
||||
useApiState<UptimeStoryResponse>(
|
||||
nymNodeId,
|
||||
async (arg) => {
|
||||
const res = await Api.fetchNymNodeUptimeHistoryById(arg);
|
||||
const uptimeHistory: UptimeStoryResponse = {
|
||||
history: res.history.data,
|
||||
identity: '',
|
||||
owner: '',
|
||||
}
|
||||
return uptimeHistory;
|
||||
},
|
||||
'Failed to fetch gateway uptime history'
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
// when the identity key changes, remove all previous data
|
||||
clearUptimeReportById()
|
||||
clearUptimeHistory()
|
||||
Promise.all([fetchUptimeReportById(), fetchUptimeHistory()])
|
||||
}, [nymNodeId])
|
||||
|
||||
const state = React.useMemo<NymNodeState>(
|
||||
() => ({
|
||||
uptimeReport,
|
||||
uptimeHistory,
|
||||
}),
|
||||
[uptimeReport, uptimeHistory]
|
||||
)
|
||||
|
||||
return (
|
||||
<NymNodeContext.Provider value={state}>{children}</NymNodeContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useChain } from '@cosmos-kit/react'
|
||||
import { Wallet } from '@cosmos-kit/core'
|
||||
import { unymToNym } from '@/app/utils/currency'
|
||||
import { useNymClient } from '@/app/hooks'
|
||||
import {
|
||||
MixnetClient,
|
||||
MixnetQueryClient,
|
||||
} from '@nymproject/contract-clients/Mixnet.client'
|
||||
import { COSMOS_KIT_USE_CHAIN } from '@/app/api/constants'
|
||||
|
||||
interface WalletState {
|
||||
balance: { status: 'loading' | 'success'; data?: string }
|
||||
address?: string
|
||||
isWalletConnected: boolean
|
||||
isWalletConnecting: boolean
|
||||
wallet?: Wallet
|
||||
nymClient?: MixnetClient
|
||||
nymQueryClient?: MixnetQueryClient
|
||||
connectWallet: () => Promise<void>
|
||||
disconnectWallet: () => Promise<void>
|
||||
}
|
||||
|
||||
export const WalletContext = createContext<WalletState>({
|
||||
address: undefined,
|
||||
balance: { status: 'loading', data: undefined },
|
||||
isWalletConnected: false,
|
||||
isWalletConnecting: false,
|
||||
nymClient: undefined,
|
||||
nymQueryClient: undefined,
|
||||
connectWallet: async () => {
|
||||
throw new Error('Please connect your wallet')
|
||||
},
|
||||
disconnectWallet: async () => {
|
||||
throw new Error('Please connect your wallet')
|
||||
},
|
||||
})
|
||||
|
||||
export const WalletProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [balance, setBalance] = useState<WalletState['balance']>({
|
||||
status: 'loading',
|
||||
data: undefined,
|
||||
})
|
||||
|
||||
const {
|
||||
connect,
|
||||
disconnect,
|
||||
wallet,
|
||||
address,
|
||||
isWalletConnected,
|
||||
isWalletConnecting,
|
||||
getCosmWasmClient,
|
||||
} = useChain(COSMOS_KIT_USE_CHAIN)
|
||||
|
||||
const { nymClient, nymQueryClient } = useNymClient(address)
|
||||
|
||||
const getBalance = async (walletAddress: string) => {
|
||||
const account = await getCosmWasmClient()
|
||||
const uNYMBalance = await account.getBalance(walletAddress, 'unym')
|
||||
const NYMBalance = unymToNym(uNYMBalance.amount)
|
||||
|
||||
return NYMBalance
|
||||
}
|
||||
|
||||
const init = async (walletAddress: string) => {
|
||||
const walletBalance = await getBalance(walletAddress)
|
||||
setBalance({ status: 'success', data: walletBalance })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isWalletConnected && address) {
|
||||
init(address)
|
||||
}
|
||||
}, [address, isWalletConnected])
|
||||
|
||||
const handleConnectWallet = async () => {
|
||||
await connect()
|
||||
}
|
||||
|
||||
const handleDisconnectWallet = async () => {
|
||||
await disconnect()
|
||||
setBalance({ status: 'loading', data: undefined })
|
||||
}
|
||||
|
||||
const contextValue: WalletState = useMemo(
|
||||
() => ({
|
||||
address,
|
||||
balance,
|
||||
wallet,
|
||||
isWalletConnected,
|
||||
isWalletConnecting,
|
||||
nymClient,
|
||||
nymQueryClient,
|
||||
connectWallet: handleConnectWallet,
|
||||
disconnectWallet: handleDisconnectWallet,
|
||||
}),
|
||||
[
|
||||
address,
|
||||
balance,
|
||||
wallet,
|
||||
isWalletConnected,
|
||||
isWalletConnecting,
|
||||
nymClient,
|
||||
nymQueryClient,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<WalletContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</WalletContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useWalletContext = () => useContext(WalletContext)
|
||||
@@ -1,311 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { DelegationModal, DelegationModalProps, Title } from '@/app/components'
|
||||
import { useWalletContext } from '@/app/context/wallet'
|
||||
import { unymToNym } from '@/app/utils/currency'
|
||||
import {
|
||||
DelegationWithRewards,
|
||||
DelegationsProvider,
|
||||
PendingEvent,
|
||||
useDelegationsContext,
|
||||
} from '@/app/context/delegations'
|
||||
import { urls } from '@/app/utils'
|
||||
import { useClipboard } from 'use-clipboard-copy'
|
||||
import { Close } from '@mui/icons-material'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
MRT_ColumnDef,
|
||||
MaterialReactTable,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table'
|
||||
|
||||
const mapToDelegationsRow = (
|
||||
delegation: DelegationWithRewards,
|
||||
index: number
|
||||
) => ({
|
||||
identity: delegation.identityKey,
|
||||
mix_id: delegation.mix_id,
|
||||
amount: `${unymToNym(delegation.amount.amount)} NYM`,
|
||||
rewards: `${unymToNym(delegation.rewards)} NYM`,
|
||||
id: index,
|
||||
pending: delegation.pending,
|
||||
})
|
||||
|
||||
const Banner = ({ onClose }: { onClose: () => void }) => {
|
||||
const { copy } = useClipboard()
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="info"
|
||||
sx={{ mb: 3, fontSize: 'medium', width: '100%' }}
|
||||
action={
|
||||
<IconButton size="small" onClick={onClose}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<AlertTitle> Mobile Delegations Beta</AlertTitle>
|
||||
<Box>
|
||||
<Typography>
|
||||
This is a beta release for mobile delegations If you have any feedback
|
||||
or feature suggestions contact us at support@nymte.ch
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => copy('support@nymte.ch')}
|
||||
sx={{ display: 'inline-block' }}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
const DelegationsPage = () => {
|
||||
const [confirmationModalProps, setConfirmationModalProps] = React.useState<
|
||||
DelegationModalProps | undefined
|
||||
>()
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [showBanner, setShowBanner] = React.useState(true)
|
||||
|
||||
const { isWalletConnected } = useWalletContext()
|
||||
const { handleGetDelegations, handleUndelegate, delegations } =
|
||||
useDelegationsContext()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
const fetchDelegations = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await handleGetDelegations()
|
||||
} catch (error) {
|
||||
setConfirmationModalProps({
|
||||
status: 'error',
|
||||
message: "Couldn't fetch delegations. Please try again later.",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
fetchDelegations()
|
||||
}, 60_000)
|
||||
}
|
||||
}
|
||||
|
||||
fetchDelegations()
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}, [handleGetDelegations])
|
||||
|
||||
const getTooltipTitle = (pending: PendingEvent) => {
|
||||
if (pending?.kind === 'undelegate') {
|
||||
return 'You have an undelegation pending'
|
||||
}
|
||||
|
||||
if (pending?.kind === 'delegate') {
|
||||
return `You have a delegation pending worth ${unymToNym(
|
||||
pending.amount.amount
|
||||
)} NYM`
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const onUndelegate = useCallback(
|
||||
async (mixId: number) => {
|
||||
setConfirmationModalProps({ status: 'loading' })
|
||||
|
||||
try {
|
||||
const tx = await handleUndelegate(mixId)
|
||||
|
||||
if (tx) {
|
||||
setConfirmationModalProps({
|
||||
status: 'success',
|
||||
message: 'Undelegation can take up to one hour to process',
|
||||
transactions: [
|
||||
{
|
||||
url: `${urls('MAINNET').blockExplorer}/transaction/${
|
||||
tx.transactionHash
|
||||
}`,
|
||||
hash: tx.transactionHash,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setConfirmationModalProps({ status: 'error', message: error.message })
|
||||
}
|
||||
}
|
||||
},
|
||||
[handleUndelegate]
|
||||
)
|
||||
|
||||
const columns = useMemo<
|
||||
MRT_ColumnDef<ReturnType<typeof mapToDelegationsRow>>[]
|
||||
>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'delegations-data',
|
||||
header: 'Delegations Data',
|
||||
columns: [
|
||||
{
|
||||
id: 'identity',
|
||||
accessorKey: 'identity',
|
||||
header: 'Identity Key',
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
id: 'mix_id',
|
||||
accessorKey: 'mix_id',
|
||||
header: 'Mix ID',
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
accessorKey: 'amount',
|
||||
header: 'Amount',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
id: 'rewards',
|
||||
accessorKey: 'rewards',
|
||||
header: 'Rewards',
|
||||
width: 150,
|
||||
enableColumnFilters: false,
|
||||
},
|
||||
{
|
||||
id: 'undelegate',
|
||||
accessorKey: 'undelegate',
|
||||
header: '',
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
Filter: () => null,
|
||||
Cell: ({ row }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{ width: '100%', display: 'flex', justifyContent: 'end' }}
|
||||
>
|
||||
{row.original.pending ? (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={getTooltipTitle(row.original.pending)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
PopperProps={{}}
|
||||
>
|
||||
<Chip size="small" label="Pending events" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onUndelegate(row.original.mix_id)
|
||||
}}
|
||||
>
|
||||
Undelegate
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}, [onUndelegate])
|
||||
|
||||
const data = useMemo(() => {
|
||||
return (delegations || []).map(mapToDelegationsRow)
|
||||
}, [delegations])
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data,
|
||||
enableFullScreenToggle: false,
|
||||
state: {
|
||||
isLoading,
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: { right: ['undelegate'] },
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{confirmationModalProps && (
|
||||
<DelegationModal
|
||||
{...confirmationModalProps}
|
||||
open={Boolean(confirmationModalProps)}
|
||||
onClose={async () => {
|
||||
if (confirmationModalProps.status === 'success') {
|
||||
await handleGetDelegations()
|
||||
}
|
||||
setConfirmationModalProps(undefined)
|
||||
}}
|
||||
sx={{
|
||||
width: {
|
||||
xs: '90%',
|
||||
sm: 600,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showBanner && <Banner onClose={() => setShowBanner(false)} />}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Title text="Your Delegations" />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => router.push('/network-components/mixnodes')}
|
||||
>
|
||||
Delegate
|
||||
</Button>
|
||||
</Box>
|
||||
{!isWalletConnected ? (
|
||||
<Box>
|
||||
<Typography mb={2} variant="h6">
|
||||
Connect your wallet to view your delegations.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<Card
|
||||
sx={{
|
||||
mt: 2,
|
||||
padding: 2,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<MaterialReactTable table={table} />
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const Delegations = () => (
|
||||
<DelegationsProvider>
|
||||
<DelegationsPage />
|
||||
</DelegationsProvider>
|
||||
)
|
||||
|
||||
export default Delegations
|
||||
@@ -1,26 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { FallbackProps } from 'react-error-boundary'
|
||||
import { Alert, AlertTitle, Container } from '@mui/material'
|
||||
import { NymThemeProvider } from '@nymproject/mui-theme'
|
||||
import { NymLogo } from '@nymproject/react/logo/NymLogo'
|
||||
|
||||
export const ErrorBoundaryContent: FCWithChildren<FallbackProps> = ({
|
||||
error,
|
||||
}) => (
|
||||
<NymThemeProvider mode="dark">
|
||||
<Container sx={{ py: 4 }}>
|
||||
<NymLogo height="75px" width="75px" />
|
||||
<h1>Oh no! Sorry, something went wrong</h1>
|
||||
<Alert severity="error" data-testid="error-message">
|
||||
<AlertTitle>{error.name}</AlertTitle>
|
||||
{error.message}
|
||||
</Alert>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Alert severity="info" sx={{ mt: 2 }} data-testid="stack-trace">
|
||||
<AlertTitle>Stack trace</AlertTitle>
|
||||
{error.stack}
|
||||
</Alert>
|
||||
)}
|
||||
</Container>
|
||||
</NymThemeProvider>
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './useIsMobile';
|
||||
export * from './useIsMounted';
|
||||
export * from './useGetMixnodeStatusColor';
|
||||
export * from './useNymClient';
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from '@mui/material';
|
||||
import { MixnodeStatus } from '@/app/typeDefs/explorer-api';
|
||||
|
||||
export const useGetMixNodeStatusColor = (status: MixnodeStatus) => {
|
||||
const theme = useTheme();
|
||||
|
||||
switch (status) {
|
||||
case MixnodeStatus.active:
|
||||
return theme.palette.nym.networkExplorer.mixnodes.status.active;
|
||||
|
||||
case MixnodeStatus.standby:
|
||||
return theme.palette.nym.networkExplorer.mixnodes.status.standby;
|
||||
|
||||
default:
|
||||
return theme.palette.nym.networkExplorer.mixnodes.status.inactive;
|
||||
}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Breakpoint, useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const useIsMobile = (queryInput: number | Breakpoint = 'md') => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(queryInput));
|
||||
|
||||
return isMobile;
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
export function useIsMounted(): () => boolean {
|
||||
const ref = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = true;
|
||||
return () => {
|
||||
ref.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useCallback(() => ref.current, [ref]);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
'use client'
|
||||
|
||||
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 { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from '@/app/api/constants'
|
||||
|
||||
export const useNymClient = (address?: string) => {
|
||||
const [nymClient, setNymClient] = useState<MixnetClient>()
|
||||
const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>()
|
||||
|
||||
const { 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(
|
||||
cosmWasmSigningClient as any,
|
||||
address,
|
||||
NYM_MIXNET_CONTRACT
|
||||
)
|
||||
const queryClient = new contracts.Mixnet.MixnetQueryClient(
|
||||
cosmWasmClient as any,
|
||||
NYM_MIXNET_CONTRACT
|
||||
)
|
||||
|
||||
setNymClient(client)
|
||||
setNymQueryClient(queryClient)
|
||||
}
|
||||
|
||||
init()
|
||||
}
|
||||
}, [address, getCosmWasmClient, getSigningCosmWasmClient])
|
||||
|
||||
return { nymClient, nymQueryClient }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon, SvgIconProps } from '@mui/material';
|
||||
|
||||
export const DelegateIcon = (props: SvgIconProps) => (
|
||||
<SvgIcon {...props}>
|
||||
<path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" />
|
||||
<path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" />
|
||||
<rect x="18" y="12" width="2" height="3" />
|
||||
<rect x="18" y="17" width="2" height="3" />
|
||||
<rect x="4" y="17" width="2" height="3" />
|
||||
</SvgIcon>
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const ElipsSVG: FCWithChildren = () => (
|
||||
<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>
|
||||
);
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const GatewaysSVG: FCWithChildren = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.2 12H22.7" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" />
|
||||
<path d="M1.30005 12H12" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" />
|
||||
<path
|
||||
d="M20.1 9.40015L22.7 12.0001L20.1 14.6001"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const LightSwitchSVG: FCWithChildren = () => {
|
||||
const { palette } = useTheme();
|
||||
return (
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z"
|
||||
fill={palette.background.default}
|
||||
/>
|
||||
<path d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z" fill={palette.text.primary} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const MixnodesSVG: FCWithChildren = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" />
|
||||
<path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" />
|
||||
<path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" />
|
||||
<path d="M2.97681 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" />
|
||||
<path d="M23.0437 23.0621L2.97681 2.99512" stroke={color} strokeMiterlimit="10" />
|
||||
<path d="M13.0103 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" />
|
||||
<path d="M2.97681 2.99512L13.0103 23.0621" stroke={color} strokeMiterlimit="10" />
|
||||
<path
|
||||
d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const MobileDrawerClose: FCWithChildren = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -5 24 24" width="25" height="25" {...props}>
|
||||
<path
|
||||
d="M0 12H13V10H0V12ZM0 7H10V5H0V7ZM0 0V2H13V0H0ZM18 9.59L14.42 6L18 2.41L16.59 1L11.59 6L16.59 11L18 9.59Z"
|
||||
fill="#F2F2F2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user