remove old explorer references
This commit is contained in:
benedettadavico
2025-06-11 16:02:19 +02:00
parent f313e95e2f
commit 3ac58e0c49
297 changed files with 5 additions and 97804 deletions
-1
View File
@@ -5,7 +5,6 @@ on:
paths:
- 'clients/**'
- 'common/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'nym-api/**'
+5 -3
View File
@@ -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
-1
View File
@@ -23,7 +23,6 @@ fn main() {
"REWARDING_VALIDATOR_ADDRESS",
"NYM_API",
"NYXD_WS",
"EXPLORER_API",
"NYM_VPN_API",
];
-3
View File
@@ -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);
}
-11
View File
@@ -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);
-1
View File
@@ -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";
-1
View File
@@ -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
-1
View File
@@ -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"
-1
View File
@@ -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/
-1
View File
@@ -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/
-1
View File
@@ -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/
-3
View File
@@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals"]
}
-36
View File
@@ -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
-36
View File
@@ -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.
-11
View File
@@ -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 }
-407
View File
@@ -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
-39
View File
@@ -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 || '';
-217
View File
@@ -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),
});
-56
View File
@@ -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>
)
}
-28
View File
@@ -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&nbsp;&nbsp;
</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
-70
View File
@@ -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>
)
}
-14
View File
@@ -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';
-129
View File
@@ -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>
</>
)
}
-9
View File
@@ -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
-252
View File
@@ -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
}
-69
View File
@@ -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>
)
}
-49
View File
@@ -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];
};
-297
View File
@@ -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>
}
-173
View File
@@ -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>
)
}
-59
View File
@@ -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' }} />,
},
]
-77
View File
@@ -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>
)
}
-123
View File
@@ -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)
-311
View File
@@ -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>
)
-4
View File
@@ -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;
}
};
-11
View File
@@ -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;
};
-16
View File
@@ -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 }
}
-12
View File
@@ -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>
);
-20
View File
@@ -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>
);
-28
View File
@@ -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>
);
};
-92
View File
@@ -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