Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42efe6d0d6 | |||
| 434b8b85c2 | |||
| a95c0530d8 | |||
| f252ecd691 | |||
| 9463f20a3a | |||
| 152b3635d1 | |||
| d82a8180e5 | |||
| 3a8cb097ad | |||
| dc5fb093e1 | |||
| d1a48e6ec1 | |||
| 2a6fe6624d | |||
| f52f07f6ec | |||
| b709d3ba0b | |||
| 40dd7dc95e | |||
| b2f6836756 | |||
| 87e429d78a | |||
| 4178809555 | |||
| 9de5d7213a | |||
| 94eb362a71 | |||
| 0f615f48f2 | |||
| d511611641 | |||
| 17d3ff2d77 | |||
| dd3dcfa7fe | |||
| 86ea2d23cb | |||
| 42a37442e8 | |||
| 6b24f081e1 | |||
| 6e5d0dac1b | |||
| 5f2740bf66 | |||
| ecb15034d3 | |||
| bd49c222a3 | |||
| 0d397ab5cc |
@@ -26,6 +26,7 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -37,6 +37,7 @@ jobs:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
IPINFO_API_TOKEN: ${{ secrets.IPINFO_API_TOKEN }}
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler
|
||||
|
||||
@@ -322,7 +322,7 @@ serde_with = "3.9.0"
|
||||
serde_yaml = "0.9.25"
|
||||
sha2 = "0.10.8"
|
||||
si-scale = "0.2.3"
|
||||
sphinx-packet = "0.1.1"
|
||||
sphinx-packet = "0.3.1"
|
||||
sqlx = "0.7.4"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
@@ -330,7 +330,7 @@ subtle-encoding = "0.5"
|
||||
syn = "1"
|
||||
sysinfo = "0.33.0"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.43"
|
||||
tar = "0.4.44"
|
||||
tempfile = "3.15"
|
||||
thiserror = "2.0"
|
||||
time = "0.3.37"
|
||||
|
||||
@@ -67,3 +67,13 @@ As a general approach, licensing is as follows this pattern:
|
||||
- documentation is Apache 2.0 or CC0-1.0
|
||||
|
||||
Nym Node Operators and Validators Terms and Conditions can be found [here](https://nym.com/operators-validators-terms).
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
@@ -204,15 +204,15 @@ impl<C, St> GatewayClient<C, St> {
|
||||
"Attemting to establish connection to gateway at: {}",
|
||||
self.gateway_address
|
||||
);
|
||||
let (ws_stream, _) = connect_async(&self.gateway_address).await?;
|
||||
let (ws_stream, _) = connect_async(
|
||||
&self.gateway_address,
|
||||
#[cfg(unix)]
|
||||
self.connection_fd_callback.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.connection = SocketState::Available(Box::new(ws_stream));
|
||||
|
||||
#[cfg(unix)]
|
||||
if let (Some(callback), Some(fd)) = (self.connection_fd_callback.as_ref(), self.ws_fd()) {
|
||||
callback.as_ref()(fd);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use crate::error::GatewayClientError;
|
||||
|
||||
use nym_http_api_client::HickoryDnsResolver;
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
os::fd::{AsRawFd, RawFd},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
use tungstenite::handshake::client::Response;
|
||||
@@ -11,7 +16,10 @@ use std::net::SocketAddr;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn connect_async(
|
||||
endpoint: &str,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), GatewayClientError> {
|
||||
use tokio::net::TcpSocket;
|
||||
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
let uri =
|
||||
Url::parse(endpoint).map_err(|_| GatewayClientError::InvalidUrl(endpoint.to_owned()))?;
|
||||
@@ -37,14 +45,41 @@ pub(crate) async fn connect_async(
|
||||
}
|
||||
};
|
||||
|
||||
let stream = TcpStream::connect(&sock_addrs[..]).await.map_err(|error| {
|
||||
GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
source: error.into(),
|
||||
let mut stream = Err(GatewayClientError::NoEndpointForConnection {
|
||||
address: endpoint.to_owned(),
|
||||
});
|
||||
for sock_addr in sock_addrs {
|
||||
let socket = if sock_addr.is_ipv4() {
|
||||
TcpSocket::new_v4()
|
||||
} else {
|
||||
TcpSocket::new_v6()
|
||||
}
|
||||
})?;
|
||||
.map_err(|err| GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
source: err.into(),
|
||||
})?;
|
||||
|
||||
tokio_tungstenite::client_async_tls(endpoint, stream)
|
||||
#[cfg(unix)]
|
||||
if let Some(callback) = connection_fd_callback.as_ref() {
|
||||
callback.as_ref()(socket.as_raw_fd());
|
||||
}
|
||||
|
||||
match socket.connect(sock_addr).await {
|
||||
Ok(s) => {
|
||||
stream = Ok(s);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
stream = Err(GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
source: err.into(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio_tungstenite::client_async_tls(endpoint, stream?)
|
||||
.await
|
||||
.map_err(|error| GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
|
||||
@@ -43,6 +43,9 @@ pub enum GatewayClientError {
|
||||
#[error("connection failed: {address}: {source}")]
|
||||
NetworkConnectionFailed { address: String, source: WsError },
|
||||
|
||||
#[error("no socket address for endpoint: {address}")]
|
||||
NoEndpointForConnection { address: String },
|
||||
|
||||
#[error("Invalid URL: {0}")]
|
||||
InvalidUrl(String),
|
||||
|
||||
|
||||
@@ -37,11 +37,10 @@ nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
|
||||
rand_chacha = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["sphinx"]
|
||||
default = []
|
||||
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
|
||||
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
outfox = ["nym-sphinx-types/outfox"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
@@ -202,6 +202,18 @@ impl PemStorableKey for PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<x25519_dalek::PublicKey> for PublicKey {
|
||||
fn from(public_key: x25519_dalek::PublicKey) -> Self {
|
||||
PublicKey(public_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for x25519_dalek::PublicKey {
|
||||
fn from(public_key: PublicKey) -> Self {
|
||||
public_key.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct PrivateKey(x25519_dalek::StaticSecret);
|
||||
|
||||
@@ -308,109 +320,15 @@ impl PemStorableKey for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
// compatibility with sphinx keys:
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl From<PublicKey> for nym_sphinx_types::PublicKey {
|
||||
fn from(key: PublicKey) -> Self {
|
||||
nym_sphinx_types::PublicKey::from(key.to_bytes())
|
||||
impl From<x25519_dalek::StaticSecret> for PrivateKey {
|
||||
fn from(secret: x25519_dalek::StaticSecret) -> Self {
|
||||
PrivateKey(secret)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl<'a> From<&'a PublicKey> for nym_sphinx_types::PublicKey {
|
||||
fn from(key: &'a PublicKey) -> Self {
|
||||
nym_sphinx_types::PublicKey::from((*key).to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl From<nym_sphinx_types::PublicKey> for PublicKey {
|
||||
fn from(pub_key: nym_sphinx_types::PublicKey) -> Self {
|
||||
Self(x25519_dalek::PublicKey::from(*pub_key.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl From<PrivateKey> for nym_sphinx_types::PrivateKey {
|
||||
fn from(key: PrivateKey) -> Self {
|
||||
nym_sphinx_types::PrivateKey::from(key.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl<'a> From<&'a PrivateKey> for nym_sphinx_types::PrivateKey {
|
||||
fn from(key: &'a PrivateKey) -> Self {
|
||||
nym_sphinx_types::PrivateKey::from(key.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl From<nym_sphinx_types::PrivateKey> for PrivateKey {
|
||||
fn from(private_key: nym_sphinx_types::PrivateKey) -> Self {
|
||||
let private_key_bytes = private_key.to_bytes();
|
||||
assert_eq!(private_key_bytes.len(), PRIVATE_KEY_SIZE);
|
||||
Self::from_bytes(&private_key_bytes).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sphinx_key_conversion {
|
||||
use super::*;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
pub(super) fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
|
||||
const NUM_ITERATIONS: usize = 100;
|
||||
|
||||
#[test]
|
||||
fn works_for_forward_conversion() {
|
||||
let mut rng = test_rng();
|
||||
|
||||
for _ in 0..NUM_ITERATIONS {
|
||||
let keys = KeyPair::new(&mut rng);
|
||||
let private = &keys.private_key;
|
||||
let public = &keys.public_key;
|
||||
|
||||
let dummy_remote = KeyPair::new(&mut rng);
|
||||
let dh1 = private.diffie_hellman(&dummy_remote.public_key);
|
||||
|
||||
let public_bytes = public.to_bytes();
|
||||
|
||||
let sphinx_private: nym_sphinx_types::PrivateKey = private.into();
|
||||
let recovered_private = PrivateKey::from(sphinx_private);
|
||||
|
||||
let dh2 = recovered_private.diffie_hellman(&dummy_remote.public_key);
|
||||
|
||||
let sphinx_public: nym_sphinx_types::PublicKey = public.into();
|
||||
let recovered_public = PublicKey::from(sphinx_public);
|
||||
assert_eq!(public_bytes, recovered_public.to_bytes());
|
||||
|
||||
// even though the byte representation of the private key changed, the resultant DH is the same
|
||||
// which is what matters
|
||||
assert_eq!(dh1, dh2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn works_for_backward_conversion() {
|
||||
for _ in 0..NUM_ITERATIONS {
|
||||
let (sphinx_private, sphinx_public) = nym_sphinx_types::crypto::keygen();
|
||||
|
||||
let private_bytes = sphinx_private.to_bytes();
|
||||
let public_bytes = sphinx_public.as_bytes();
|
||||
|
||||
let private: PrivateKey = sphinx_private.into();
|
||||
let recovered_sphinx_private: nym_sphinx_types::PrivateKey = private.into();
|
||||
|
||||
let public: PublicKey = sphinx_public.into();
|
||||
let recovered_sphinx_public: nym_sphinx_types::PublicKey = public.into();
|
||||
assert_eq!(private_bytes, recovered_sphinx_private.to_bytes());
|
||||
assert_eq!(public_bytes, recovered_sphinx_public.as_bytes());
|
||||
}
|
||||
impl AsRef<x25519_dalek::StaticSecret> for PrivateKey {
|
||||
fn as_ref(&self) -> &x25519_dalek::StaticSecret {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ mime = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
serde_yaml = { workspace = true }
|
||||
subtle.workspace = true
|
||||
tower = { workspace = true }
|
||||
tracing.workspace = true
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
@@ -7,6 +7,7 @@ use axum::{extract::Request, response::Response};
|
||||
use futures::future::BoxFuture;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use subtle::ConstantTimeEq;
|
||||
use tower::{Layer, Service};
|
||||
use tracing::{debug, instrument, trace};
|
||||
use zeroize::Zeroizing;
|
||||
@@ -76,7 +77,7 @@ impl<S> RequireAuth<S> {
|
||||
return Err("`Authorization` header must contain non-empty `Bearer` token");
|
||||
}
|
||||
|
||||
if self.bearer_token.as_str() != bearer_token {
|
||||
if bool::from(self.bearer_token.as_bytes().ct_ne(bearer_token.as_bytes())) {
|
||||
return Err("`Authorization` header does not contain the correct `Bearer` token");
|
||||
}
|
||||
|
||||
|
||||
@@ -48,12 +48,10 @@ features = ["sync"]
|
||||
[features]
|
||||
default = ["sphinx"]
|
||||
sphinx = [
|
||||
"nym-crypto/sphinx",
|
||||
"nym-sphinx-params/sphinx",
|
||||
"nym-sphinx-types/sphinx",
|
||||
]
|
||||
outfox = [
|
||||
"nym-crypto/outfox",
|
||||
"nym-sphinx-params/outfox",
|
||||
"nym-sphinx-types/outfox",
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
nym-crypto = { path = "../../crypto", features = ["asymmetric"] } # all addresses are expressed in terms on their crypto keys
|
||||
nym-crypto = { path = "../../crypto", features = ["asymmetric", "sphinx"] } # all addresses are expressed in terms on their crypto keys
|
||||
nym-sphinx-types = { path = "../types", features = ["sphinx"] } # we need to be able to refer to some types defined inside sphinx crate
|
||||
serde = { workspace = true } # implementing serialization/deserialization for some types, like `Recipient`
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -559,7 +559,7 @@ mod tests {
|
||||
let mut address_bytes = [0; NODE_ADDRESS_LENGTH];
|
||||
rng.fill_bytes(&mut address_bytes);
|
||||
|
||||
let dummy_private = PrivateKey::new_with_rng(rng);
|
||||
let dummy_private = PrivateKey::random_from_rng(rng);
|
||||
let pub_key = (&dummy_private).into();
|
||||
Node {
|
||||
address: NodeAddressBytes::from_bytes(address_bytes),
|
||||
|
||||
@@ -130,28 +130,33 @@ impl Decoder for NymCodec {
|
||||
mod packet_encoding {
|
||||
use super::*;
|
||||
use nym_sphinx_types::{
|
||||
crypto, Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes,
|
||||
DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
|
||||
Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes,
|
||||
PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
|
||||
};
|
||||
|
||||
fn random_pubkey() -> nym_sphinx_types::PublicKey {
|
||||
let private_key = PrivateKey::random();
|
||||
(&private_key).into()
|
||||
}
|
||||
|
||||
fn make_valid_outfox_packet(size: PacketSize) -> NymPacket {
|
||||
let (_, node1_pk) = crypto::keygen();
|
||||
let node1_pk = random_pubkey();
|
||||
let node1 = Node::new(
|
||||
NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]),
|
||||
node1_pk,
|
||||
);
|
||||
let (_, node2_pk) = crypto::keygen();
|
||||
let node2_pk = random_pubkey();
|
||||
let node2 = Node::new(
|
||||
NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]),
|
||||
node2_pk,
|
||||
);
|
||||
let (_, node3_pk) = crypto::keygen();
|
||||
let node3_pk = random_pubkey();
|
||||
let node3 = Node::new(
|
||||
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
|
||||
node3_pk,
|
||||
);
|
||||
|
||||
let (_, node4_pk) = crypto::keygen();
|
||||
let node4_pk = random_pubkey();
|
||||
let node4 = Node::new(
|
||||
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
|
||||
node4_pk,
|
||||
@@ -170,17 +175,17 @@ mod packet_encoding {
|
||||
}
|
||||
|
||||
fn make_valid_sphinx_packet(size: PacketSize) -> NymPacket {
|
||||
let (_, node1_pk) = crypto::keygen();
|
||||
let node1_pk = random_pubkey();
|
||||
let node1 = Node::new(
|
||||
NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]),
|
||||
node1_pk,
|
||||
);
|
||||
let (_, node2_pk) = crypto::keygen();
|
||||
let node2_pk = random_pubkey();
|
||||
let node2 = Node::new(
|
||||
NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]),
|
||||
node2_pk,
|
||||
);
|
||||
let (_, node3_pk) = crypto::keygen();
|
||||
let node3_pk = random_pubkey();
|
||||
let node3 = Node::new(
|
||||
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
|
||||
node3_pk,
|
||||
|
||||
@@ -4,8 +4,10 @@ use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressE
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError,
|
||||
NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacket, SphinxError,
|
||||
NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacketData, SphinxError,
|
||||
Version as SphinxPacketVersion,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::packet::FramedNymPacket;
|
||||
@@ -13,12 +15,38 @@ use nym_metrics::nanos;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MixProcessingResult {
|
||||
pub enum MixProcessingResultData {
|
||||
/// Contains unwrapped data that should first get delayed before being sent to next hop.
|
||||
ForwardHop(MixPacket, Option<SphinxDelay>),
|
||||
ForwardHop {
|
||||
packet: MixPacket,
|
||||
delay: Option<SphinxDelay>,
|
||||
},
|
||||
|
||||
/// Contains all data extracted out of the final hop packet that could be forwarded to the destination.
|
||||
FinalHop(ProcessedFinalHop),
|
||||
FinalHop { final_hop_data: ProcessedFinalHop },
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum MixPacketVersion {
|
||||
Outfox,
|
||||
Sphinx(SphinxPacketVersion),
|
||||
}
|
||||
|
||||
impl Display for MixPacketVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
MixPacketVersion::Outfox => "outfox".fmt(f),
|
||||
MixPacketVersion::Sphinx(sphinx_version) => {
|
||||
write!(f, "sphinx-{}", sphinx_version.value())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MixProcessingResult {
|
||||
pub packet_version: MixPacketVersion,
|
||||
pub processing_data: MixProcessingResultData,
|
||||
}
|
||||
|
||||
type ForwardAck = MixPacket;
|
||||
@@ -107,37 +135,63 @@ fn perform_final_processing(
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
match packet {
|
||||
NymProcessedPacket::Sphinx(packet) => {
|
||||
match packet {
|
||||
ProcessedPacket::ForwardHop(packet, address, delay) => {
|
||||
process_forward_hop(NymPacket::Sphinx(*packet), address, delay, packet_type)
|
||||
}
|
||||
let processing_data = match packet.data {
|
||||
ProcessedPacketData::ForwardHop {
|
||||
next_hop_packet,
|
||||
next_hop_address,
|
||||
delay,
|
||||
} => process_forward_hop(
|
||||
NymPacket::Sphinx(next_hop_packet),
|
||||
next_hop_address,
|
||||
delay,
|
||||
packet_type,
|
||||
),
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacket::FinalHop(destination, _, payload) => process_final_hop(
|
||||
ProcessedPacketData::FinalHop {
|
||||
destination,
|
||||
identifier: _,
|
||||
payload,
|
||||
} => process_final_hop(
|
||||
destination,
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
),
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Sphinx(packet.version),
|
||||
processing_data,
|
||||
})
|
||||
}
|
||||
NymProcessedPacket::Outfox(packet) => {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
if packet.is_final_hop() {
|
||||
process_final_hop(
|
||||
let processing_data = process_final_hop(
|
||||
DestinationAddressBytes::from_bytes(next_address),
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
)
|
||||
)?;
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data,
|
||||
})
|
||||
} else {
|
||||
let mix_packet = MixPacket::new(
|
||||
let packet = MixPacket::new(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
);
|
||||
Ok(MixProcessingResult::ForwardHop(mix_packet, None))
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data: MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,14 +202,16 @@ fn process_final_hop(
|
||||
payload: Vec<u8>,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
) -> Result<MixProcessingResultData, PacketProcessingError> {
|
||||
let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?;
|
||||
|
||||
Ok(MixProcessingResult::FinalHop(ProcessedFinalHop {
|
||||
destination,
|
||||
forward_ack,
|
||||
message,
|
||||
}))
|
||||
Ok(MixProcessingResultData::FinalHop {
|
||||
final_hop_data: ProcessedFinalHop {
|
||||
destination,
|
||||
forward_ack,
|
||||
message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn split_into_ack_and_message(
|
||||
@@ -211,11 +267,14 @@ fn process_forward_hop(
|
||||
forward_address: NodeAddressBytes,
|
||||
delay: SphinxDelay,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
) -> Result<MixProcessingResultData, PacketProcessingError> {
|
||||
let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?;
|
||||
|
||||
let mix_packet = MixPacket::new(next_hop_address, packet, packet_type);
|
||||
Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay)))
|
||||
let packet = MixPacket::new(next_hop_address, packet, packet_type);
|
||||
Ok(MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: Some(delay),
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: what more could we realistically test here?
|
||||
|
||||
@@ -16,5 +16,5 @@ nym-sphinx-types = { path = "../types" }
|
||||
|
||||
[features]
|
||||
default = ["sphinx"]
|
||||
sphinx = ["nym-crypto/sphinx", "nym-sphinx-types/outfox"]
|
||||
outfox = ["nym-crypto/outfox", "nym-sphinx-types/outfox"]
|
||||
sphinx = ["nym-sphinx-types/outfox"]
|
||||
outfox = ["nym-sphinx-types/outfox"]
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::{array::TryFromSliceError, fmt};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "outfox")]
|
||||
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
|
||||
|
||||
#[cfg(feature = "outfox")]
|
||||
pub use nym_outfox::{
|
||||
constants::MIN_PACKET_SIZE, constants::MIX_PARAMS_LEN, constants::OUTFOX_PACKET_OVERHEAD,
|
||||
error::OutfoxError,
|
||||
};
|
||||
// re-exporting types and constants available in sphinx
|
||||
#[cfg(feature = "outfox")]
|
||||
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub use sphinx_packet::{
|
||||
constants::{
|
||||
@@ -21,12 +29,10 @@ pub use sphinx_packet::{
|
||||
payload::{Payload, PAYLOAD_OVERHEAD_SIZE},
|
||||
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
|
||||
surb::{SURBMaterial, SURB},
|
||||
Error as SphinxError, ProcessedPacket,
|
||||
version::Version,
|
||||
version::UPDATED_LEGACY_VERSION,
|
||||
Error as SphinxError, ProcessedPacket, ProcessedPacketData,
|
||||
};
|
||||
#[cfg(feature = "sphinx")]
|
||||
use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
|
||||
use std::{array::TryFromSliceError, fmt};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NymPacketError {
|
||||
@@ -85,8 +91,12 @@ impl NymPacket {
|
||||
destination: &Destination,
|
||||
delays: &[Delay],
|
||||
) -> Result<NymPacket, NymPacketError> {
|
||||
// FIXME:
|
||||
// for now explicitly use the legacy version until sufficient number of nodes
|
||||
// understand both variants
|
||||
Ok(NymPacket::Sphinx(
|
||||
SphinxPacketBuilder::new()
|
||||
.with_version(UPDATED_LEGACY_VERSION)
|
||||
.with_payload_size(size)
|
||||
.build_packet(message, route, destination, delays)?,
|
||||
))
|
||||
|
||||
@@ -27,7 +27,7 @@ wasm-bindgen = { workspace = true, optional = true }
|
||||
|
||||
## internal
|
||||
nym-config = { path = "../config" }
|
||||
nym-crypto = { path = "../crypto", features = ["sphinx", "outfox"] }
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-sphinx-addressing = { path = "../nymsphinx/addressing" }
|
||||
nym-sphinx-types = { path = "../nymsphinx/types", features = [
|
||||
|
||||
@@ -105,7 +105,7 @@ impl<'a> From<&'a RoutingNode> for SphinxNode {
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
SphinxNode::new(node_address_bytes, (&node.sphinx_key).into())
|
||||
SphinxNode::new(node_address_bytes, node.sphinx_key.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
44.332
|
||||
44.811
|
||||
|
||||
@@ -1 +1 @@
|
||||
Monday, February 3rd 2025, 13:47:19 UTC
|
||||
Wednesday, February 26th 2025, 16:02:47 UTC
|
||||
|
||||
@@ -16,8 +16,10 @@ Options:
|
||||
If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
|
||||
--local
|
||||
Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode <MODE>
|
||||
Specifies the current mode of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway]
|
||||
--mode [<MODE>...]
|
||||
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
--modes <MODES>
|
||||
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes
|
||||
If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT>
|
||||
@@ -31,7 +33,7 @@ Options:
|
||||
--location <LOCATION>
|
||||
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its http API. default: `0.0.0.0:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH>
|
||||
Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN>
|
||||
@@ -43,27 +45,29 @@ Options:
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE>
|
||||
Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS>
|
||||
Address this node will bind to for listening for mixnet packets default: `0.0.0.0:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS>
|
||||
Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--nyxd-urls <NYXD_URLS>
|
||||
Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING>
|
||||
Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED>
|
||||
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its wireguard interface. default: `0.0.0.0:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-announced-port <WIREGUARD_ANNOUNCED_PORT>
|
||||
Port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
|
||||
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its verloc API. default: `0.0.0.0:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its client websocket API. default: `0.0.0.0:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT>
|
||||
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT>
|
||||
|
||||
@@ -0,0 +1,746 @@
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { VarInfo } from 'components/variable-info.tsx';
|
||||
import { Steps } from 'nextra/components';
|
||||
import {Accordion, AccordionItem} from "@nextui-org/react";
|
||||
import { MyTab } from 'components/generic-tabs.tsx';
|
||||
import { AccordionTemplate } from 'components/accordion-template.tsx';
|
||||
|
||||
# Advanced Server Administration
|
||||
|
||||
This page is for experienced operators and aspiring sys-admins who seek for higher optimisation and better efficiency of their work managing Nym infrastructure. The steps shared on this page cannot be simply copy-pasted, they ask you for more attention and consideration all the way from choosing server and OS to specs per VM allocation.
|
||||
|
||||
<VarInfo />
|
||||
|
||||
## Virtualising a Dedicated Server
|
||||
|
||||
Some operators or squads of operators orchestrate multiple Nym nodes. Among other benefits (which are out of scope of this page), these operators can decide to acquire one larger dedicated (or bare-metal) server with enough specs (CPU, RAM, storage, bandwidth and port speed) to meet [minimum requirements](../../../nodes#minimum-requirements) for multiple nodes run in parallel.
|
||||
|
||||
This guide explains how to prepare your server in order to be able to host multiple nodes running on separated VMs.
|
||||
|
||||
<Callout type="info">
|
||||
This guide is based on Ubuntu 22.04, in case you prefer another OS, you may have to do a bit of your own research to troubleshoot networking configuration and other parameters.
|
||||
</Callout>
|
||||
|
||||
### Installing KVM on a Server with Ubuntu 22.04
|
||||
|
||||
**KVM** stands for **Kernel-based Virtual Machine**. It is a virtualization technology for Linux that allows a user to run multiple virtual machines (VMs) on a single physical machine. KVM turns the Linux kernel into a hypervisor, enabling it to manage multiple virtualised systems.
|
||||
|
||||
Follow the steps below to install KVM on Ubuntu 22.04 LTS.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
<Callout type="warning">
|
||||
Operators aiming to run Nym node as mixnet [Exit Gateway](../../../community-counsel/exit-gateway) or with wireguard enabled should familiarize themselves with the challenges possibly coming along `nym-node` operation, described in our [community counsel](../../../community-counsel) and follow up with [legal suggestions](../../../community-counsel/legal). Particularly important is to [introduce yourself](../../../community-counsel/legal#introduce-nym-node-to-your-provider) and your intentions to run a Nym node to your provider.
|
||||
|
||||
This step is essential part of legal self defense because it may prevent your provider immediately shutting down your entire service (with all the VMs on it) when receiving first abuse report.
|
||||
|
||||
Additionally, before purchasing a large server, **contact the provider and ask if the offered CPU supports Virtualization Technology (VT)**, without this feature you will not be able to proceed.
|
||||
</Callout>
|
||||
|
||||
Start with obtaining a server with Ubuntu 22.04 LTS:
|
||||
- Make sure that your server meets [minimum requirements](../vps-setup#nym-node---dedicated-server) multiplied by number of `nym-node` instance you aim to run on it.
|
||||
- Most people rent a server from a provider and it comes with a pre-installed OS (in this guide we use Ubuntu 22.04). In case your choice is a bare-metal machine, you probably know what you are doing, there are some useful guides to install a new OS, like [this one on ostechnix.com](https://ostechnix.com/install-ubuntu-server/).
|
||||
|
||||
Make sure thay your system actually supports hardware virtualisation:
|
||||
- Check out the methods documented in [this guide by ostechnix.com](https://ostechnix.com/how-to-find-if-a-cpu-supports-virtualization-technology-vt/).
|
||||
|
||||
Order enough IPv4 and IPv6 (static and public) addresses to have one of each for each planned VM plus one extra for the main machine.
|
||||
|
||||
|
||||
When you have your OS installed, validated CPU virtualisation support and obtained IP addresses, you can start configuring your VMs, following the steps below.
|
||||
|
||||
> Note that the commands below require root permission. You can either go through the setup as `root` or use `sudo` prefix with the commands used in the guide. You can switch to `root` shell by entering one of these commands `sudo su` or `sudo -i`.
|
||||
<Steps>
|
||||
|
||||
##### 1. Install KVM
|
||||
|
||||
- Install KVM and required components:
|
||||
```sh
|
||||
apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst
|
||||
```
|
||||
<br/>
|
||||
<AccordionTemplate name="Component breakdown">
|
||||
- `qemu-kvm`: Provides the core **KVM virtualization** support using QEMU.
|
||||
- `libvirt-daemon-system`: Manages virtual machines via the **libvirt daemon**.
|
||||
- `libvirt-clients` Provides command-line tools like `virsh` to manage VMs.
|
||||
- `bridge-utils`: Enables **network bridging**, allowing VMs to communicate over the network.
|
||||
- `virtinst`: Includes `virt-install` for **creating virtual machines** via CLI.
|
||||
</AccordionTemplate>
|
||||
|
||||
- Start the `libvertd` service:
|
||||
```sh
|
||||
systemctl enable libvirtd
|
||||
systemctl start libvirtd
|
||||
```
|
||||
- Validate by checking status of `libvirt` service:
|
||||
```sh
|
||||
systemctl status libvirtd
|
||||
```
|
||||
<br/>
|
||||
<AccordionTemplate name="Console output">
|
||||
The command output should look similar to this one:
|
||||
```
|
||||
root@nym-exit:~# systemctl status libvirtd
|
||||
● libvirtd.service - Virtualization daemon
|
||||
Loaded: loaded (/lib/systemd/system/libvirtd.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Thu 2025-02-27 14:25:28 MSK; 2min 1s ago
|
||||
TriggeredBy: ● libvirtd-ro.socket
|
||||
● libvirtd.socket
|
||||
● libvirtd-admin.socket
|
||||
Docs: man:libvirtd(8)
|
||||
https://libvirt.org
|
||||
Main PID: 6232 (libvirtd)
|
||||
Tasks: 21 (limit: 32768)
|
||||
Memory: 11.8M
|
||||
CPU: 852ms
|
||||
CGroup: /system.slice/libvirtd.service
|
||||
├─6232 /usr/sbin/libvirtd
|
||||
├─6460 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/lib/libvirt/libvirt_leaseshelper
|
||||
└─6461 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/lib/libvirt/libvirt_leaseshelper
|
||||
|
||||
Feb 27 14:25:28 nym-exit.example.com systemd[1]: Started Virtualization daemon.
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: started, version 2.90 cachesize 150
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: compile time options: IPv6 GNU-getopt DBus no-UBus i18n IDN2 DHCP DHCPv6 no-Lua TFTP conntrack ipset no-nftset auth cryptohash DNSSEC loop-detect inotify dump>
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq-dhcp[6460]: DHCP, IP range 192.168.122.2 -- 192.168.122.254, lease time 1h
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq-dhcp[6460]: DHCP, sockets bound exclusively to interface virbr0
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: reading /etc/resolv.conf
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: using nameserver 127.0.0.53#53
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: read /etc/hosts - 8 names
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: read /var/lib/libvirt/dnsmasq/default.addnhosts - 0 names
|
||||
Feb 27 14:25:30 nym-exit.example.com dnsmasq-dhcp[6460]: read /var/lib/libvirt/dnsmasq/default.hostsfile
|
||||
```
|
||||
</AccordionTemplate>
|
||||
|
||||
- In case you don't configure KVM as `root`, add your current user to the `kvm` and `libvirt` groups to enable VM creation and management using the `virsh` command-line tool or the `virt-manager` GUI:
|
||||
```bash
|
||||
usermod -aG kvm $USER
|
||||
usermod -aG libvirt $USER
|
||||
```
|
||||
|
||||
##### 2. Setup Bridge Networking with KVM
|
||||
|
||||
A **bridged network** lets VMs share the host’s network interface, allowing direct IPv4/IPv6 access like a physical machine.
|
||||
|
||||
By default, KVM sets up a **private virtual bridge**, enabling VM-to-VM communication within the host. It provides its own subnet, DHCP, and NAT for external access.
|
||||
|
||||
Check the IP of KVM’s default virtual interfaces with:
|
||||
|
||||
```bash
|
||||
ip a
|
||||
```
|
||||
<br/>
|
||||
<AccordionTemplate name="Console output">
|
||||
The command output should look similar to this one:
|
||||
```
|
||||
root@nym-exit:~# ip a
|
||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
inet 127.0.0.1/8 scope host lo
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 ::1/128 scope host
|
||||
valid_lft forever preferred_lft forever
|
||||
2: eno1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:14 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f0
|
||||
3: eno49: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
|
||||
link/ether 38:63:bb:2e:9d:20 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp4s0f0
|
||||
inet 31.222.238.222/24 brd 31.222.238.255 scope global eno49
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::3a63:bbff:fe2e:9d20/64 scope link
|
||||
valid_lft forever preferred_lft forever
|
||||
4: eno2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:15 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f1
|
||||
5: eno3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:16 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f2
|
||||
6: eno50: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 38:63:bb:2e:9d:24 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp4s0f1
|
||||
7: eno4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:17 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f3
|
||||
8: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
|
||||
link/ether 52:54:00:ac:d3:ba brd ff:ff:ff:ff:ff:ff
|
||||
inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0
|
||||
valid_lft forever preferred_lft forever
|
||||
```
|
||||
</AccordionTemplate>
|
||||
|
||||
|
||||
By default, KVM uses the `virbr0` network with `<IPv4_ADDRESS>.1/24`, assigning guest VMs IPs in the `<IPv4_ADDRESS>.0/24` range. The host OS is reachable at `<IPv4_ADDRESS>.1`, allowing SSH and file transfers (`scp`) between the host and guests.
|
||||
|
||||
This setup works if you only access VMs from the host. However, remote systems on a different subnet (e.g., `<IPv4_ADDRESS_ALT>.0/24`) **cannot** reach the VMs.
|
||||
|
||||
To enable external access, we need a *public bridge* that connects VMs to the host’s main network, using its DHCP. This ensures VMs get IPs in the same range as the host.
|
||||
|
||||
Before configuring a public bridge, **disable Netfilter** on bridges for better performance and security, as it is enabled by default.
|
||||
|
||||
- Create a file located at `/etc/sysctl.d/bridge.conf`:
|
||||
```bash
|
||||
nano /etc/sysctl.d/bridge.conf
|
||||
|
||||
# in case of using custom editor, replace nano in the syntax
|
||||
```
|
||||
|
||||
- Paste inside the following block, save and exit:
|
||||
```ini
|
||||
net.bridge.bridge-nf-call-ip6tables=0
|
||||
net.bridge.bridge-nf-call-iptables=0
|
||||
net.bridge.bridge-nf-call-arptables=0
|
||||
```
|
||||
|
||||
- Create a file `/etc/udev/rules.d/99-bridge.rules`:
|
||||
```bash
|
||||
nano /etc/udev/rules.d/99-bridge.rules
|
||||
```
|
||||
|
||||
- Paste this line, save and exit:
|
||||
```bash
|
||||
ACTION=="add", SUBSYSTEM=="module", KERNEL=="br_netfilter", RUN+="/sbin/sysctl -p /etc/sysctl.d/bridge.conf"
|
||||
```
|
||||
|
||||
This disables Netfilter on bridges at startup. Save, exit, and reboot to apply changes.
|
||||
|
||||
- Disable KVM’s default networking. Find the default network interface with:
|
||||
```bash
|
||||
ip link
|
||||
```
|
||||
|
||||
<br/>
|
||||
<AccordionTemplate name="Console output">
|
||||
The command output should look similar to this one:
|
||||
```
|
||||
root@nym-exit:~# ip link
|
||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
2: eno1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:14 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f0
|
||||
3: eno2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:15 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f1
|
||||
4: eno49: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
|
||||
link/ether 38:63:bb:2e:9d:20 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp4s0f0
|
||||
5: eno3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:16 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f2
|
||||
6: eno50: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 38:63:bb:2e:9d:24 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp4s0f1
|
||||
7: eno4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:17 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f3
|
||||
8: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 52:54:00:ac:d3:ba brd ff:ff:ff:ff:ff:ff
|
||||
```
|
||||
|
||||
The `virbr0` interface is KVM’s default network. Note your physical interface’s MAC address (e.g., `eno49`). It's the only interface that is currently `UP` and running (`LOWER_UP` state). Other interfaces are `DOWN` and not in use.
|
||||
</AccordionTemplate>
|
||||
|
||||
- Remove the default KVM network:
|
||||
```bash
|
||||
virsh net-destroy default
|
||||
```
|
||||
|
||||
- Remove the default network configuration:
|
||||
```bash
|
||||
virsh net-undefine default
|
||||
```
|
||||
|
||||
- In case last two commands didn't work, try this:
|
||||
```bash
|
||||
ip link delete virbr0 type bridge
|
||||
```
|
||||
- Verify that the `virbr0` and `virbr0-nic` interfaces are deleted:
|
||||
```bash
|
||||
ip link
|
||||
```
|
||||
<AccordionTemplate name="Console output">
|
||||
The command output should look similar to this one:
|
||||
```
|
||||
root@nym-exit:~# ip link
|
||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
2: eno1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:14 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f0
|
||||
3: eno2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:15 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f1
|
||||
4: eno49: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
|
||||
link/ether 38:63:bb:2e:9d:20 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp4s0f0
|
||||
5: eno3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:16 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f2
|
||||
6: eno50: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 38:63:bb:2e:9d:24 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp4s0f1
|
||||
7: eno4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:17 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f3
|
||||
```
|
||||
KVM network is gone.
|
||||
</AccordionTemplate>
|
||||
|
||||
|
||||
##### 3. Setup KVM public bridge for new VMs
|
||||
|
||||
To create a KVM network bridge on Ubuntu, edit a config file located in `/etc/netplan/` either called `00-installer.yaml` or `00-installer-config.yaml` and add the bridge details.
|
||||
|
||||
- Before you edit the file, make a backup to stay on the save side:
|
||||
```bash
|
||||
cp /etc/netplan/00-installer-config.yaml /etc/netplan/00-installer-config.yaml.bak
|
||||
# or
|
||||
cp /etc/netplan/00-installer.yaml /etc/netplan/00-installer.yaml.bak
|
||||
```
|
||||
|
||||
- Open `00-installer-config.yaml` or `00-installer.yaml.`config in a text editor:
|
||||
```bash
|
||||
nano /etc/netplan/00-installer.yaml
|
||||
# or
|
||||
nano /etc/netplan/00-installer-config.yaml
|
||||
```
|
||||
|
||||
- Edit the block below and paste it to the config file, save and exit:
|
||||
```ini
|
||||
#####################################################
|
||||
######## CHANGE ALL VARIABLES IN <> BRACKETS ########
|
||||
#####################################################
|
||||
|
||||
# <INTERFACE> is your own one, you can get with command ip link show
|
||||
# <HOST> is your server main IPv4 address
|
||||
# <GATEWAY> value can be found by running: ip route | grep default
|
||||
|
||||
|
||||
# This is the network config written by 'subiquity'
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
<INTERFACE>:
|
||||
dhcp4: false
|
||||
dhcp6: false
|
||||
|
||||
# Bridge interface configuration
|
||||
bridges:
|
||||
br0:
|
||||
interfaces: [<INTERFACE>]
|
||||
addresses: [<HOST>/24]
|
||||
routes:
|
||||
- to: default
|
||||
via: <GATEWAY>
|
||||
mtu: 1500
|
||||
nameservers:
|
||||
addresses:
|
||||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
- 77.88.8.8
|
||||
parameters:
|
||||
stp: false # Disable STP unless multiple bridges exist
|
||||
forward-delay: 15 # Can be shortened, 15 sec is a common default
|
||||
```
|
||||
|
||||
<Callout type="warning">
|
||||
Ensure the indentation matches exactly as shown above. Incorrect spacing will prevent the bridged network interface from activating.
|
||||
</Callout>
|
||||
|
||||
- Validate `netplan` configuration without applying to prevent breaking network changes:
|
||||
```bash
|
||||
netplan generate
|
||||
|
||||
# Correct configuration output will show nothing
|
||||
```
|
||||
|
||||
- Safety test your changes to catch syntax errors before applying:
|
||||
```bash
|
||||
netplan try
|
||||
```
|
||||
|
||||
- Apply your changes:
|
||||
```bash
|
||||
netplan --debug apply
|
||||
```
|
||||
|
||||
- In case of proubems try some of these steps:
|
||||
<AccordionTemplate name="Netplan configuration troubleshooting">
|
||||
- Validate YAML configuration, given that YAML is syntax sensitive:
|
||||
```bash
|
||||
apt install yamllint -y
|
||||
|
||||
yamllint /etc/netplan/00-installer.yaml
|
||||
# or
|
||||
yamllint /etc/netplan/00-installer-config.yaml
|
||||
|
||||
|
||||
```
|
||||
- Apply correct permissions:
|
||||
```bash
|
||||
chmod 600 /etc/netplan/00-installer.yaml
|
||||
chown root:root /etc/netplan/00-installer.yaml
|
||||
```
|
||||
|
||||
- Manually bring up the bridge:
|
||||
```bash
|
||||
ip link add name br0 type bridge
|
||||
ip link set br0 up
|
||||
ip a show br0
|
||||
```
|
||||
|
||||
- ensure `systemd-networkd` is enabled:
|
||||
```bash
|
||||
systemctl restart systemd-networkd
|
||||
systemctl status systemd-networkd
|
||||
# if inactive, enable it:
|
||||
systemctl enable --now systemd-networkd
|
||||
```
|
||||
</AccordionTemplate>
|
||||
|
||||
- If things went wrong, you can always revert from the backed up file:
|
||||
```bash
|
||||
cp /etc/netplan/00-installer-config.yaml.bak /etc/netplan/00-installer-config.yaml
|
||||
# or
|
||||
cp /etc/netplan/00-installer.yaml.bak /etc/netplan/00-installer.yaml
|
||||
# and
|
||||
netplan apply
|
||||
```
|
||||
|
||||
<Callout type="warning">
|
||||
Using different IPs for your physical NIC and KVM bridge will disconnect SSH when applying changes. Reconnect using the bridge's new IP. If both share the same IP, no disruption occurs.
|
||||
</Callout>
|
||||
|
||||
|
||||
- Verify that the IP address has been assigned to the bridge interface:
|
||||
```bash
|
||||
ip a
|
||||
```
|
||||
<AccordionTemplate name="Console output">
|
||||
The command output should look similar to this one:
|
||||
```
|
||||
root@nym-exit:~# ip a
|
||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
inet 127.0.0.1/8 scope host lo
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 ::1/128 scope host
|
||||
valid_lft forever preferred_lft forever
|
||||
2: eno1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:14 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f0
|
||||
3: eno2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:15 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f1
|
||||
4: eno3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:16 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f2
|
||||
5: eno49: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master br0 state UP group default qlen 1000
|
||||
link/ether 38:63:bb:2e:9d:20 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp4s0f0
|
||||
6: eno4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 14:02:ec:35:2e:17 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp2s0f3
|
||||
7: eno50: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
|
||||
link/ether 38:63:bb:2e:9d:24 brd ff:ff:ff:ff:ff:ff
|
||||
altname enp4s0f1
|
||||
8: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
|
||||
link/ether 46:50:aa:c0:49:a5 brd ff:ff:ff:ff:ff:ff
|
||||
inet 31.222.238.222/24 brd 31.222.238.255 scope global br0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::4450:aaff:fec0:49a5/64 scope link
|
||||
valid_lft forever preferred_lft forever
|
||||
```
|
||||
The bridged interface `br0` now has the IP `<HOST>`, and `<INTERFACE>` shows `master br0`, indicating it is part of the bridge.
|
||||
</AccordionTemplate>
|
||||
|
||||
Alternatively you can use `brctl` command to display the KVM bridge network status:
|
||||
```bash
|
||||
brctl show br0
|
||||
```
|
||||
|
||||
##### 4. Add Bridge Network to KVM
|
||||
|
||||
- Configure KVM to use the bridge by creating `host-bridge.xml`, open a text editor and pate the block below:
|
||||
```bash
|
||||
nano host-bridge.xml
|
||||
```
|
||||
|
||||
```xml
|
||||
<network>
|
||||
<name>host-bridge</name>
|
||||
<forward mode="bridge"/>
|
||||
<bridge name="br0"/>
|
||||
</network>
|
||||
```
|
||||
|
||||
- Start the new bridge and set it as the default for VMs:
|
||||
```bash
|
||||
virsh net-define host-bridge.xml
|
||||
virsh net-start host-bridge
|
||||
virsh net-autostart host-bridge
|
||||
```
|
||||
|
||||
- Verify that the KVM bridge is active:
|
||||
```bash
|
||||
virsh net-list --all
|
||||
```
|
||||
<AccordionTemplate name="Console output">
|
||||
```bash
|
||||
root@nym-exit:~# virsh net-list --all
|
||||
Name State Autostart Persistent
|
||||
------------------------------------------------
|
||||
host-bridge active yes yes
|
||||
```
|
||||
</AccordionTemplate>
|
||||
|
||||
KVM bridge networking is successfully set up and active!
|
||||
|
||||
Your KVM installation is now ready to deploy and manage VMs.
|
||||
|
||||
</Steps>
|
||||
|
||||
### Setting Up Virtual Machines
|
||||
|
||||
After finishing the [installation of KVM](#installing-kvm-on-a-server-with-ubuntu-2204), we can move to the virtualisation configuration.
|
||||
|
||||
> **The steps below will guide you through a setup of one VM, therefore you will have to repeat this process for each VM**. That also means that you have to be mindful of space and memory allocation.
|
||||
|
||||
<Steps>
|
||||
##### 1. Install OS for VMs
|
||||
|
||||
This is the OS on which the nodes themselves will run. You can chose any GNU/Linux of your preference. For this guide we are going to be using Ubuntu 24.04 LTS (Noble Numbat) cloud image from [cloud-images.ubuntu.com](https://cloud-images.ubuntu.com/noble/current/).
|
||||
|
||||
- Download Ubuntu Cloud image:
|
||||
```bash
|
||||
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
|
||||
```
|
||||
- Copy the image to to `/var/lib/libvirt/images/` asigning to it a name your VM
|
||||
```bash
|
||||
cp noble-server-cloudimg-amd64.img /var/lib/libvirt/images/<VM_NAME>.img
|
||||
|
||||
# for example:
|
||||
# cp noble-server-cloudimg-amd64.img /var/lib/libvirt/images/ubuntu-1.img
|
||||
```
|
||||
|
||||
##### 2. Create and resize a virtual machine
|
||||
|
||||
- Get `guestfs-tools` to be able to customize your login credentials:
|
||||
```bash
|
||||
apt install guestfs-tools
|
||||
```
|
||||
|
||||
- Define login credentials:
|
||||
```bash
|
||||
virt-customize -a /var/lib/libvirt/images/<VM_NAME>.img --root-password password:<PASSWORD>
|
||||
# for example
|
||||
# virt-customize -a /var/lib/libvirt/images/ubuntu-1.img --root-password password:makesuretosaveyourpasswordslocallytoapasswordmanager
|
||||
```
|
||||
|
||||
- Use `qemu-img` tool with a command `resize` to create a VM according your needs. You can see `qemu` [documentation page`](https://www.qemu.org/docs/master/tools/qemu-img.html) for more info on how to use it correctly.
|
||||
```bash
|
||||
qemu-img resize /var/lib/libvirt/images/<VM_NAME>.img +<SIZE_IN_GB>G
|
||||
# for example
|
||||
# qemu-img resize /var/lib/libvirt/images/ubuntu-1.img +100G
|
||||
```
|
||||
|
||||
- Resize it from within it after `virt-install` command:
|
||||
```bash
|
||||
virt-install \
|
||||
--name <VM_NAME> \
|
||||
--ram=<SIZE_IN_MB> \
|
||||
--vcpus=<NUMBER_OF_VIRTUAL_CPUS> \
|
||||
--cpu host \
|
||||
--hvm \
|
||||
--disk bus=virtio,path=/var/lib/libvirt/images/<VM_NAME>.img \
|
||||
--network bridge=br0 \
|
||||
--graphics none \
|
||||
--console pty,target_type=serial \
|
||||
--osinfo <YOUR_CHOSEN_OS_NAME> \
|
||||
--import
|
||||
```
|
||||
|
||||
- In our example we go with 4 GB RAM on the same machine as before:
|
||||
<br/>
|
||||
<AccordionTemplate name="Command example">
|
||||
```bash
|
||||
virt-install \
|
||||
--name ubuntu-1 \
|
||||
--ram=4096 \
|
||||
--vcpus=4 \
|
||||
--cpu host \
|
||||
--hvm \
|
||||
--disk bus=virtio,path=/var/lib/libvirt/images/ubuntu-1.img \
|
||||
--network bridge=br0 \
|
||||
--graphics none \
|
||||
--console pty,target_type=serial \
|
||||
--osinfo ubuntunoble \
|
||||
--import
|
||||
```
|
||||
</AccordionTemplate>
|
||||
|
||||
- After loading you should see a login console, you can also initiate it by:
|
||||
```bash
|
||||
virsh console <VM_NAME>
|
||||
# for example
|
||||
# virsh console ubuntu-1
|
||||
```
|
||||
|
||||
- Log in to your new VM using your credentials.
|
||||
|
||||
##### 3. Validate your setup
|
||||
|
||||
- Make sure the `root` disk has the expected space by running:
|
||||
```bash
|
||||
df -h
|
||||
```
|
||||
|
||||
- If not, run:
|
||||
```bash
|
||||
growpart /dev/vda 1
|
||||
resize2fs /dev/vda1
|
||||
```
|
||||
|
||||
##### 4. Configure networking for the VM
|
||||
|
||||
As this guide is based on a newer Ubuntu, we use `netplan`, this may be different on different OS.
|
||||
|
||||
- Open `/etc/netplan/01-network-config.yaml` in your favourite text editor:
|
||||
```bash
|
||||
nano /etc/netplan/01-network-config.yaml
|
||||
```
|
||||
|
||||
- Insert this config, using your correct IP configuration, save and exit:
|
||||
```ini
|
||||
network:
|
||||
version: 2
|
||||
renderer: networkd
|
||||
ethernets:
|
||||
<INTERFACE>:
|
||||
dhcp4: false
|
||||
dhcp6: false # Set to true if you want automatic IPv6 assignment
|
||||
addresses:
|
||||
- <IPv4_VM>/24 # Assign IPv4 address to the VM
|
||||
- <IPv6_VM>/64 # Assign IPv6 address to the VM
|
||||
routes:
|
||||
- to: default
|
||||
via: <IPv4_GATEWAY_HOST_SERVER> # IPv4 gateway (host machine)
|
||||
- to: default
|
||||
via: <IPv6_GATEWAY_HOST_SERVER> # IPv6 gateway (host machine)
|
||||
nameservers:
|
||||
addresses:
|
||||
- 1.1.1.1 # Cloudflare IPv4 DNS
|
||||
- 8.8.8.8 # Google IPv4 DNS
|
||||
- 2606:4700:4700::1111 # Cloudflare IPv6 DNS
|
||||
- 2001:4860:4860::8888 # Google IPv6 DNS
|
||||
```
|
||||
- Fix wide permissions on the config file:
|
||||
```bash
|
||||
chmod 600 /etc/netplan/01-network-config.yaml
|
||||
```
|
||||
|
||||
- Check if the config has any errors:
|
||||
```bash
|
||||
netplan generate
|
||||
```
|
||||
|
||||
- Apply the configuration:
|
||||
```bash
|
||||
netplan --debug apply
|
||||
```
|
||||
|
||||
- Verify by checking if IPv4 and IPv6 are assigned correctly and if they route:
|
||||
```bash
|
||||
ip -4 a
|
||||
ip -6 a
|
||||
```
|
||||
```bash
|
||||
ip -4 r
|
||||
ip -6 r
|
||||
```
|
||||
```bash
|
||||
# to ping through IPv6, use:
|
||||
ping6 nym.com
|
||||
```
|
||||
- You should be able to ping your new VM from a local machine:
|
||||
```bash
|
||||
ping <IPv4_VM>
|
||||
ping6 <IPv6_VM>
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
Your VM should be working and fully routable. To be able to use it properly, we will create a direct SSH access to the VM.
|
||||
|
||||
#### Configure VM SSH access
|
||||
|
||||
<Steps>
|
||||
|
||||
##### 1. Log in to your VM, update and upgrade your OS:
|
||||
- Log in to your server using as `root` or as a non-root user with `sudo` privileges
|
||||
```bash
|
||||
apt update; apt upgrade
|
||||
```
|
||||
|
||||
##### 2. Generate new host SSH keys
|
||||
|
||||
Since we used a `cloud-init` image without an SSH server, we need to generate SSH host keys for client authentication and server identity verification. All of them will be saved to this location: `/etc/ssh/<KEY>`.
|
||||
|
||||
- Generate a new RSA host key:
|
||||
```bash
|
||||
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
|
||||
```
|
||||
- Generate a new DSA host key:
|
||||
```bash
|
||||
ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
|
||||
```
|
||||
- Generate a new ECDSA host key:
|
||||
```bash
|
||||
ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key
|
||||
```
|
||||
- Finally, generate a new ED25519 host key:
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key
|
||||
```
|
||||
##### 3. Restart the SSH service on the server
|
||||
- Run:
|
||||
```bash
|
||||
systemctl restart ssh.service
|
||||
```
|
||||
|
||||
##### 4. Check if the SSH serice is active
|
||||
- Run:
|
||||
```bash
|
||||
systemctl status ssh.service
|
||||
```
|
||||
|
||||
##### 5. Create file `~/.ssh/authorized_keys` and add you public key:
|
||||
- Create `.ssh` directory:
|
||||
```bash
|
||||
mkdir ~/.ssh
|
||||
```
|
||||
|
||||
- Open with your favourite text editor:
|
||||
```bash
|
||||
nano ~/.ssh/authorized_keys
|
||||
```
|
||||
- Paste your SSH public key, save and exit
|
||||
|
||||
- In case of non-root, setup a correct ownership and permissions:
|
||||
```bash
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
chmod 700 ~/.ssh
|
||||
chown : ~/.ssh
|
||||
```
|
||||
|
||||
##### 5. Test by connecting via SSH
|
||||
|
||||
- Now you should be able to connect to the VM directly from your local terminal
|
||||
```bash
|
||||
ssh root@<IPv4> -i ~/.ssh/your_ssh_key
|
||||
```
|
||||
</Steps>
|
||||
|
||||
Now your VM is almost ready for `nym-node` [setup](../../nym-node/setup). Before you proceed, ssh in and [configure all prerequisities](../vps-setup#vps-configuration) needed for `nym-node` installation and operation.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"git": {
|
||||
"deploymentEnabled": {
|
||||
"master": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/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/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## 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/app/building-your-application/deploying) for more details.
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": ["node_modules", ".next", "public"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
|
||||
basePath: "/explorer",
|
||||
assetPrefix: "/explorer",
|
||||
trailingSlash: false,
|
||||
|
||||
async redirects() {
|
||||
return [
|
||||
// Change the basePath to /explorer
|
||||
{
|
||||
source: "/",
|
||||
destination: "/explorer",
|
||||
basePath: false,
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@nymproject/explorer-v2",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build:prod": "yarn --cwd .. build && next build",
|
||||
"start": "next start",
|
||||
"lint": "biome check --fix"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chain-registry/types": "^0.50.36",
|
||||
"@cosmos-kit/keplr-extension": "^2.14.0",
|
||||
"@cosmos-kit/react": "^2.20.1",
|
||||
"@emotion/cache": "^11.13.5",
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@emotion/styled": "^11.13.5",
|
||||
"@interchain-ui/react": "^1.26.1",
|
||||
"@mui/icons-material": "^5.16.11",
|
||||
"@mui/material": "^6.1.10",
|
||||
"@mui/material-nextjs": "^6.1.9",
|
||||
"@mui/x-date-pickers": "^7.23.2",
|
||||
"@nivo/line": "^0.88.0",
|
||||
"@nymproject/contract-clients": "^1.4.1",
|
||||
"@nymproject/react": "1.0.0",
|
||||
"@tanstack/react-query": "^5.64.2",
|
||||
"@tanstack/react-query-devtools": "^5.64.2",
|
||||
"@tanstack/react-query-next-experimental": "^5.66.0",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"chain-registry": "^1.69.64",
|
||||
"cldr-compact-number": "^0.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"isomorphic-dompurify": "^2.21.0",
|
||||
"material-react-table": "^3.0.3",
|
||||
"next": "^15.2.0",
|
||||
"openapi-fetch": "^0.13.4",
|
||||
"qrcode.react": "^4.1.0",
|
||||
"qs": "^6.14.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-random-avatars": "^1.3.1",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"lefthook": "^1.8.5",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,61 @@
|
||||
<svg
|
||||
width="33"
|
||||
height="32"
|
||||
viewBox="0 0 33 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="6.4"
|
||||
height="6.38019"
|
||||
transform="matrix(1 1.74846e-07 1.74846e-07 -1 7.06641 25.5204)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
width="6.4"
|
||||
height="6.38019"
|
||||
transform="matrix(1 1.74846e-07 1.74846e-07 -1 0.666504 31.9006)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
width="6.4"
|
||||
height="6.38019"
|
||||
transform="matrix(1 1.74846e-07 1.74846e-07 -1 13.4663 19.1406)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
width="6.4"
|
||||
height="6.38019"
|
||||
transform="matrix(1 1.74846e-07 1.74846e-07 -1 19.8667 12.761)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<path
|
||||
d="M32.6663 32L26.2663 32L26.2663 6.38018L0.708989 6.38017L0.70899 -1.52588e-05L24.9863 -1.1014e-05C29.2278 -1.02724e-05 32.6663 3.43845 32.6663 7.68L32.6663 32Z"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="15" width="3" height="3" transform="rotate(-90 0 15)" fill="currentColor" />
|
||||
<rect x="3" y="12" width="3" height="3" transform="rotate(-90 3 12)" fill="currentColor" />
|
||||
<rect x="6" y="9" width="3" height="3" transform="rotate(-90 6 9)" fill="currentColor" />
|
||||
<rect x="9" y="6" width="3" height="3" transform="rotate(-90 9 6)" fill="currentColor"/>
|
||||
<path d="M0 2.19345e-05V3.00002H11.9997V15H14.9997V3.00002C14.9997 1.34317 13.6565 2.19345e-05 11.9997 2.19345e-05H0Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 579 B |
@@ -0,0 +1,31 @@
|
||||
<svg width="361" height="361" viewBox="0 0 361 361" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_236_24563)">
|
||||
<path d="M180.789 120.41V150.41H210.789V180.41H240.789V180.41C240.789 147.273 213.926 120.41 180.789 120.41V120.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M180.789 240.487L180.789 210.487L150.789 210.487L150.789 180.487L120.789 180.487V180.487C120.789 213.624 147.652 240.487 180.789 240.487V240.487Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M120.789 180.41L150.789 180.41L150.789 150.41L180.789 150.41L180.789 120.41V120.41C147.652 120.41 120.789 147.273 120.789 180.41V180.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M240.789 180.487L210.789 180.487L210.789 210.487L180.789 210.487L180.789 240.487V240.487C213.926 240.487 240.789 213.624 240.789 180.487V180.487Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M330.773 120.41L330.773 150.41V150.41C347.342 150.41 360.773 136.979 360.773 120.41V120.41L330.773 120.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M270.789 60.4102L270.789 90.4102L300.789 90.4102L300.789 60.4102L270.789 60.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M240.789 30.4102L240.789 60.4102L270.789 60.4102L270.789 30.4102L240.789 30.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M60.7891 30.4102L60.7891 60.4102L90.7891 60.4102L90.7891 30.4102L60.7891 30.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M30.7734 60.4102L30.7734 90.4102L60.7734 90.4102L60.7734 60.4102L30.7734 60.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M90.7891 0.410156L90.7891 30.4102L240.789 30.4102L240.789 0.410181L90.7891 0.410156Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<rect width="30" height="150.466" transform="matrix(-1.62921e-07 1 1 1.62921e-07 180.789 120.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<rect width="120" height="30" transform="matrix(-1.62921e-07 1 1 1.62921e-07 330.773 0.410156)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M301.023 90.4102L301.023 120.41L331.023 120.41L331.023 90.4102L301.023 90.4102Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M30.7734 240.41L30.7734 210.41V210.41C14.2049 210.41 0.77344 223.842 0.773438 240.41V240.41L30.7734 240.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M90.7734 300.41L90.7734 270.41L60.7734 270.41L60.7734 300.41L90.7734 300.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M120.773 330.41L120.773 300.41L90.7734 300.41L90.7734 330.41L120.773 330.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M300.773 330.41L300.773 300.41L270.773 300.41L270.773 330.41L300.773 330.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M330.773 300.41L330.773 270.41L300.773 270.41L300.773 300.41L330.773 300.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M270.773 360.41L270.773 330.41L120.773 330.41L120.773 360.41L270.773 360.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<rect width="30" height="150.481" transform="matrix(1.62921e-07 -1 -1 -1.62921e-07 180.789 240.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<rect width="120" height="30" transform="matrix(1.62921e-07 -1 -1 -1.62921e-07 30.7734 360.41)" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
<path d="M60.5391 270.41L60.5391 240.41L30.5391 240.41L30.5391 270.41L60.5391 270.41Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_236_24563">
|
||||
<rect width="360" height="360" fill="white" style="fill:white;fill-opacity:1;" transform="translate(0.773438 0.410156)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M3 3L-4.47035e-08 3L-8.31509e-07 6L3 6L3 3Z" fill="currentColor" />
|
||||
<path d="M6 6L3 6L3 9L6 9L6 6Z" fill="currentColor" />
|
||||
<path d="M9 9L6 9L6 12L9 12L9 9Z" fill="currentColor" />
|
||||
<path d="M12 6L9 6L9 9L12 9L12 6Z" fill="currentColor" />
|
||||
<path d="M15 3L12 3L12 6L15 6L15 3Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 431 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
width="9"
|
||||
height="5"
|
||||
viewBox="0 0 9 5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0.828125 0.828125L4.5 4.5L8.17188 0.828125"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -0,0 +1,35 @@
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.6668 13.3333V14.6667H10.0002V15.3333H2.00016V14.6667H1.3335V3.99999H2.00016V3.33333H4.00016V13.3333H10.6668Z"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<path
|
||||
d="M10.6665 4.66667V0.666672H5.33317V1.33334H4.6665V12H5.33317V12.6667H13.9998V12H14.6665V4.66667H10.6665ZM13.3332 11.3333H5.99984V2.00001H9.33317V6.00001H13.3332V11.3333Z"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<path
|
||||
d="M14.6668 3.33334V4H11.3335V0.666672H12.0002V1.33334H12.6668V2.00001H13.3335V2.66667H14.0002V3.33334H14.6668Z"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 967 B |
@@ -0,0 +1,135 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
>
|
||||
<g clip-path="url(#clip0_8428_65000)">
|
||||
<rect
|
||||
y="3"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 0 3)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
y="15"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 0 15)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="12"
|
||||
y="3"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 12 3)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="9"
|
||||
y="12"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 9 12)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="12"
|
||||
y="15"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 12 15)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="3"
|
||||
y="6"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 3 6)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="3"
|
||||
y="12"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 3 12)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="9"
|
||||
y="6"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 9 6)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
x="6"
|
||||
y="9"
|
||||
width="3"
|
||||
height="3"
|
||||
transform="rotate(-90 6 9)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_8428_65000">
|
||||
<rect
|
||||
width="15"
|
||||
height="15"
|
||||
fill="white"
|
||||
style="fill: white; fill-opacity: 1"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.8343 11.2552C23.7199 14.0472 24.6511 17.1964 24.303 20.8219C24.3015 20.8372 24.2936 20.8513 24.2811 20.8606C22.8532 21.9165 21.4697 22.5573 20.1057 22.9823C20.0951 22.9855 20.0837 22.9853 20.0732 22.9817C20.0627 22.9782 20.0535 22.9714 20.047 22.9623C19.7319 22.5207 19.4456 22.0552 19.1947 21.5663C19.1803 21.5375 19.1935 21.5028 19.2231 21.4915C19.6779 21.3189 20.1103 21.1121 20.5262 20.8673C20.559 20.8479 20.5611 20.8007 20.5308 20.778C20.4425 20.712 20.3551 20.6426 20.2714 20.5733C20.2557 20.5604 20.2347 20.5579 20.2169 20.5665C17.5166 21.8223 14.5586 21.8223 11.8263 20.5665C11.8086 20.5585 11.7875 20.5613 11.7723 20.5739C11.6888 20.6432 11.6011 20.712 11.5137 20.778C11.4834 20.8007 11.4859 20.8479 11.5189 20.8673C11.9349 21.1075 12.3673 21.3189 12.8214 21.4923C12.8508 21.5037 12.8648 21.5375 12.8502 21.5663C12.6048 22.0558 12.3184 22.5213 11.9975 22.9629C11.9835 22.9808 11.9605 22.989 11.9388 22.9823C10.5813 22.5573 9.19781 21.9165 7.76992 20.8606C7.75802 20.8513 7.74947 20.8366 7.74822 20.8213C7.45729 17.6853 8.0502 14.51 10.2146 11.2546C10.2198 11.246 10.2277 11.2393 10.2369 11.2353C11.3019 10.743 12.4428 10.3809 13.6353 10.1741C13.657 10.1707 13.6787 10.1808 13.69 10.2002C13.8373 10.4629 14.0057 10.7998 14.1197 11.0751C15.3767 10.8818 16.6532 10.8818 17.9365 11.0751C18.0505 10.8057 18.213 10.4629 18.3597 10.2002C18.365 10.1906 18.3731 10.1829 18.3829 10.1782C18.3927 10.1735 18.4037 10.1721 18.4144 10.1741C19.6075 10.3815 20.7485 10.7437 21.8126 11.2353C21.822 11.2393 21.8297 11.246 21.8343 11.2552V11.2552ZM14.7587 17.2178C14.7719 16.2908 14.1007 15.5236 13.2582 15.5236C12.4226 15.5236 11.7579 16.284 11.7579 17.2178C11.7579 18.1514 12.4357 18.9118 13.2582 18.9118C14.094 18.9118 14.7587 18.1514 14.7587 17.2178V17.2178ZM20.3062 17.2178C20.3194 16.2908 19.6482 15.5236 18.8059 15.5236C17.9701 15.5236 17.3054 16.284 17.3054 17.2178C17.3054 18.1514 17.9833 18.9118 18.8059 18.9118C19.6482 18.9118 20.3062 18.1514 20.3062 17.2178V17.2178Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<g clip-path="url(#clip0_6568_5882)">
|
||||
<path d="M4 8H8V9H4V8ZM4 6H8V7H4V6ZM7 1H3C2.45 1 2 1.45 2 2V10C2 10.55 2.445 11 2.995 11H9C9.55 11 10 10.55 10 10V4L7 1ZM9 10H3V2H6.5V4.5H9V10Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6568_5882">
|
||||
<rect width="12" height="12" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 526 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M2.5 5H2V4H3V4.5H3.5V5H4V5.5H4.5V6H5V6.5H5.5V0.5H6.5V6.5H7V6H7.5V5.5H8V5H8.5V4.5H9V4H10V5H9.5V5.5H9V6H8.5V6.5H8V7H7.5V7.5H7V8H6.5V8.5H5.5V8H5V7.5H4.5V7H4V6.5H3.5V6H3V5.5H2.5V5Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
|
||||
<path d="M11 10.5H1V11.5H11V10.5Z" fill="#242B2D" style="fill:#242B2D;fill:color(display-p3 0.1412 0.1686 0.1765);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 520 B |
@@ -0,0 +1,22 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" />
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_angular_2549_7570"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(12 12.5) rotate(90) scale(12)"
|
||||
>
|
||||
<stop stopColor="#22D27E" />
|
||||
<stop offset="1" stopColor="#9002FF" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 515 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M63 0L56 -2.44784e-06L56 7L63 7L70 7H77V14V21L77 28L84 28V21V14C84 6.26801 77.732 0 70 0H63ZM7 14L7 7H14H21L28 7L28 2.14186e-06L21 0H14C6.26801 0 0 6.26801 0 14V21L-3.87835e-06 28L7 28L7 21V14ZM84 70C84 77.732 77.732 84 70 84H63H56L56 77H63H70H77V70L77 63L77 56L84 56V63V70ZM21 77H14L14 70L21 70L21 63H28L28 56H35L35 63H42H49L49 56H56L56 49L63 49L63 42V35L56 35V28L49 28L49 21L42 21H35L35 28L28 28L28 35H21L21 42V49L28 49L28 56H21L21 63L14 63L14 70H7L7 77L7 70L7 63L7 56L0 56L-3.87835e-06 63L0 70C0 77.732 6.26801 84 14 84H21H28L28 77H21ZM28 49L28 42L28 35H35L35 28H42L49 28L49 35L56 35V42L56 49H49V56L42 56H35V49H28Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 869 B |
@@ -0,0 +1,79 @@
|
||||
<svg
|
||||
width="84"
|
||||
height="84"
|
||||
viewBox="0 0 84 84"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="7"
|
||||
height="56"
|
||||
transform="matrix(-2.36041e-07 1 1 7.28523e-08 28 77)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
width="7"
|
||||
height="56"
|
||||
transform="matrix(1 0 -1.63189e-07 -1 0 56)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<path
|
||||
d="M63 0L63 21L84 21L84 28L63 28C59.134 28 56 24.866 56 21L56 0H63Z"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
width="7"
|
||||
height="42"
|
||||
transform="matrix(1 0 -1.63189e-07 -1 28 42)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<rect
|
||||
width="7"
|
||||
height="35"
|
||||
transform="matrix(-2.36041e-07 1 1 7.28523e-08 49 49)"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<path
|
||||
d="M28 84L28 77L7 77L7 56L-1.89145e-06 56L-2.80939e-06 77C-2.97838e-06 80.866 3.134 84 7 84L28 84Z"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
<path
|
||||
d="M56 56L56 49L35 49L35 28L28 28L28 49C28 52.866 31.134 56 35 56L56 56Z"
|
||||
fill="#242B2D"
|
||||
style="
|
||||
fill: #242b2d;
|
||||
fill: color(display-p3 0.1412 0.1686 0.1765);
|
||||
fill-opacity: 1;
|
||||
"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9408 7.86426C10.9716 7.86426 6.94678 11.8731 6.94678 16.8225C6.94678 20.7866 9.52132 24.1347 13.0965 25.3217C13.5462 25.4001 13.7148 25.1313 13.7148 24.8962C13.7148 24.6834 13.7035 23.9779 13.7035 23.2277C11.4438 23.642 10.8592 22.679 10.6793 22.1751C10.5781 21.9175 10.1397 21.1225 9.75741 20.9097C9.44262 20.7418 8.99292 20.3274 9.74617 20.3162C10.4545 20.3051 10.9604 20.9657 11.129 21.2345C11.9385 22.5894 13.2314 22.2087 13.7485 21.9735C13.8272 21.3912 14.0633 20.9993 14.3219 20.7754C12.3207 20.5514 10.2296 19.7788 10.2296 16.3522C10.2296 15.378 10.5781 14.5718 11.1515 13.9447C11.0616 13.7207 10.7468 12.8025 11.2414 11.5707C11.2414 11.5707 11.9947 11.3356 13.7148 12.489C14.4343 12.2874 15.1988 12.1866 15.9633 12.1866C16.7278 12.1866 17.4923 12.2874 18.2118 12.489C19.9319 11.3244 20.6852 11.5707 20.6852 11.5707C21.1798 12.8025 20.8651 13.7207 20.7751 13.9447C21.3485 14.5718 21.697 15.3668 21.697 16.3522C21.697 19.79 19.5946 20.5514 17.5935 20.7754C17.9195 21.0553 18.2006 21.5928 18.2006 22.4326C18.2006 23.6308 18.1893 24.5938 18.1893 24.8962C18.1893 25.1313 18.358 25.4113 18.8077 25.3217C22.3603 24.1347 24.9349 20.7754 24.9349 16.8225C24.9349 11.8731 20.91 7.86426 15.9408 7.86426Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg viewBox="0 0 89 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0 2.73999C0 1.50288 1.00579 0.5 2.2465 0.5H10.4256C11.8792 0.5 13.1513 1.47412 13.5263 2.87439L18.7506 22.3815C18.8474 22.7432 19.3816 22.6733 19.3816 22.299V0.5H25.8001V22.2599C25.8001 23.497 24.7943 24.4999 23.5536 24.4999H15.314C13.8643 24.4999 12.5946 23.5308 12.216 22.1355L7.04928 3.0892C6.95141 2.72844 6.41855 2.79902 6.41855 3.17275V24.4999H0V2.73999Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M55.5159 0.500025C54.2752 0.500025 53.2694 1.5029 53.2694 2.74002V24.4999H59.6879V3.18233C59.6879 2.79548 60.2483 2.74066 60.3237 3.12013L64.0601 21.9219C64.3579 23.4203 65.6762 24.4999 67.2082 24.4999L74.6942 24.4999C76.2294 24.4999 77.5496 23.4158 77.8439 21.9135L81.5171 3.16662C81.5916 2.78643 82.153 2.84061 82.153 3.22798V24.4999H88.5714V2.74002C88.5714 1.5029 87.5656 0.500025 86.3249 0.500025L78.3813 0.50002C76.859 0.500019 75.5461 1.56646 75.2383 3.05306L71.2361 22.3844C71.1655 22.7251 70.6773 22.7246 70.6074 22.3838L66.6405 3.05837C66.3349 1.56926 65.0208 0.500019 63.4964 0.50002L55.5159 0.500025Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M26.5569 0.50009H33.3531L39.2253 13.0169C39.4563 13.5093 40.1589 13.5085 40.3888 13.0155L46.2237 0.50009H53.1458L41.8949 24.5H34.9858L39.4713 14.9H35.3564C34.1107 14.9 32.9775 14.1813 32.4496 13.0563L26.5569 0.50009Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M63 0H56V7H63H70H77V14V21V28H84V21V14C84 6.26801 77.732 0 70 0H63ZM7 77H14H21H28V84H21H14C6.26801 84 0 77.732 0 70V63V56H7L7 63L7 70L7 77ZM70 84C77.732 84 84 77.732 84 70V63V56H77V63V70V77H70H63H56V84H63H70ZM7 7L7 14L7 21L7 28H0V21V14C1.93187e-06 6.26801 6.26802 0 14 0H21H28V7L21 7L14 7L7 7ZM63 21L63 28L56 28L56 21L63 21ZM63 21V14L70 14L70 21L63 21ZM49 35V28L56 28V35H49ZM49 49V42L49 35L42 35L35 35L35 28L28 28L28 21L21 21L21 14L14 14L14 21L21 21L21 28L28 28V35L35 35V42V49L28 49L28 56L21 56L21 63H14L14 70H21L21 63L28 63L28 56H35V49H42L49 49ZM56 56H49L49 49L56 49V56ZM63 63L56 63V56L63 56L63 63ZM63 63H70V70H63L63 63Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 872 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M84 14C84 6.26801 77.732 0 70 0H63H56V7H63H70H77V14V21V28H84V21V14ZM28 14C20.268 14 14 20.268 14 28C14 35.732 20.268 42 28 42C20.268 42 14 48.268 14 56C14 63.732 20.268 70 28 70C35.7299 70 41.9966 63.7354 42 56.0062C42.0034 63.7353 48.2701 70 56 70C63.732 70 70 63.732 70 56C70 48.2701 63.7353 42.0034 56.0062 42C63.7353 41.9966 70 35.7299 70 28C70 20.268 63.732 14 56 14C48.268 14 42 20.268 42 28C42 20.268 35.732 14 28 14ZM49 28V35L56 35H63V28V21L56 21H49V28ZM42 28.0062C41.9966 35.7329 35.7339 41.996 28.0074 42C35.736 42.004 42 48.2705 42 56C42 48.268 48.268 42 56 42C48.2701 42 42.0034 35.7353 42 28.0062ZM56 63H63V56V49L56 49H49V56V63L56 63ZM28 35H35V28V21L28 21H21V28L21 35L28 35ZM21 49V56L21 63L28 63H35V56V49L28 49H21ZM7 77H14H21H28V84H21H14C6.26801 84 0 77.732 0 70V63V56H7L7 63L7 70L7 77ZM70 84C77.732 84 84 77.732 84 70V63V56H77V63V70V77H70H63H56V84H63H70ZM7 7L7 14L7 21L7 28H0V21V14C1.93187e-06 6.26801 6.26802 0 14 0H21H28V7L21 7L14 7L7 7Z" fill="#14E76F" style="fill:#14E76F;fill:color(display-p3 0.0784 0.9059 0.4353);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.90246 15.9332C8.90246 15.9332 15.4128 13.1912 17.6706 12.2256C18.5362 11.8394 21.4715 10.6036 21.4715 10.6036C21.4715 10.6036 22.8262 10.0629 22.7133 11.376C22.6757 11.9167 22.3746 13.8091 22.0736 15.8559C21.622 18.7525 21.1328 21.9194 21.1328 21.9194C21.1328 21.9194 21.0575 22.8077 20.4178 22.9621C19.7781 23.1166 18.7243 22.4215 18.5362 22.267C18.3856 22.1511 15.7138 20.4132 14.7354 19.5635C14.4719 19.3318 14.1709 18.8684 14.773 18.3277C16.1278 17.0532 17.7459 15.4698 18.7243 14.4656C19.1759 14.0022 19.6275 12.9208 17.7459 14.2339C15.0741 16.1263 12.4398 17.9029 12.4398 17.9029C12.4398 17.9029 11.8377 18.289 10.7088 17.9414C9.57979 17.5939 8.26268 17.1304 8.26268 17.1304C8.26268 17.1304 7.35957 16.5511 8.90246 15.9332Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
@@ -0,0 +1,23 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
>
|
||||
<g clipPath="url(#clip0_2549_7563)">
|
||||
<path
|
||||
d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2549_7563">
|
||||
<rect width="24" height="24" fill="white" transform="translate(0 0.5)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<path d="M17.4206 15.4955L22.9798 9.03333H21.6625L16.8354 14.6444L12.98 9.03333H8.5332L14.3633 17.5182L8.5332 24.2948H9.85065L14.9482 18.3694L19.0198 24.2948H23.4665L17.4202 15.4955H17.4206ZM15.6161 17.593L15.0254 16.7481L10.3253 10.0251H12.3489L16.1419 15.4507L16.7326 16.2956L21.6631 23.3482H19.6396L15.6161 17.5933V17.593Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 453 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<path d="M24.9004 12.0986C24.6915 11.3132 24.0734 10.6936 23.2871 10.4812C21.8652 10.1001 16.1605 10.1001 16.1605 10.1001C16.1605 10.1001 10.4587 10.1001 9.03399 10.4812C8.25053 10.6906 7.63247 11.3103 7.42065 12.0986C7.04053 13.5241 7.04053 16.5001 7.04053 16.5001C7.04053 16.5001 7.04053 19.4761 7.42065 20.9016C7.62957 21.687 8.24763 22.3066 9.03399 22.519C10.4587 22.9001 16.1605 22.9001 16.1605 22.9001C16.1605 22.9001 21.8652 22.9001 23.2871 22.519C24.0705 22.3096 24.6886 21.6899 24.9004 20.9016C25.2805 19.4761 25.2805 16.5001 25.2805 16.5001C25.2805 16.5001 25.2805 13.5241 24.9004 12.0986Z" fill="currentColor"/>
|
||||
<path d="M14.3383 19.2434L19.0767 16.5001L14.3383 13.7568V19.2434Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 810 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24.9004 12.0986C24.6915 11.3132 24.0734 10.6936 23.2871 10.4812C21.8652 10.1001 16.1605 10.1001 16.1605 10.1001C16.1605 10.1001 10.4587 10.1001 9.03399 10.4812C8.25053 10.6906 7.63247 11.3103 7.42065 12.0986C7.04053 13.5241 7.04053 16.5001 7.04053 16.5001C7.04053 16.5001 7.04053 19.4761 7.42065 20.9016C7.62957 21.687 8.24763 22.3066 9.03399 22.519C10.4587 22.9001 16.1605 22.9001 16.1605 22.9001C16.1605 22.9001 21.8652 22.9001 23.2871 22.519C24.0705 22.3096 24.6886 21.6899 24.9004 20.9016C25.2805 19.4761 25.2805 16.5001 25.2805 16.5001C25.2805 16.5001 25.2805 13.5241 24.9004 12.0986Z" fill="currentColor"/>
|
||||
<path d="M14.3383 19.2434L19.0767 16.5001L14.3383 13.7568V19.2434Z" fill="#242B2D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 811 B |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 543 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 387 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,62 @@
|
||||
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export default async function Account({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ address: string }>;
|
||||
}) {
|
||||
try {
|
||||
const address = (await params).address;
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<SectionHeading title="Nym Node Details" />
|
||||
<ExplorerButtonGroup
|
||||
onPage="Account"
|
||||
options={[
|
||||
{
|
||||
label: "Nym Node",
|
||||
isSelected: true,
|
||||
link: `/account/${address}/not-found/`,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: false,
|
||||
link: `/account/${address}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography variant="h5">
|
||||
<Markdown className="reactMarkDownLink">
|
||||
This account does’t have a Nym node bonded. Is this your account?
|
||||
Start [setting up your node](https://nym.com/docs) today!
|
||||
</Markdown>
|
||||
</Typography>
|
||||
{/* <Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
</Grid>
|
||||
<BlogArticlesCards ids={[1]} />
|
||||
</Grid> */}
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
let errorMessage = "An error occurred";
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { fetchNodes } from "@/app/api";
|
||||
import type { NodeData } from "@/app/api/types";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { AccountBalancesCard } from "../../../../components/accountPageComponents/AccountBalancesCard";
|
||||
import { AccountInfoCard } from "../../../../components/accountPageComponents/AccountInfoCard";
|
||||
import { ContentLayout } from "../../../../components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "../../../../components/headings/SectionHeading";
|
||||
import ExplorerButtonGroup from "../../../../components/toggleButton/ToggleButton";
|
||||
|
||||
export default async function Account({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ address: string }>;
|
||||
}) {
|
||||
try {
|
||||
const address = (await params).address;
|
||||
|
||||
const nymNodes: NodeData[] = await fetchNodes();
|
||||
|
||||
const nymNode = nymNodes.find(
|
||||
(node) => node.bond_information.owner === address,
|
||||
);
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={6}>
|
||||
<SectionHeading title="Account Details" />
|
||||
</Grid>
|
||||
|
||||
<Grid size={6} justifyContent="flex-end">
|
||||
<Box sx={{ display: "flex", justifyContent: "end" }}>
|
||||
<ExplorerButtonGroup
|
||||
onPage="Account"
|
||||
options={[
|
||||
{
|
||||
label: "Nym Node",
|
||||
isSelected: false,
|
||||
link: nymNode
|
||||
? `/nym-node/${nymNode.node_id}`
|
||||
: `/account/${address}/not-found`,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: true,
|
||||
link: `/account/${address}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<AccountInfoCard address={address} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<AccountBalancesCard address={address} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("error :>> ", error);
|
||||
return <Typography>Error loading account data</Typography>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { fetchNodeIdByIdentityKey, fetchNodeInfo } from "@/app/api";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard";
|
||||
import { NodeDataCard } from "@/components/nymNodePageComponents/NodeDataCard";
|
||||
// import { NodeChatCard } from "@/components/nymNodePageComponents/ChatCard";
|
||||
import NodeDelegationsCard from "@/components/nymNodePageComponents/NodeDelegationsCard";
|
||||
import { NodeParametersCard } from "@/components/nymNodePageComponents/NodeParametersCard";
|
||||
import { NodeProfileCard } from "@/components/nymNodePageComponents/NodeProfileCard";
|
||||
import { NodeRoleCard } from "@/components/nymNodePageComponents/NodeRoleCard";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { Box } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
|
||||
export default async function NymNode({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>; // node_id or identity_key
|
||||
}) {
|
||||
try {
|
||||
let id: string | number;
|
||||
const paramsId = (await params).id;
|
||||
|
||||
// check if the params id is a node_id or identity_key
|
||||
|
||||
if (paramsId.length > 10) {
|
||||
id = await fetchNodeIdByIdentityKey(paramsId);
|
||||
} else {
|
||||
id = Number(paramsId);
|
||||
}
|
||||
|
||||
const observatoryNymNode = await fetchNodeInfo(id);
|
||||
|
||||
if (!observatoryNymNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<SectionHeading title="Nym Node Details" />
|
||||
{observatoryNymNode.bonding_address && (
|
||||
<ExplorerButtonGroup
|
||||
onPage="Nym Node"
|
||||
options={[
|
||||
{
|
||||
label: "Nym Node",
|
||||
isSelected: true,
|
||||
link: `/nym-node/${id}`,
|
||||
},
|
||||
{
|
||||
label: "Account",
|
||||
isSelected: false,
|
||||
link: `/account/${observatoryNymNode.bonding_address}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<NodeProfileCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<BasicInfoCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 4,
|
||||
}}
|
||||
>
|
||||
<NodeRoleCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeParametersCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
md: 6,
|
||||
}}
|
||||
>
|
||||
<NodeDataCard id={id} />
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
}}
|
||||
>
|
||||
<NodeDelegationsCard id={id} />
|
||||
</Grid>
|
||||
{/*
|
||||
<Grid
|
||||
size={{
|
||||
xs: 12,
|
||||
}}
|
||||
>
|
||||
<NodeChatCard />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
let errorMessage = "An error occurred";
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import TableOfContents from "@/components/blogs/TableOfContents";
|
||||
import type BlogArticle from "@/components/blogs/types";
|
||||
import { Breadcrumbs } from "@/components/breadcrumbs/Breadcrumbs";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import { Link } from "@/components/muiLink";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { format } from "date-fns";
|
||||
import Image from "next/image";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export default async function BlogPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
const blogArticle: BlogArticle = await import(`@/data/${slug}.json`);
|
||||
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
label: "Onboarding",
|
||||
href: "/onboarding",
|
||||
},
|
||||
{ label: blogArticle.title, isCurrentPage: true },
|
||||
];
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Wrapper>
|
||||
<Grid container spacing={5}>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Stack spacing={4}>
|
||||
<Breadcrumbs items={breadcrumbItems} />
|
||||
<SectionHeading title={blogArticle.title} />
|
||||
<Box
|
||||
sx={{
|
||||
borderTop: "1px dashed",
|
||||
paddingBlockStart: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle3"
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
Author
|
||||
{(blogArticle?.attributes?.blogAuthors?.length ?? 0) > 1
|
||||
? "s"
|
||||
: ""}
|
||||
:{" "}
|
||||
{blogArticle?.attributes?.blogAuthors?.map(
|
||||
(author: string) => (
|
||||
<Typography key={author} variant="subtitle3">
|
||||
{author}
|
||||
</Typography>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
<time dateTime={blogArticle?.attributes?.date.toString()}>
|
||||
{format(
|
||||
new Date(blogArticle?.attributes?.date),
|
||||
"MMMM dd, yyyy"
|
||||
)}
|
||||
</time>
|
||||
</Typography>
|
||||
<Typography variant="subtitle3">
|
||||
{blogArticle.attributes.readingTime}{" "}
|
||||
{blogArticle.attributes.readingTime > 1 ? "mins" : "min"}{" "}
|
||||
read
|
||||
</Typography>
|
||||
</Box>
|
||||
<Image
|
||||
src={blogArticle.image}
|
||||
alt="blog-image"
|
||||
width={120}
|
||||
height={60}
|
||||
sizes="100vw"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
{blogArticle.overview.content.map(({ text }) => (
|
||||
<Box key={text} sx={{ mt: 3 }}>
|
||||
<Typography variant="body2" component="span">
|
||||
<Markdown className="reactMarkDownLink reactMarkDownList">
|
||||
{text}
|
||||
</Markdown>
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{blogArticle.sections.map((section) => (
|
||||
<Box key={section.heading} id={section.id}>
|
||||
<SectionHeading title={section.heading} />
|
||||
{section.text.map(({ text }) => (
|
||||
<Box key={text} sx={{ mt: 3 }}>
|
||||
<Typography variant="body2" component="span">
|
||||
<Markdown className="reactMarkDownLink reactMarkDownList">
|
||||
{text}
|
||||
</Markdown>
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid size={{ md: 4 }}>
|
||||
<TableOfContents
|
||||
headings={blogArticle.sections.map((section) => ({
|
||||
heading: section.heading,
|
||||
id: section.id,
|
||||
}))}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Wrapper>
|
||||
</ContentLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Wrapper>
|
||||
<SectionHeading title={"Off the grid, like your data"} />
|
||||
<Typography variant="body2">
|
||||
Oops! Looks like the page you’re looking for got mixed up in the
|
||||
noise. Don’t worry, your privacy is intact. Let’s get you
|
||||
<Link href="/">back to the homepage.</Link>
|
||||
</Typography>
|
||||
</Wrapper>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<SectionHeading title="Onboarding page" />
|
||||
<Grid container spacing={4}>
|
||||
<BlogArticlesCards ids={[1, 2]} />
|
||||
</Grid>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
// import Grid from "@mui/material/Grid2";
|
||||
import { ContentLayout } from "../../../components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "../../../components/headings/SectionHeading";
|
||||
import OverviewCards from "../../../components/staking/OverviewCards";
|
||||
import StakeTableWithAction from "../../../components/staking/StakeTableWithAction";
|
||||
import SubHeaderRow from "../../../components/staking/SubHeaderRow";
|
||||
|
||||
export default async function StakingPage() {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<SectionHeading title="Staking" />
|
||||
<SubHeaderRow />
|
||||
<OverviewCards />
|
||||
<StakeTableWithAction />
|
||||
{/* <Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
</Grid>
|
||||
<BlogArticlesCards ids={[1]} />
|
||||
</Grid> */}
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "@/components/headings/SectionHeading";
|
||||
import NodeTableWithAction from "@/components/nodeTable/NodeTableWithAction";
|
||||
import NodeAndAddressSearch from "@/components/search/NodeAndAddressSearch";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
// import Grid from "@mui/material/Grid2";
|
||||
|
||||
export default function ExplorerPage() {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Wrapper>
|
||||
<Stack gap={5}>
|
||||
<SectionHeading title="Explorer" />
|
||||
<NodeAndAddressSearch />
|
||||
</Stack>
|
||||
<Box sx={{ mt: 5 }}>
|
||||
<NodeTableWithAction />
|
||||
</Box>
|
||||
{/* <Grid container columnSpacing={5} rowSpacing={5} mt={10}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
</Grid>
|
||||
<BlogArticlesCards limit={2} />
|
||||
</Grid> */}
|
||||
</Wrapper>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { addSeconds } from "date-fns";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
CurrentEpochData,
|
||||
ExplorerData,
|
||||
GatewayStatus,
|
||||
IAccountBalancesInfo,
|
||||
IObservatoryNode,
|
||||
IPacketsAndStakingData,
|
||||
NodeData,
|
||||
NodeRewardDetails,
|
||||
NymTokenomics,
|
||||
ObservatoryBalance,
|
||||
} from "./types";
|
||||
import {
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
DATA_OBSERVATORY_BALANCES_URL,
|
||||
DATA_OBSERVATORY_NODES_DELEGATIONS_URL,
|
||||
DATA_OBSERVATORY_NODES_URL,
|
||||
NS_API_MIXNODES_STATS,
|
||||
NYM_ACCOUNT_ADDRESS,
|
||||
NYM_NODES,
|
||||
NYM_PRICES_API,
|
||||
OBSERVATORY_GATEWAYS_URL,
|
||||
} from "./urls";
|
||||
|
||||
// Fetch function for epoch rewards
|
||||
export const fetchEpochRewards = async (): Promise<
|
||||
ExplorerData["currentEpochRewardsData"]
|
||||
> => {
|
||||
const response = await fetch(CURRENT_EPOCH_REWARDS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
cache: "no-store", // Ensures fresh data on every request
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch epoch rewards");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// Fetch gateway status based on identity key
|
||||
export const fetchGatewayStatus = async (
|
||||
identityKey: string,
|
||||
): Promise<GatewayStatus | null> => {
|
||||
const response = await fetch(`${OBSERVATORY_GATEWAYS_URL}/${identityKey}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch gateway status");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchNodeInfo = async (
|
||||
id: number,
|
||||
): Promise<IObservatoryNode | undefined> => {
|
||||
const nodes = await fetchObservatoryNodes();
|
||||
return nodes?.find((node) => node.node_id === id);
|
||||
};
|
||||
|
||||
export const fetchNodeIdByIdentityKey = async (
|
||||
identity_key: string,
|
||||
): Promise<number> => {
|
||||
const nodes = await fetchObservatoryNodes();
|
||||
const node = nodes?.find((node) => node.identity_key === identity_key);
|
||||
return node?.node_id || 0;
|
||||
};
|
||||
|
||||
export const fetchNodeDelegations = async (
|
||||
id: number,
|
||||
): Promise<NodeRewardDetails[]> => {
|
||||
const response = await fetch(
|
||||
`${DATA_OBSERVATORY_NODES_DELEGATIONS_URL}/${id}/delegations`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch delegations");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchCurrentEpoch = async () => {
|
||||
const response = await fetch(CURRENT_EPOCH, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
cache: "no-store", // Ensures fresh data on every request
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch current epoch data");
|
||||
}
|
||||
|
||||
const data: CurrentEpochData = await response.json();
|
||||
const epochEndTime = addSeconds(
|
||||
new Date(data.current_epoch_start),
|
||||
data.epoch_length.secs,
|
||||
).toISOString();
|
||||
|
||||
return { ...data, current_epoch_end: epochEndTime };
|
||||
};
|
||||
|
||||
// Fetch balances based on the address
|
||||
export const fetchBalances = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Calculate total stake
|
||||
return (
|
||||
Number(balances.rewards.staking_rewards.amount) +
|
||||
Number(balances.delegated.amount)
|
||||
);
|
||||
};
|
||||
|
||||
// Fetch function to get total staker rewards
|
||||
export const fetchTotalStakerRewards = async (
|
||||
address: string,
|
||||
): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Return the staking rewards amount
|
||||
return Number(balances.rewards.staking_rewards.amount);
|
||||
};
|
||||
|
||||
// Fetch function to get the original stake
|
||||
export const fetchOriginalStake = async (address: string): Promise<number> => {
|
||||
const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch balances");
|
||||
}
|
||||
|
||||
const balances: ObservatoryBalance = await response.json();
|
||||
|
||||
// Return the delegated amount
|
||||
return Number(balances.delegated.amount);
|
||||
};
|
||||
|
||||
export const fetchNoise = async (): Promise<IPacketsAndStakingData[]> => {
|
||||
const response = await fetch(NS_API_MIXNODES_STATS, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
|
||||
const data: IPacketsAndStakingData[] = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
// Fetch Account Balance
|
||||
export const fetchAccountBalance = async (
|
||||
address: string,
|
||||
): Promise<IAccountBalancesInfo> => {
|
||||
const res = await fetch(`${NYM_ACCOUNT_ADDRESS}/${address}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch account balance error from api");
|
||||
}
|
||||
|
||||
const data: IAccountBalancesInfo = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
// 🔹 Fetch Nodes
|
||||
export const fetchNodes = async (): Promise<NodeData[]> => {
|
||||
const res = await fetch(NYM_NODES, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch nodes");
|
||||
}
|
||||
const data: NodeData[] = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchObservatoryNodes = async (): Promise<IObservatoryNode[]> => {
|
||||
const allNodes: IObservatoryNode[] = [];
|
||||
let page = 1;
|
||||
const PAGE_SIZE = 200;
|
||||
let hasMoreData = true;
|
||||
|
||||
while (hasMoreData) {
|
||||
const response = await fetch(
|
||||
`${DATA_OBSERVATORY_NODES_URL}?page=${page}&limit=${PAGE_SIZE}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch observatory nodes (page ${page})`);
|
||||
}
|
||||
|
||||
const nodes: IObservatoryNode[] = await response.json();
|
||||
allNodes.push(...nodes);
|
||||
|
||||
if (nodes.length < PAGE_SIZE) {
|
||||
hasMoreData = false; // Stop fetching when the last page has fewer than 200 items
|
||||
} else {
|
||||
page++; // Move to the next page
|
||||
}
|
||||
}
|
||||
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
// 🔹 Fetch NYM Price
|
||||
export const fetchNymPrice = async (): Promise<NymTokenomics> => {
|
||||
const res = await fetch(NYM_PRICES_API, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch NYM price");
|
||||
}
|
||||
const data: NymTokenomics = await res.json();
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,482 @@
|
||||
export type API_RESPONSE<T> = {
|
||||
data: T[];
|
||||
};
|
||||
|
||||
export type Denom = "unym" | "nym";
|
||||
|
||||
export interface IPacketsAndStakingData {
|
||||
date_utc: string;
|
||||
total_packets_received: number;
|
||||
total_packets_sent: number;
|
||||
total_packets_dropped: number;
|
||||
total_stake: number;
|
||||
}
|
||||
|
||||
export interface CurrentEpochData {
|
||||
id: number;
|
||||
current_epoch_id: number;
|
||||
current_epoch_start: string;
|
||||
epoch_length: { secs: number; nanos: number };
|
||||
epochs_in_interval: number;
|
||||
total_elapsed_epochs: number;
|
||||
}
|
||||
export interface ExplorerData {
|
||||
circulatingNymSupplyData: {
|
||||
circulating_supply: { denom: Denom; amount: string };
|
||||
mixmining_reserve: { denom: Denom; amount: string };
|
||||
total_supply: { denom: Denom; amount: string };
|
||||
vesting_tokens: { denom: Denom; amount: string };
|
||||
};
|
||||
nymNodesData: {
|
||||
gateways: {
|
||||
bonded: { count: number; last_updated_utc: string };
|
||||
blacklisted: { count: number; last_updated_utc: string };
|
||||
historical: { count: number; last_updated_utc: string };
|
||||
explorer: { count: number; last_updated_utc: string };
|
||||
};
|
||||
mixnodes: {
|
||||
bonded: {
|
||||
count: number;
|
||||
active: number;
|
||||
inactive: number;
|
||||
reserve: number;
|
||||
last_updated_utc: string;
|
||||
};
|
||||
blacklisted: {
|
||||
count: number;
|
||||
last_updated_utc: string;
|
||||
};
|
||||
historical: { count: number; last_updated_utc: string };
|
||||
};
|
||||
};
|
||||
packetsAndStakingData: IPacketsAndStakingData[];
|
||||
|
||||
currentEpochRewardsData: {
|
||||
interval: {
|
||||
reward_pool: string;
|
||||
staking_supply: string;
|
||||
staking_supply_scale_factor: string;
|
||||
epoch_reward_budget: string;
|
||||
stake_saturation_point: string;
|
||||
active_set_work_factor: string;
|
||||
interval_pool_emission: string;
|
||||
sybil_resistance: string;
|
||||
};
|
||||
rewarded_set: {
|
||||
entry_gateways: number;
|
||||
exit_gateways: number;
|
||||
mixnodes: number;
|
||||
standby: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type NodeDescription = {
|
||||
last_polled: string;
|
||||
host_information: {
|
||||
ip_address: string[];
|
||||
hostname: string;
|
||||
keys: {
|
||||
ed25519: string;
|
||||
x25519: string;
|
||||
x25519_noise: string | null;
|
||||
};
|
||||
};
|
||||
declared_role: {
|
||||
mixnode: boolean;
|
||||
entry: boolean;
|
||||
exit_nr: boolean;
|
||||
exit_ipr: boolean;
|
||||
};
|
||||
auxiliary_details: {
|
||||
location: string;
|
||||
announce_ports: {
|
||||
verloc_port: number | null;
|
||||
mix_port: number | null;
|
||||
};
|
||||
accepted_operator_terms_and_conditions: boolean;
|
||||
};
|
||||
build_information: {
|
||||
binary_name: string;
|
||||
build_timestamp: string;
|
||||
build_version: string;
|
||||
commit_sha: string;
|
||||
commit_timestamp: string;
|
||||
commit_branch: string;
|
||||
rustc_version: string;
|
||||
rustc_channel: string;
|
||||
cargo_profile: string;
|
||||
cargo_triple: string;
|
||||
};
|
||||
network_requester: {
|
||||
address: string;
|
||||
uses_exit_policy: boolean;
|
||||
};
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
};
|
||||
authenticator: {
|
||||
address: string;
|
||||
};
|
||||
wireguard: string | null;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number | null;
|
||||
};
|
||||
} | null;
|
||||
|
||||
export type BondInformation = {
|
||||
node_id: number;
|
||||
owner: string;
|
||||
original_pledge: {
|
||||
denom: string;
|
||||
amount: string;
|
||||
};
|
||||
bonding_height: number;
|
||||
is_unbonding: boolean;
|
||||
node: {
|
||||
host: string;
|
||||
custom_http_port: number;
|
||||
identity_key: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type RewardingDetails = {
|
||||
cost_params: {
|
||||
profit_margin_percent: string;
|
||||
interval_operating_cost: {
|
||||
denom: string;
|
||||
amount: string;
|
||||
};
|
||||
};
|
||||
operator: string;
|
||||
delegates: string;
|
||||
total_unit_reward: string;
|
||||
unit_delegation: string;
|
||||
last_rewarded_epoch: number;
|
||||
unique_delegations: number;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
two_letter_iso_country_code?: string;
|
||||
three_letter_iso_country_code?: string;
|
||||
country_name?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
};
|
||||
|
||||
export type NodeData = {
|
||||
node_id: number;
|
||||
contract_node_type: string;
|
||||
description: NodeDescription;
|
||||
bond_information: BondInformation;
|
||||
rewarding_details: RewardingDetails;
|
||||
location: Location;
|
||||
};
|
||||
|
||||
// ACCOUNT BALANCES
|
||||
|
||||
export interface IRewardDetails {
|
||||
amount_staked: IAmountDetails;
|
||||
node_id: number;
|
||||
node_still_fully_bonded: boolean;
|
||||
rewards: IAmountDetails;
|
||||
}
|
||||
|
||||
export interface IAmountDetails {
|
||||
denom: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export interface IDelegationDetails {
|
||||
node_id: number;
|
||||
delegated: IAmountDetails;
|
||||
height: number;
|
||||
proxy: null | string;
|
||||
}
|
||||
|
||||
export interface IAccountBalancesInfo {
|
||||
accumulated_rewards: IRewardDetails[];
|
||||
address: string;
|
||||
balances: IAmountDetails[];
|
||||
claimable_rewards: IAmountDetails;
|
||||
delegations: IDelegationDetails[];
|
||||
operator_rewards?: null | IAmountDetails;
|
||||
total_delegations: IAmountDetails;
|
||||
total_value: IAmountDetails;
|
||||
vesting_account?: null | string;
|
||||
}
|
||||
|
||||
export interface IObservatoryNode {
|
||||
accepted_tnc: boolean;
|
||||
bonded: boolean;
|
||||
bonding_address: string;
|
||||
description: {
|
||||
authenticator: {
|
||||
address: string;
|
||||
};
|
||||
auxiliary_details: {
|
||||
accepted_operator_terms_and_conditions: boolean;
|
||||
announce_ports: {
|
||||
mix_port: number | null;
|
||||
verloc_port: number | null;
|
||||
};
|
||||
location: string | null;
|
||||
};
|
||||
build_information: {
|
||||
binary_name: string;
|
||||
build_timestamp: string;
|
||||
build_version: string;
|
||||
cargo_profile: string;
|
||||
cargo_triple: string;
|
||||
commit_branch: string;
|
||||
commit_sha: string;
|
||||
commit_timestamp: string;
|
||||
rustc_channel: string;
|
||||
rustc_version: string;
|
||||
};
|
||||
declared_role: {
|
||||
entry: boolean;
|
||||
exit_ipr: boolean;
|
||||
exit_nr: boolean;
|
||||
mixnode: boolean;
|
||||
};
|
||||
host_information: {
|
||||
hostname: string | null;
|
||||
ip_address: string[];
|
||||
};
|
||||
keys: {
|
||||
ed25519: string;
|
||||
x25519: string;
|
||||
x25519_noise: string | null;
|
||||
};
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
};
|
||||
last_polled: string;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number | null;
|
||||
};
|
||||
network_requester: {
|
||||
address: string;
|
||||
uses_exit_policy: boolean;
|
||||
};
|
||||
wireguard: string | null;
|
||||
geoip: {
|
||||
city: string;
|
||||
country: string;
|
||||
ip_address: string;
|
||||
loc: string;
|
||||
node_id: number;
|
||||
org: string;
|
||||
postal: string;
|
||||
region: string;
|
||||
};
|
||||
};
|
||||
identity_key: string;
|
||||
ip_address: string;
|
||||
node_id: number;
|
||||
node_type: string;
|
||||
original_pledge: number;
|
||||
rewarding_details: {
|
||||
cost_params: {
|
||||
interval_operating_cost: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
};
|
||||
profit_margin_percent: string;
|
||||
};
|
||||
delegates: string;
|
||||
last_rewarded_epoch: number;
|
||||
operator: string;
|
||||
total_unit_reward: string;
|
||||
unique_delegations: number;
|
||||
unit_delegation: string;
|
||||
};
|
||||
self_description: {
|
||||
details: string;
|
||||
moniker: string;
|
||||
security_contact: string;
|
||||
website: string;
|
||||
};
|
||||
total_stake: number;
|
||||
uptime: number;
|
||||
}
|
||||
export interface NodeRewardDetails {
|
||||
amount: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
};
|
||||
cumulative_reward_ratio: string;
|
||||
height: number;
|
||||
node_id: number;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
export type LastProbeResult = {
|
||||
gateway: string;
|
||||
outcome: {
|
||||
as_entry: {
|
||||
can_connect: boolean;
|
||||
can_route: boolean;
|
||||
};
|
||||
as_exit: {
|
||||
can_connect: boolean;
|
||||
can_route_ip_external_v4: boolean;
|
||||
can_route_ip_external_v6: boolean;
|
||||
can_route_ip_v4: boolean;
|
||||
can_route_ip_v6: boolean;
|
||||
};
|
||||
wg: {
|
||||
can_handshake_v4: boolean;
|
||||
can_handshake_v6: boolean;
|
||||
can_register: boolean;
|
||||
can_resolve_dns_v4: boolean;
|
||||
can_resolve_dns_v6: boolean;
|
||||
ping_hosts_performance_v4: number;
|
||||
ping_hosts_performance_v6: number;
|
||||
ping_ips_performance_v4: number;
|
||||
ping_ips_performance_v6: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type GatewayStatus = {
|
||||
blacklisted: boolean;
|
||||
bonded: boolean;
|
||||
config_score: number;
|
||||
description: {
|
||||
details: string;
|
||||
moniker: string;
|
||||
security_contact: string;
|
||||
website: string;
|
||||
};
|
||||
explorer_pretty_bond: {
|
||||
identity_key: string;
|
||||
location: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
two_letter_iso_country_code: string;
|
||||
};
|
||||
owner: string;
|
||||
pledge_amount: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
};
|
||||
};
|
||||
gateway_identity_key: string;
|
||||
last_probe_log: string;
|
||||
last_probe_result: LastProbeResult; // Reference to the separate type
|
||||
last_testrun_utc: string;
|
||||
last_updated_utc: string;
|
||||
performance: number;
|
||||
routing_score: number;
|
||||
self_described: {
|
||||
authenticator: {
|
||||
address: string;
|
||||
};
|
||||
auxiliary_details: {
|
||||
accepted_operator_terms_and_conditions: boolean;
|
||||
announce_ports: {
|
||||
mix_port: number | null;
|
||||
verloc_port: number | null;
|
||||
};
|
||||
location: string;
|
||||
};
|
||||
build_information: {
|
||||
binary_name: string;
|
||||
build_timestamp: string;
|
||||
build_version: string;
|
||||
cargo_profile: string;
|
||||
cargo_triple: string;
|
||||
};
|
||||
declared_role: {
|
||||
entry: boolean;
|
||||
exit_ipr: boolean;
|
||||
exit_nr: boolean;
|
||||
mixnode: boolean;
|
||||
};
|
||||
host_information: {
|
||||
hostname: string;
|
||||
ip_address: string[];
|
||||
keys: {
|
||||
ed25519: string;
|
||||
x25519: string;
|
||||
x25519_noise: string | null;
|
||||
};
|
||||
};
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
};
|
||||
last_polled: string;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number | null;
|
||||
};
|
||||
network_requester: {
|
||||
address: string;
|
||||
uses_exit_policy: boolean;
|
||||
};
|
||||
wireguard: {
|
||||
port: number;
|
||||
public_key: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type BalanceDetails = {
|
||||
amount: number;
|
||||
denom: string;
|
||||
};
|
||||
|
||||
export type ObservatoryRewards = {
|
||||
operator_commissions: BalanceDetails;
|
||||
staking_rewards: BalanceDetails;
|
||||
unlocked: BalanceDetails;
|
||||
};
|
||||
|
||||
export type ObservatoryBalance = {
|
||||
delegated: BalanceDetails;
|
||||
locked: BalanceDetails;
|
||||
rewards: ObservatoryRewards;
|
||||
self_bonded: BalanceDetails;
|
||||
spendable: BalanceDetails;
|
||||
};
|
||||
|
||||
export type Quote = {
|
||||
ath_date: string;
|
||||
ath_price: number;
|
||||
market_cap: number;
|
||||
market_cap_change_24h: number;
|
||||
percent_change_12h: number;
|
||||
percent_change_15m: number;
|
||||
percent_change_1h: number;
|
||||
percent_change_1y: number;
|
||||
percent_change_24h: number;
|
||||
percent_change_30d: number;
|
||||
percent_change_30m: number;
|
||||
percent_change_6h: number;
|
||||
percent_change_7d: number;
|
||||
percent_from_price_ath: number;
|
||||
price: number;
|
||||
volume_24h: number;
|
||||
volume_24h_change_24h: number;
|
||||
};
|
||||
|
||||
export type Quotes = {
|
||||
USD: Quote;
|
||||
};
|
||||
|
||||
export type NymTokenomics = {
|
||||
beta_value: number;
|
||||
first_data_at: string;
|
||||
id: string;
|
||||
last_updated: string;
|
||||
max_supply: number;
|
||||
name: string;
|
||||
quotes: Quotes;
|
||||
rank: number;
|
||||
symbol: string;
|
||||
total_supply: number;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
export const NYM_NODES =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/nym-nodes";
|
||||
export const HARBOURMASTER_API_MIXNODES_STATS =
|
||||
"https://harbourmaster.nymtech.net/v2/mixnodes/stats";
|
||||
export const NS_API_MIXNODES_STATS =
|
||||
"https://staging-node-status-api.nymte.ch/v2/mixnodes/stats";
|
||||
|
||||
export const CURRENT_EPOCH =
|
||||
"https://validator.nymtech.net/api/v1/epoch/current";
|
||||
export const CURRENT_EPOCH_REWARDS =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const NYM_NODE_BONDED =
|
||||
"https://validator.nymtech.net/api/v1/nym-nodes/bonded";
|
||||
export const NYM_ACCOUNT_ADDRESS =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/account";
|
||||
export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price";
|
||||
export const VALIDATOR_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net";
|
||||
export const DATA_OBSERVATORY_NODES_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/nodes";
|
||||
export const DATA_OBSERVATORY_NODES_DELEGATIONS_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/nodes";
|
||||
export const DATA_OBSERVATORY_DELEGATIONS_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/delegations";
|
||||
export const DATA_OBSERVATORY_BALANCES_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/balances";
|
||||
export const OBSERVATORY_GATEWAYS_URL =
|
||||
"https://mainnet-node-status-api.nymtech.cc/v2/gateways";
|
||||
@@ -0,0 +1 @@
|
||||
export const TABLET_WIDTH = "(min-width:700px)";
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
|
||||
import { Link } from "@/components/muiLink";
|
||||
import { Button, Stack, Typography } from "@mui/material";
|
||||
|
||||
const ErrorPage = ({ error }: { error: Error }) => {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Stack spacing={2} justifyContent="flex-start">
|
||||
<Typography variant="body1">
|
||||
An error occurred: {error.message}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Please try again later or contact support
|
||||
</Typography>
|
||||
<Link href="/" underline="none">
|
||||
<Button variant="contained" sx={{ maxWidth: 100 }} size="small">
|
||||
Home
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
</ContentLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,34 @@
|
||||
// API
|
||||
import { client } from "../../../lib/strapiClient";
|
||||
|
||||
// Types
|
||||
import type { Languages } from "../../../i18n";
|
||||
|
||||
import type { components } from "@/app/lib/strapi";
|
||||
// Constants
|
||||
import { footerApiPath } from "../../footer/config/constants";
|
||||
|
||||
// Fetch footer data
|
||||
export const getFooter = async (
|
||||
locale: Languages,
|
||||
): Promise<{
|
||||
id?: number;
|
||||
attributes?: components["schemas"]["Footer"];
|
||||
} | null> => {
|
||||
const footer = await client.GET(footerApiPath, {
|
||||
params: {
|
||||
query: {
|
||||
locale,
|
||||
// @ts-expect-error - populate is not typed correctly?
|
||||
|
||||
populate: {
|
||||
linkBlocks: {
|
||||
populate: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return footer?.data?.data ? footer?.data?.data : null;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
// Types
|
||||
import type { components } from "../../../lib/strapi";
|
||||
|
||||
// Components
|
||||
import { Link } from "@/components/muiLink";
|
||||
|
||||
// MUI Components
|
||||
import { Box, Grid2, Typography } from "@mui/material";
|
||||
|
||||
export const FooterLinks = ({
|
||||
linkBlocks = [],
|
||||
}: {
|
||||
linkBlocks: components["schemas"]["FooterLinkBlockComponent"][];
|
||||
}) => {
|
||||
return (
|
||||
<Grid2
|
||||
container
|
||||
spacing={{ xs: 2, md: 3 }}
|
||||
columns={{ xs: 1, sm: 8, md: 5 }}
|
||||
>
|
||||
{linkBlocks?.map((block) => {
|
||||
return (
|
||||
<Grid2 key={block.id} size={{ xs: 1, sm: 4, md: 1 }}>
|
||||
<Typography
|
||||
component={block?.heading?.level || "h3"}
|
||||
variant="subtitle1"
|
||||
sx={{ mb: 4 }}
|
||||
>
|
||||
{block?.heading?.title}
|
||||
</Typography>
|
||||
<Box
|
||||
component={"ul"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{block?.links?.map((link) => {
|
||||
const isLinkExternal = link.url?.startsWith("http");
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
listStyle: "none",
|
||||
}}
|
||||
component={"li"}
|
||||
key={link.id}
|
||||
>
|
||||
<Link
|
||||
href={link?.url || ""}
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body3">
|
||||
{link.title}
|
||||
{isLinkExternal ? " ↗" : ""}
|
||||
</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Grid2>
|
||||
);
|
||||
})}
|
||||
</Grid2>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const footerApiPath = "/footer";
|
||||
@@ -0,0 +1,68 @@
|
||||
:root {
|
||||
--max-width: 1120px;
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiCardActionArea-focusHighlight {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #aaa;
|
||||
border-radius: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.reactMarkDownLink a {
|
||||
color: #000000;
|
||||
display: inline;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.reactMarkDownList ul {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.reactMarkDownList ol {
|
||||
margin-left: 20px;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createInstance } from "i18next";
|
||||
import resourcesToBackend from "i18next-resources-to-backend";
|
||||
import { initReactI18next } from "react-i18next/initReactI18next";
|
||||
import { getOptions } from "./settings";
|
||||
import type { Languages } from "./types";
|
||||
|
||||
const initI18next = async ({ lng, ns }: { lng: Languages; ns?: string }) => {
|
||||
const i18nInstance = createInstance();
|
||||
await i18nInstance
|
||||
.use(initReactI18next)
|
||||
.use(
|
||||
resourcesToBackend(
|
||||
(language: string, namespace: string) =>
|
||||
import(`./locales/${language}/${namespace}.json`),
|
||||
),
|
||||
)
|
||||
.init(getOptions(lng, ns));
|
||||
return i18nInstance;
|
||||
};
|
||||
|
||||
export const useTranslation = async ({
|
||||
lng,
|
||||
ns,
|
||||
options,
|
||||
}: {
|
||||
lng: Languages;
|
||||
ns?: string;
|
||||
options?: { keyPrefix: string };
|
||||
}) => {
|
||||
const i18nextInstance = await initI18next({ lng, ns });
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(
|
||||
lng,
|
||||
Array.isArray(ns) ? ns[0] : ns,
|
||||
options?.keyPrefix,
|
||||
),
|
||||
i18n: i18nextInstance,
|
||||
};
|
||||
};
|
||||
|
||||
export type { Languages, GeneralTranslations } from "./types";
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"langSelectorLabel": "Language",
|
||||
"socialLinks": [
|
||||
{ "name": "Telegram", "link": "https://nymtech.net/go/telegram" },
|
||||
{ "name": "Twitter", "link": "https://nymtech.net/go/x" },
|
||||
{
|
||||
"name": "Discord",
|
||||
"link": "https://nymtech.net/go/discord"
|
||||
},
|
||||
{ "name": "GitHub", "link": "https://nymtech.net/go/github" },
|
||||
{ "name": "YouTube", "link": "https://nymtech.net/go/youtube" }
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"title": "Get started",
|
||||
"links": [
|
||||
{
|
||||
"name": "Download NymVPN",
|
||||
"link": "download"
|
||||
},
|
||||
{
|
||||
"name": "Test NymVPN",
|
||||
"link": "alpha"
|
||||
},
|
||||
{
|
||||
"name": "Become an operator",
|
||||
"link": "https://nymtech.net/operators"
|
||||
},
|
||||
{ "name": "Visit nymtech.net", "link": "https://nymtech.net" },
|
||||
{
|
||||
"name": "Subscribe to Nym’s newsletter",
|
||||
"link": "https://eepurl.com/gdor_f"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Popular blog posts",
|
||||
"links": [
|
||||
{ "name": "Decentralized VPNs", "link": "blog/decentralized-vpns" },
|
||||
{
|
||||
"name": "Blockchain-based VPNs",
|
||||
"link": "blog/blockchain-based-vpns-all-you-need-to-know"
|
||||
},
|
||||
{
|
||||
"name": "Privacy protection with VPN",
|
||||
"link": "blog/how-does-a-vpn-protect-you-and-your-privacy"
|
||||
},
|
||||
{
|
||||
"name": "Tracking prevention with VPN",
|
||||
"link": "blog/can-you-be-tracked-while-using-a-vpn"
|
||||
},
|
||||
{
|
||||
"name": "Online privacy threats",
|
||||
"link": "blog/internet-privacy-main-threats-and-protections"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Resources",
|
||||
"links": [
|
||||
{ "name": "NymVPN blog", "link": "blog" },
|
||||
{
|
||||
"name": "NymVPN public roadmap",
|
||||
"link": "https://trello.com/b/qVhBo3e2/nymvpn-public-roadmap"
|
||||
},
|
||||
{
|
||||
"name": "NymVPN localization",
|
||||
"link": "https://weblate.nymte.ch/projects/nymvpn/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Company",
|
||||
"links": [
|
||||
{ "name": "Contact", "link": "contact" },
|
||||
{
|
||||
"name": "Careers",
|
||||
"link": "https://nym.teamtailor.com/"
|
||||
},
|
||||
{
|
||||
"name": "Support",
|
||||
"link": "https://support.nymvpn.com/hc/en-us"
|
||||
},
|
||||
{ "name": "Imprint", "link": "imprint" },
|
||||
{ "name": "Privacy statements", "link": "privacy" },
|
||||
{ "name": "Terms of use", "link": "terms" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"trademarkText": "WireGuard is a registered trademark of Jason A. Donenfeld",
|
||||
"rightsText": "© 2024 Nym Technologies S.A., all rights reserved"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"socialChannels": [
|
||||
{ "name": "Telegram", "link": "https://nym.com/go/telegram" },
|
||||
{ "name": "Twitter", "link": "https://nym.com/go/x" },
|
||||
{
|
||||
"name": "Discord",
|
||||
"link": "https://nym.com/go/discord"
|
||||
},
|
||||
{ "name": "GitHub", "link": "https://nym.com/go/github" },
|
||||
{ "name": "YouTube", "link": "https://nym.com/go/youtube" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export const fallbackLng = "en";
|
||||
export const languages = [fallbackLng, "es"] as const;
|
||||
export const defaultNS = "translation";
|
||||
export const cookieName = "i18next";
|
||||
|
||||
export function getOptions(lng = fallbackLng, ns = defaultNS) {
|
||||
return {
|
||||
// debug: true,
|
||||
supportedLngs: languages,
|
||||
fallbackLng,
|
||||
lng,
|
||||
fallbackNS: defaultNS,
|
||||
defaultNS,
|
||||
ns,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export interface RedeemFreePassCopy {
|
||||
errors: {
|
||||
invalidCode: string;
|
||||
alreadyRedeemed: string;
|
||||
unknown: string;
|
||||
};
|
||||
}
|
||||
|
||||
type GetStarted = {
|
||||
title: string;
|
||||
description: string;
|
||||
ctaText: string;
|
||||
inputLabel: string;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type AlphaPageSteps = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type AlphaPageTranslations = {
|
||||
hero: {
|
||||
title: {
|
||||
firstLine: string;
|
||||
secondLine?: string;
|
||||
};
|
||||
getCredential: GetStarted;
|
||||
signUp: GetStarted;
|
||||
imageAlt: string;
|
||||
stepsTitle: string;
|
||||
finalSteps: AlphaPageSteps[];
|
||||
finalStepsAlert: string;
|
||||
credentialsTitle: string;
|
||||
credentialsButton: string;
|
||||
};
|
||||
redeemFreePass: RedeemFreePassCopy;
|
||||
firstSection: {
|
||||
title: string;
|
||||
content: string[];
|
||||
};
|
||||
secondSection: {
|
||||
title: string;
|
||||
content: string[];
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { languages } from "../settings";
|
||||
|
||||
export type Languages = (typeof languages)[number];
|
||||
|
||||
// They don't belong to any specific page, that's why I defined them here, but open to suggestions.
|
||||
export type GeneralTranslations = {
|
||||
alert: string;
|
||||
copyToClipboard: {
|
||||
copy: string;
|
||||
copied: string;
|
||||
copiedTimeLimitation: string;
|
||||
};
|
||||
};
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,30 @@
|
||||
import { Header } from "@/components/header";
|
||||
import { Wrapper } from "@/components/wrapper";
|
||||
import Providers from "@/providers";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import "./globals.css";
|
||||
import "@interchain-ui/react/styles";
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Nym Explorer V2",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Providers>
|
||||
<Header />
|
||||
<Wrapper>{children}</Wrapper>
|
||||
<Footer />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import createClient from "openapi-fetch";
|
||||
import qs from "qs";
|
||||
import type { paths } from "./strapi";
|
||||
|
||||
if (!process.env.NEXT_PUBLIC_CMS_API_URL) {
|
||||
throw new Error(
|
||||
"NEXT_PUBLIC_CMS_API_URL environment variable is not defined",
|
||||
);
|
||||
}
|
||||
if (!process.env.NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE) {
|
||||
throw new Error(
|
||||
"NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE environment variable is not defined",
|
||||
);
|
||||
}
|
||||
|
||||
const client = createClient<paths>({
|
||||
baseUrl: process.env.NEXT_PUBLIC_CMS_API_URL,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
fetch: (request: unknown) => {
|
||||
const req = request as Request;
|
||||
const url = new URL(req.url, process.env.NEXT_PUBLIC_CMS_API_URL);
|
||||
|
||||
return fetch(new Request(url, req), {
|
||||
next: {
|
||||
revalidate: Number(process.env.NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE),
|
||||
},
|
||||
});
|
||||
},
|
||||
querySerializer(params) {
|
||||
return qs.stringify(params, {
|
||||
encodeValuesOnly: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export { client };
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import BlogArticlesCards from "../components/blogs/BlogArticleCards";
|
||||
import { ContentLayout } from "../components/contentLayout/ContentLayout";
|
||||
import SectionHeading from "../components/headings/SectionHeading";
|
||||
import { CurrentEpochCard } from "../components/landingPageComponents/CurrentEpochCard";
|
||||
import { NetworkStakeCard } from "../components/landingPageComponents/NetworkStakeCard";
|
||||
import { NoiseCard } from "../components/landingPageComponents/NoiseCard";
|
||||
import { RewardsCard } from "../components/landingPageComponents/StakersNumberCard";
|
||||
import { TokenomicsCard } from "../components/landingPageComponents/TokenomicsCard";
|
||||
import NodeTable from "../components/nodeTable/NodeTableWithAction";
|
||||
import NodeAndAddressSearch from "../components/search/NodeAndAddressSearch";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<ContentLayout>
|
||||
<Stack gap={5}>
|
||||
<Typography variant="h1" textTransform={"uppercase"}>
|
||||
Mixnet in your hands
|
||||
</Typography>
|
||||
<NodeAndAddressSearch />
|
||||
</Stack>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Noise Generating Mixnet Overview" />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<NoiseCard />
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
columnSpacing={5}
|
||||
rowSpacing={5}
|
||||
size={{ xs: 12, sm: 6, md: 3 }}
|
||||
>
|
||||
<Grid size={12}>
|
||||
<RewardsCard />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<CurrentEpochCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<NetworkStakeCard />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<TokenomicsCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Nym Nodes" />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<NodeTable />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Onboarding" />
|
||||
</Grid>
|
||||
<BlogArticlesCards ids={[1, 2]} />
|
||||
</Grid>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
import { fetchAccountBalance, fetchNymPrice } from "@/app/api";
|
||||
import { Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { IRewardDetails } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import { AccountBalancesTable } from "./AccountBalancesTable";
|
||||
|
||||
export interface IAccontStatsRowProps {
|
||||
type: string;
|
||||
allocation: number;
|
||||
amount: number;
|
||||
value: number;
|
||||
history?: { type: string; amount: number }[];
|
||||
isLastRow?: boolean;
|
||||
progressBarColor?: string;
|
||||
}
|
||||
|
||||
interface IAccountBalancesCardProps {
|
||||
address: string;
|
||||
}
|
||||
|
||||
const getNymsFormated = (unyms: number): number => {
|
||||
if (unyms === 0) {
|
||||
return 0;
|
||||
}
|
||||
const balance = unyms / 1000000;
|
||||
return balance;
|
||||
};
|
||||
const getPriceInUSD = (unyms: number, usdPrice: number): number => {
|
||||
if (unyms === 0) {
|
||||
return 0;
|
||||
}
|
||||
const balanceInUSD = (unyms / 1000000) * usdPrice;
|
||||
const balanceFormated = Number(balanceInUSD.toFixed(2));
|
||||
return balanceFormated;
|
||||
};
|
||||
|
||||
const getAllocation = (unyms: number, totalUnyms: number): number => {
|
||||
if (unyms === 0) {
|
||||
return 0;
|
||||
}
|
||||
const allocationPercentage = (unyms * 100) / totalUnyms;
|
||||
return Number(allocationPercentage.toFixed(2));
|
||||
};
|
||||
|
||||
const calculateStakingRewards = (
|
||||
accumulatedRewards: IRewardDetails[],
|
||||
): number => {
|
||||
if (accumulatedRewards.length > 0) {
|
||||
const totalRewards = accumulatedRewards.reduce((total, rewardDetail) => {
|
||||
return total + Number.parseFloat(rewardDetail.rewards.amount);
|
||||
}, 0);
|
||||
|
||||
const result = getNymsFormated(totalRewards);
|
||||
|
||||
return result;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
const { address } = props;
|
||||
|
||||
const {
|
||||
data: accountInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["accountBalance", address],
|
||||
queryFn: () => fetchAccountBalance(address),
|
||||
enabled: !!address,
|
||||
});
|
||||
|
||||
const {
|
||||
data: nymPrice,
|
||||
isLoading: isLoadingPrice,
|
||||
error: priceError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymPrice"],
|
||||
queryFn: fetchNymPrice,
|
||||
});
|
||||
|
||||
if (isLoading || isLoadingPrice) {
|
||||
return (
|
||||
<ExplorerCard label="Total value">
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" height={38} />
|
||||
<Skeleton variant="text" height={380} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || priceError || !accountInfo || !nymPrice) {
|
||||
return (
|
||||
<ExplorerCard label="Total value">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to account data.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const nymPriceData = nymPrice.quotes.USD.price;
|
||||
|
||||
const totalBalanceUSD = getPriceInUSD(
|
||||
Number(accountInfo.total_value.amount),
|
||||
nymPriceData,
|
||||
);
|
||||
const spendableNYM =
|
||||
accountInfo.balances.length > 0
|
||||
? getNymsFormated(Number(accountInfo.balances[0].amount))
|
||||
: 0;
|
||||
const spendableUSD =
|
||||
accountInfo.balances.length > 0
|
||||
? getPriceInUSD(Number(accountInfo.balances[0].amount), nymPriceData)
|
||||
: 0;
|
||||
const spendableAllocation =
|
||||
accountInfo.balances.length > 0
|
||||
? getAllocation(
|
||||
Number(accountInfo.balances[0].amount),
|
||||
Number(accountInfo.total_value.amount),
|
||||
)
|
||||
: 0;
|
||||
|
||||
const delegationsNYM = getNymsFormated(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
);
|
||||
const delegationsUSD = getPriceInUSD(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
nymPriceData,
|
||||
);
|
||||
const delegationsAllocation = getAllocation(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const operatorRewardsAllocation = getAllocation(
|
||||
Number(accountInfo.operator_rewards?.amount || 0),
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const operatorRewardsNYM = getNymsFormated(
|
||||
Number(accountInfo.operator_rewards?.amount || 0),
|
||||
);
|
||||
|
||||
const operatorRewardsUSD = getPriceInUSD(
|
||||
Number(accountInfo.operator_rewards?.amount || 0),
|
||||
nymPriceData,
|
||||
);
|
||||
|
||||
const claimableNYM = getNymsFormated(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
);
|
||||
const claimableUSD = getPriceInUSD(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
nymPriceData,
|
||||
);
|
||||
const claimableAllocation = getAllocation(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const stakingRewards =
|
||||
accountInfo.accumulated_rewards.length > 0
|
||||
? calculateStakingRewards(accountInfo.accumulated_rewards)
|
||||
: 0;
|
||||
|
||||
const tableRows = [
|
||||
{
|
||||
type: "Spendable",
|
||||
allocation: spendableAllocation,
|
||||
amount: spendableNYM,
|
||||
value: spendableUSD,
|
||||
},
|
||||
{
|
||||
type: "Delegated",
|
||||
allocation: delegationsAllocation,
|
||||
amount: delegationsNYM,
|
||||
value: delegationsUSD,
|
||||
// history: [
|
||||
// { type: "Liquid", amount: 6900 },
|
||||
// { type: "Locked", amount: 6900 },
|
||||
// ],
|
||||
},
|
||||
{
|
||||
type: "Claimable",
|
||||
allocation: claimableAllocation,
|
||||
amount: claimableNYM,
|
||||
value: claimableUSD,
|
||||
history: [
|
||||
// { type: "Unlocked", amount: 6900 },
|
||||
{
|
||||
type: "Staking rewards",
|
||||
amount: stakingRewards,
|
||||
},
|
||||
{ type: "Operator comission", amount: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "Operator Rewards",
|
||||
allocation: operatorRewardsAllocation,
|
||||
amount: operatorRewardsNYM,
|
||||
value: operatorRewardsUSD,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ExplorerCard
|
||||
label="Total value"
|
||||
title={`$ ${totalBalanceUSD}`}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<AccountBalancesTable rows={tableRows} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,321 @@
|
||||
"use client";
|
||||
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
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 Typography from "@mui/material/Typography";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import * as React from "react";
|
||||
import { TABLET_WIDTH } from "../../app/constants";
|
||||
import { MultiSegmentProgressBar } from "../progressBars/MultiSegmentProgressBar";
|
||||
import { StaticProgressBar } from "../progressBars/StaticProgressBar";
|
||||
|
||||
export interface IAccontStatsRowProps {
|
||||
type: string;
|
||||
allocation: number;
|
||||
amount: number;
|
||||
value: number;
|
||||
history?: { type: string; amount: number }[];
|
||||
isLastRow?: boolean;
|
||||
progressBarColor?: string;
|
||||
}
|
||||
|
||||
const progressBarColours = [
|
||||
"#BEF885",
|
||||
"#7FB0FF",
|
||||
"#00D17D",
|
||||
"#004650",
|
||||
"#FEECB3",
|
||||
];
|
||||
|
||||
const Row = (props: IAccontStatsRowProps) => {
|
||||
const tablet = useMediaQuery(TABLET_WIDTH);
|
||||
|
||||
const {
|
||||
type,
|
||||
allocation,
|
||||
amount,
|
||||
value,
|
||||
history,
|
||||
isLastRow,
|
||||
progressBarColor,
|
||||
} = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* Main Row */}
|
||||
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "25%",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
{type}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "25%",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
{allocation}%
|
||||
</Typography>
|
||||
<StaticProgressBar
|
||||
value={allocation}
|
||||
color={progressBarColor || "green"}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "20%",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
{amount.toFixed(4)} NYM
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "20%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ color: "pine.950", fontWeight: 700 }}
|
||||
>
|
||||
${value}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "10%",
|
||||
}}
|
||||
>
|
||||
{history && (
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
// MOBILE VIEW
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "45%",
|
||||
}}
|
||||
>
|
||||
<Box display={"flex"} gap={1} alignItems={"center"}>
|
||||
<CircleIcon sx={{ color: progressBarColor, fontSize: 12 }} />
|
||||
{type}
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "45%",
|
||||
}}
|
||||
>
|
||||
<Typography>{amount.toFixed(4)} NYM</Typography>
|
||||
<Typography>$ {value}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: isLastRow
|
||||
? "none"
|
||||
: "1px solid rgba(224, 224, 224, 1)",
|
||||
width: "10%",
|
||||
}}
|
||||
>
|
||||
{history && (
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{/* History Rows */}
|
||||
{history &&
|
||||
open &&
|
||||
history.map((historyRow) => (
|
||||
<TableRow key={historyRow.type}>
|
||||
<TableCell
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
pl: 5,
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
<span style={{ marginRight: 8 }}>•</span>
|
||||
{historyRow.type}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
{tablet && (
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
>
|
||||
<Typography variant="body4" sx={{ color: "pine.950" }}>
|
||||
{historyRow.amount.toFixed(4)} NYM
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
sx={{
|
||||
borderBottom: "none", // Explicitly remove border
|
||||
}}
|
||||
/>
|
||||
</TableRow>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IAccountBalancesTableProps {
|
||||
rows: Array<IAccontStatsRowProps>;
|
||||
}
|
||||
|
||||
export const AccountBalancesTable = (props: IAccountBalancesTableProps) => {
|
||||
const { rows } = props;
|
||||
const tablet = useMediaQuery(TABLET_WIDTH);
|
||||
const progressBarPercentages = () => {
|
||||
return rows.map((row) => row.allocation);
|
||||
};
|
||||
const getProgressValues = () => {
|
||||
const percentages = progressBarPercentages();
|
||||
const result: Array<{ percentage: number; color: string }> = [];
|
||||
percentages.map((value, i) => {
|
||||
result.push({
|
||||
percentage: value,
|
||||
color: progressBarColours[i],
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const progressValues = getProgressValues();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{!tablet && <MultiSegmentProgressBar values={progressValues} />}
|
||||
<TableContainer>
|
||||
<Table aria-label="collapsible table" sx={{ marginBottom: 3 }}>
|
||||
<TableHead>
|
||||
{tablet ? (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Type
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Allocation
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Amount
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Type
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="subtitle2" sx={{ color: "pine.600" }}>
|
||||
Amount / Value
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => (
|
||||
<Row
|
||||
key={row.type}
|
||||
{...row}
|
||||
isLastRow={i === rows.length - 1}
|
||||
progressBarColor={progressBarColours[i]}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
import { fetchAccountBalance } from "@/app/api";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
import { CardQRCode } from "../qrCode/QrCode";
|
||||
|
||||
interface IAccountInfoCardProps {
|
||||
address: string;
|
||||
}
|
||||
|
||||
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
const { address } = props;
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["accountBalance", address],
|
||||
queryFn: () => fetchAccountBalance(address),
|
||||
enabled: !!address,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Total NYM">
|
||||
<Stack gap={1}>
|
||||
<Skeleton variant="text" height={38} />
|
||||
<Skeleton variant="rectangular" height={128} width={128} />
|
||||
<Skeleton variant="text" height={300} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="Total NYM">
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to account data.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const balance =
|
||||
data.balances.length > 0 ? Number(data.total_value.amount) / 1000000 : 0;
|
||||
const balanceFormated = `${balance.toFixed(4)} NYM`;
|
||||
|
||||
return (
|
||||
<ExplorerCard
|
||||
label="Total NYM"
|
||||
title={balanceFormated}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
<Stack gap={5}>
|
||||
<Box display={"flex"} justifyContent={"flex-start"}>
|
||||
<CardQRCode url={data.address} />
|
||||
</Box>
|
||||
|
||||
<ExplorerListItem
|
||||
label="Address"
|
||||
value={
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={0.1}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Typography
|
||||
variant="body4"
|
||||
sx={{ wordWrap: "break-word", maxWidth: "85%" }}
|
||||
>
|
||||
{data.address}
|
||||
</Typography>
|
||||
<CopyToClipboard text={data.address} />
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import ExplorerHeroCard from "../cards/ExplorerHeroCard";
|
||||
import type { BlogArticleWithLink } from "./types";
|
||||
import { icons, IconName } from "@/utils/getIconByName";
|
||||
|
||||
// TODO: Articles should be sorted by date
|
||||
|
||||
const BlogArticlesCards = async ({
|
||||
limit,
|
||||
ids,
|
||||
}: {
|
||||
limit?: number;
|
||||
ids?: Array<number>;
|
||||
}) => {
|
||||
const blogsDir = path.join(process.cwd(), "/src/data");
|
||||
const blogsDirFilenames = await fs.readdir(blogsDir);
|
||||
|
||||
// Read all blog articles from the data directory
|
||||
const blogArticles: BlogArticleWithLink[] = await Promise.all(
|
||||
blogsDirFilenames.map(async (filename) => {
|
||||
const filePath = path.join(blogsDir, filename);
|
||||
const fileContent = await fs.readFile(filePath, "utf-8");
|
||||
const blogArticle = JSON.parse(fileContent);
|
||||
return {
|
||||
...blogArticle,
|
||||
link: `/onboarding/${filename.replace(".json", "")}`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const limitedOrFilteredBlogArticles = (
|
||||
blogArticles: BlogArticleWithLink[],
|
||||
limit?: number,
|
||||
ids?: number[],
|
||||
): BlogArticleWithLink[] => {
|
||||
let filteredArticles = blogArticles;
|
||||
|
||||
// Filter by IDs if provided
|
||||
if (ids && ids.length > 0) {
|
||||
filteredArticles = filteredArticles.filter((article) =>
|
||||
ids.includes(article.id),
|
||||
);
|
||||
}
|
||||
|
||||
// Apply limit if provided
|
||||
if (limit) {
|
||||
filteredArticles = filteredArticles.slice(0, limit);
|
||||
}
|
||||
|
||||
return filteredArticles;
|
||||
};
|
||||
const articles = limitedOrFilteredBlogArticles(blogArticles, limit, ids);
|
||||
|
||||
return articles
|
||||
.sort((a, b) => {
|
||||
// sort by date
|
||||
return (
|
||||
new Date(b.attributes.date).getTime() -
|
||||
new Date(a.attributes.date).getTime()
|
||||
);
|
||||
})
|
||||
.map((blogArticle) => {
|
||||
return (
|
||||
<Grid
|
||||
size={{
|
||||
sm: 12,
|
||||
md: 6,
|
||||
}}
|
||||
key={blogArticle.title}
|
||||
>
|
||||
<ExplorerHeroCard
|
||||
label={blogArticle.label}
|
||||
title={blogArticle.title}
|
||||
description={blogArticle.description}
|
||||
icon={icons[blogArticle.icon as IconName]?.src}
|
||||
link={blogArticle.link || ""}
|
||||
sx={{ height: "100%" }}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default BlogArticlesCards;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Card, CardContent, CardHeader, Typography } from "@mui/material";
|
||||
import { Link } from "../muiLink";
|
||||
|
||||
const TableOfContents = ({
|
||||
headings,
|
||||
}: {
|
||||
headings: { id: string; heading: string }[];
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: {
|
||||
xs: "none",
|
||||
md: "block",
|
||||
},
|
||||
p: 4,
|
||||
position: "sticky",
|
||||
top: 50,
|
||||
}}
|
||||
>
|
||||
<CardHeader title="Table of contents" />
|
||||
<CardContent>
|
||||
{headings.map((heading) => (
|
||||
<Link href={`#${heading.id}`} key={heading.id}>
|
||||
<Typography variant="body2" sx={{ mb: 3 }}>
|
||||
{heading.heading}
|
||||
</Typography>
|
||||
</Link>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableOfContents;
|
||||