Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37f0b02cee | |||
| 04f75e7e48 | |||
| 866dcd1e39 | |||
| a8526d698e | |||
| 3f5e0cdb1f | |||
| 96239a7812 | |||
| 762cfb8709 | |||
| 9835ad3396 | |||
| f73a3ac932 | |||
| 5af4d8d862 | |||
| 2c81195e79 | |||
| 4a9066fb6b | |||
| 86cc600ea3 | |||
| 459b109b5c | |||
| 08b6be93c4 | |||
| f0d3d41a1f | |||
| 9a42cab16d | |||
| 970db22702 | |||
| b9dcafa04f | |||
| 260a7de083 | |||
| 51ca727ff2 |
@@ -21,7 +21,7 @@ jobs:
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.0.0
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.0.0
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
||||
@@ -19,9 +19,7 @@ jobs:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [custom-ubuntu-22.04]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
runs-on: arc-ubuntu-22.04
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
|
||||
@@ -44,21 +44,24 @@ jobs:
|
||||
|
||||
- name: Download EV CodeSignTool from ssl.com
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: ${{ inputs.sign || startsWith(github.ref, 'refs/tags/nym-wallet-') }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
|
||||
unzip codesigntool.zip
|
||||
chmod +x ./CodeSignTool.sh
|
||||
|
||||
- name: Get EV certificate credential id
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: ${{ inputs.sign || startsWith(github.ref, 'refs/tags/nym-wallet-') }}
|
||||
id: get_credential_ids
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Add custom sign command to tauri.conf.json
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: ${{ inputs.sign || startsWith(github.ref, 'refs/tags/nym-wallet-') }}
|
||||
shell: bash
|
||||
run: |
|
||||
yq eval --inplace '.bundle.windows +=
|
||||
@@ -79,6 +82,7 @@ jobs:
|
||||
]
|
||||
}
|
||||
}' tauri.conf.json
|
||||
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
@@ -93,10 +97,10 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
|
||||
SSL_COM_USERNAME: ${{ (inputs.sign || startsWith(github.ref, 'refs/tags/nym-wallet-')) && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ (inputs.sign || startsWith(github.ref, 'refs/tags/nym-wallet-')) && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ (inputs.sign || startsWith(github.ref, 'refs/tags/nym-wallet-')) && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ (inputs.sign || startsWith(github.ref, 'refs/tags/nym-wallet-')) && secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
echo "Starting build process..."
|
||||
yarn build
|
||||
@@ -140,7 +144,7 @@ jobs:
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: github.event_name == 'release'
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release' }}
|
||||
with:
|
||||
files: |
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
|
||||
|
||||
@@ -4,6 +4,32 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.7-tex] (2025-04-14)
|
||||
|
||||
- Expand /v3/nym-nodes with geodata ([#5686])
|
||||
- chore: clippy for 1.86 ([#5685])
|
||||
- Featrure: Bash scripts to init and configure VMs conveniently and update docs ([#5681])
|
||||
- Update node versions in CI ([#5677])
|
||||
- build(deps): bump the patch-updates group across 1 directory with 8 updates ([#5668])
|
||||
- Update log crate ([#5667])
|
||||
- Minor fixes involving key cloning and hashing ([#5664])
|
||||
- mix throughput tester ([#5661])
|
||||
- build(deps): bump blake3 from 1.6.1 to 1.7.0 ([#5658])
|
||||
- build(deps): bump elliptic from 6.5.5 to 6.6.1 ([#5483])
|
||||
- Move all workflows on ubuntu-20 to ubuntu-22 ([#5455])
|
||||
|
||||
[#5686]: https://github.com/nymtech/nym/pull/5686
|
||||
[#5685]: https://github.com/nymtech/nym/pull/5685
|
||||
[#5681]: https://github.com/nymtech/nym/pull/5681
|
||||
[#5677]: https://github.com/nymtech/nym/pull/5677
|
||||
[#5668]: https://github.com/nymtech/nym/pull/5668
|
||||
[#5667]: https://github.com/nymtech/nym/pull/5667
|
||||
[#5664]: https://github.com/nymtech/nym/pull/5664
|
||||
[#5661]: https://github.com/nymtech/nym/pull/5661
|
||||
[#5658]: https://github.com/nymtech/nym/pull/5658
|
||||
[#5483]: https://github.com/nymtech/nym/pull/5483
|
||||
[#5455]: https://github.com/nymtech/nym/pull/5455
|
||||
|
||||
## [2025.6-chuckles] (2025-03-31)
|
||||
|
||||
- Remove Google public DNS ([#5660])
|
||||
|
||||
Generated
+100
-64
@@ -218,9 +218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.97"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
@@ -430,6 +430,17 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
@@ -489,6 +500,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
@@ -516,7 +533,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
name = "autodoc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"env_logger 0.11.7",
|
||||
"env_logger 0.11.8",
|
||||
"log",
|
||||
]
|
||||
|
||||
@@ -1194,9 +1211,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.34"
|
||||
version = "4.5.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
|
||||
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -1204,9 +1221,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.34"
|
||||
version = "4.5.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
|
||||
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1601,6 +1618,12 @@ dependencies = [
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
@@ -2382,9 +2405,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.7"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -2560,9 +2583,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -2964,6 +2987,25 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.3.1",
|
||||
"indexmap 2.7.1",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.4.1"
|
||||
@@ -3130,57 +3172,59 @@ checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.24.4"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
|
||||
checksum = "6d844af74f7b799e41c78221be863bade11c430d46042c3b49ca8ae0c6d27287"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"critical-section",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http 0.2.12",
|
||||
"h2 0.4.9",
|
||||
"http 1.3.1",
|
||||
"idna",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"rustls 0.21.12",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"thiserror 1.0.69",
|
||||
"rand 0.9.0",
|
||||
"ring",
|
||||
"rustls 0.23.25",
|
||||
"thiserror 2.0.12",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tracing",
|
||||
"url",
|
||||
"webpki-roots 0.25.4",
|
||||
"webpki-roots 0.26.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.24.4"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
|
||||
checksum = "a128410b38d6f931fcc6ca5c107a3b02cabd6c05967841269a4ad65d23c44331"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-proto",
|
||||
"ipconfig",
|
||||
"lru-cache",
|
||||
"moka",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.0",
|
||||
"resolv-conf",
|
||||
"rustls 0.21.12",
|
||||
"rustls 0.23.25",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tracing",
|
||||
"webpki-roots 0.25.4",
|
||||
"webpki-roots 0.26.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3372,7 +3416,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
@@ -3452,9 +3496,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.10"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
||||
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -3462,6 +3506,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.6.0",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@@ -4053,9 +4098,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.169"
|
||||
version = "0.2.171"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -4097,12 +4142,6 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -4168,15 +4207,6 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -4331,9 +4361,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.4"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
|
||||
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
@@ -5126,6 +5156,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"tungstenite 0.20.1",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@@ -6579,7 +6610,6 @@ dependencies = [
|
||||
name = "nym-sphinx"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"nym-crypto",
|
||||
"nym-metrics",
|
||||
"nym-mixnet-contract-common",
|
||||
@@ -6599,6 +6629,7 @@ dependencies = [
|
||||
"rand_distr",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7183,6 +7214,10 @@ name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -8163,7 +8198,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
@@ -8521,6 +8556,7 @@ version = "0.23.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -9221,9 +9257,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -9745,9 +9781,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint"
|
||||
version = "0.40.1"
|
||||
version = "0.40.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9703e34d940c2a293804752555107f8dbe2b84ec4c6dd5203831235868105d2"
|
||||
checksum = "ab2972a56891bc9173eaebdd51290bc848e448aa281881f3a4712bd8fe1899b3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"digest 0.10.7",
|
||||
@@ -9775,9 +9811,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-config"
|
||||
version = "0.40.1"
|
||||
version = "0.40.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89cc3ea9a39b7ee34eefcff771cc067ecaa0c988c1c5ac08defd878471a06f76"
|
||||
checksum = "651cb680d41e586bb688cc17ab0b6dfe2e62a959c5742d1351fe0b084cd75d59"
|
||||
dependencies = [
|
||||
"flex-error",
|
||||
"serde",
|
||||
@@ -9789,9 +9825,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-proto"
|
||||
version = "0.40.1"
|
||||
version = "0.40.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9e1705aa0fa5ecb2c6aa7fb78c2313c4a31158ea5f02048bf318f849352eb"
|
||||
checksum = "ca44b9eaaa98e8440fbafe5593b8a32a1c395c094ac566b3eb50497f8bd2bee0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"flex-error",
|
||||
@@ -9804,9 +9840,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-rpc"
|
||||
version = "0.40.1"
|
||||
version = "0.40.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "835a52aa504c63ec05519e31348d3f4ba2fe79493c588e2cad5323d5e81b161a"
|
||||
checksum = "9367678af1a9fc6c064fab8b5d05f03c0fc243fe411581c00c7eed83f8ced380"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"async-tungstenite",
|
||||
@@ -10277,7 +10313,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
|
||||
+7
-7
@@ -185,7 +185,7 @@ aes = "0.8.1"
|
||||
aes-gcm = "0.10.1"
|
||||
aes-gcm-siv = "0.11.1"
|
||||
ammonia = "4"
|
||||
anyhow = "1.0.97"
|
||||
anyhow = "1.0.98"
|
||||
arc-swap = "1.7.1"
|
||||
argon2 = "0.5.0"
|
||||
async-trait = "0.1.88"
|
||||
@@ -211,7 +211,7 @@ chacha20 = "0.9.0"
|
||||
chacha20poly1305 = "0.10.1"
|
||||
chrono = "0.4.40"
|
||||
cipher = "0.4.3"
|
||||
clap = "4.5.34"
|
||||
clap = "4.5.36"
|
||||
clap_complete = "4.5"
|
||||
clap_complete_fig = "4.5"
|
||||
colored = "2.2"
|
||||
@@ -236,12 +236,12 @@ dotenvy = "0.15.6"
|
||||
ecdsa = "0.16"
|
||||
ed25519-dalek = "2.1"
|
||||
encoding_rs = "0.8.35"
|
||||
env_logger = "0.11.7"
|
||||
env_logger = "0.11.8"
|
||||
envy = "0.4"
|
||||
etherparse = "0.13.0"
|
||||
eyre = "0.6.9"
|
||||
fastrand = "2.1.1"
|
||||
flate2 = "1.1.0"
|
||||
flate2 = "1.1.1"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3"
|
||||
generic-array = "0.14.7"
|
||||
@@ -251,7 +251,7 @@ handlebars = "3.5.5"
|
||||
headers = "0.4.0"
|
||||
hex = "0.4.3"
|
||||
hex-literal = "0.3.3"
|
||||
hickory-resolver = "0.24.4"
|
||||
hickory-resolver = "0.25"
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = "1"
|
||||
@@ -385,8 +385,8 @@ bip32 = { version = "0.5.3", default-features = false }
|
||||
|
||||
|
||||
cosmrs = { version = "0.21.1" }
|
||||
tendermint = "0.40.0"
|
||||
tendermint-rpc = "0.40.0"
|
||||
tendermint = "0.40.3"
|
||||
tendermint-rpc = "0.40.3"
|
||||
prost = { version = "0.13", default-features = false }
|
||||
|
||||
# wasm-related dependencies
|
||||
|
||||
@@ -27,6 +27,7 @@ thiserror = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
time = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
# internal
|
||||
|
||||
@@ -414,6 +414,12 @@ pub struct Traffic {
|
||||
pub use_legacy_sphinx_format: bool,
|
||||
|
||||
pub packet_type: PacketType,
|
||||
|
||||
/// Indicates whether to mix hops or not. If mix hops are enabled, traffic
|
||||
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing
|
||||
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly
|
||||
/// from the entry gateway to the exit gateway, bypassing the mix nodes.
|
||||
pub disable_mix_hops: bool,
|
||||
}
|
||||
|
||||
impl Traffic {
|
||||
@@ -444,6 +450,7 @@ impl Default for Traffic {
|
||||
// we should use the legacy format until sufficient number of nodes understand the
|
||||
// improved encoding
|
||||
use_legacy_sphinx_format: true,
|
||||
disable_mix_hops: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::client::real_messages_control::{AckActionSender, Action};
|
||||
use crate::client::replies::reply_controller::MaxRetransmissions;
|
||||
use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags};
|
||||
use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
|
||||
@@ -27,6 +26,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
// TODO: move that error elsewhere since it seems to be contaminating different files
|
||||
#[derive(Debug, Error)]
|
||||
@@ -98,6 +98,12 @@ pub(crate) struct Config {
|
||||
/// Specify whether route selection should be determined by the packet header.
|
||||
deterministic_route_selection: bool,
|
||||
|
||||
/// Indicates whether to mix hops or not. If mix hops are enabled, traffic
|
||||
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing
|
||||
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly
|
||||
/// from the entry gateway to the exit gateway, bypassing the mix nodes.
|
||||
disable_mix_hops: bool,
|
||||
|
||||
/// Average delay a data packet is going to get delay at a single mixnode.
|
||||
average_packet_delay: Duration,
|
||||
|
||||
@@ -133,6 +139,7 @@ impl Config {
|
||||
primary_packet_size: PacketSize::default(),
|
||||
secondary_packet_size: None,
|
||||
use_legacy_sphinx_format: use_legacy_reply_surb_format,
|
||||
disable_mix_hops: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +154,12 @@ impl Config {
|
||||
self.secondary_packet_size = packet_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure whether messages senders using this config should use mix hops or not when sending messages.
|
||||
pub fn disable_mix_hops(mut self, disable_mix_hops: bool) -> Self {
|
||||
self.disable_mix_hops = disable_mix_hops;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -193,6 +206,7 @@ where
|
||||
config.average_packet_delay,
|
||||
config.average_ack_delay,
|
||||
config.use_legacy_sphinx_format,
|
||||
config.disable_mix_hops,
|
||||
);
|
||||
MessageHandler {
|
||||
config,
|
||||
@@ -284,7 +298,7 @@ where
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
let msg = NymMessage::new_reply(message);
|
||||
let packet_size = self.optimal_packet_size(&msg);
|
||||
debug!("Using {packet_size} packets for {msg}");
|
||||
trace!("Using {packet_size} packets for {msg}");
|
||||
|
||||
let mut fragment = self
|
||||
.message_preparer
|
||||
@@ -348,7 +362,7 @@ where
|
||||
pub(crate) fn split_reply_message(&mut self, message: Vec<u8>) -> Vec<Fragment> {
|
||||
let msg = NymMessage::new_reply(ReplyMessage::new_data_message(message));
|
||||
let packet_size = self.optimal_packet_size(&msg);
|
||||
debug!("Using {packet_size} packets for {msg}");
|
||||
trace!("Using {packet_size} packets for {msg}");
|
||||
|
||||
self.message_preparer
|
||||
.pad_and_split_message(msg, packet_size)
|
||||
@@ -481,7 +495,7 @@ where
|
||||
} else {
|
||||
self.optimal_packet_size(&message)
|
||||
};
|
||||
debug!("Using {packet_size} packets for {message}");
|
||||
trace!("Using {packet_size} packets for {message}");
|
||||
let fragments = self
|
||||
.message_preparer
|
||||
.pad_and_split_message(message, packet_size);
|
||||
|
||||
@@ -103,6 +103,7 @@ impl<'a> From<&'a Config> for message_handler::Config {
|
||||
)
|
||||
.with_custom_primary_packet_size(cfg.traffic.primary_packet_size)
|
||||
.with_custom_secondary_packet_size(cfg.traffic.secondary_packet_size)
|
||||
.disable_mix_hops(cfg.traffic.disable_mix_hops)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ mime = { workspace = true }
|
||||
nym-bin-common = { path = "../bin-common" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
||||
hickory-resolver = { workspace = true, features = ["dns-over-https-rustls", "webpki-roots"] }
|
||||
hickory-resolver = { workspace = true, features = ["https-ring", "tls-ring", "webpki-roots"] }
|
||||
|
||||
# for request timeout until https://github.com/seanmonstar/reqwest/issues/1135 is fixed
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
|
||||
|
||||
@@ -35,10 +35,10 @@ use std::{
|
||||
};
|
||||
|
||||
use hickory_resolver::{
|
||||
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ResolverOpts},
|
||||
error::{ResolveError, ResolveErrorKind},
|
||||
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ServerOrderingStrategy},
|
||||
lookup_ip::{LookupIp, LookupIpIntoIter},
|
||||
TokioAsyncResolver,
|
||||
name_server::TokioConnectionProvider,
|
||||
ResolveError, TokioResolver,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
@@ -92,8 +92,8 @@ pub struct HickoryDnsResolver {
|
||||
// Since we might not have been called in the context of a
|
||||
// Tokio Runtime in initialization, so we must delay the actual
|
||||
// construction of the resolver.
|
||||
state: Arc<OnceCell<TokioAsyncResolver>>,
|
||||
fallback: Arc<OnceCell<TokioAsyncResolver>>,
|
||||
state: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Arc<OnceCell<TokioResolver>>,
|
||||
dont_use_shared: bool,
|
||||
}
|
||||
|
||||
@@ -118,11 +118,8 @@ impl Resolve for HickoryDnsResolver {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
match e.kind() {
|
||||
ResolveErrorKind::NoRecordsFound { .. } => {}
|
||||
_ => {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
let resolver = fallback.get_or_try_init(|| {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
@@ -166,11 +163,8 @@ impl HickoryDnsResolver {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
match e.kind() {
|
||||
ResolveErrorKind::NoRecordsFound { .. } => {}
|
||||
_ => {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
let resolver = self
|
||||
.fallback
|
||||
@@ -190,7 +184,7 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver(&self) -> Result<TokioAsyncResolver, HickoryDnsError> {
|
||||
fn new_resolver(&self) -> Result<TokioResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared {
|
||||
new_resolver()
|
||||
} else {
|
||||
@@ -198,7 +192,7 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver_system(&self) -> Result<TokioAsyncResolver, HickoryDnsError> {
|
||||
fn new_resolver_system(&self) -> Result<TokioResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
@@ -212,29 +206,30 @@ impl HickoryDnsResolver {
|
||||
|
||||
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
|
||||
/// up for both IPv4 and IPv6 addresses to work with "happy eyeballs" algorithm.
|
||||
fn new_resolver() -> Result<TokioAsyncResolver, HickoryDnsError> {
|
||||
fn new_resolver() -> Result<TokioResolver, HickoryDnsError> {
|
||||
let mut name_servers = NameServerConfigGroup::quad9_tls();
|
||||
name_servers.merge(NameServerConfigGroup::quad9_https());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_https());
|
||||
|
||||
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
|
||||
let mut resolver_builder =
|
||||
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
|
||||
|
||||
let mut opts = ResolverOpts::default();
|
||||
opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
// Would like to enable this when 0.25 stabilizes
|
||||
// opts.server_ordering_strategy = ServerOrderingStrategy::RoundRobin;
|
||||
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
resolver_builder.options_mut().server_ordering_strategy = ServerOrderingStrategy::RoundRobin;
|
||||
|
||||
Ok(TokioAsyncResolver::tokio(config, opts))
|
||||
Ok(resolver_builder.build())
|
||||
}
|
||||
|
||||
/// Create a new resolver with the default configuration, which reads from the system DNS config
|
||||
/// (i.e. `/etc/resolve.conf` in unix). The options are overridden to look up for both IPv4 and IPv6
|
||||
/// addresses to work with "happy eyeballs" algorithm.
|
||||
fn new_resolver_system() -> Result<TokioAsyncResolver, HickoryDnsError> {
|
||||
let (config, mut opts) = hickory_resolver::system_conf::read_system_conf()?;
|
||||
opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
Ok(TokioAsyncResolver::tokio(config, opts))
|
||||
fn new_resolver_system() -> Result<TokioResolver, HickoryDnsError> {
|
||||
let mut resolver_builder = TokioResolver::builder_tokio()?;
|
||||
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -8,7 +8,7 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_distr = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
|
||||
@@ -47,11 +47,17 @@ impl SurbAck {
|
||||
average_delay: time::Duration,
|
||||
topology: &NymRouteProvider,
|
||||
packet_type: PacketType,
|
||||
disable_mix_hops: bool,
|
||||
) -> Result<Self, NymTopologyError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let route = topology.random_route_to_egress(rng, recipient.gateway())?;
|
||||
let route = if disable_mix_hops {
|
||||
topology.empty_route_to_egress(recipient.gateway())?
|
||||
} else {
|
||||
topology.random_route_to_egress(rng, recipient.gateway())?
|
||||
};
|
||||
|
||||
let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len());
|
||||
let destination = recipient.as_sphinx_destination();
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ impl ReplySurb {
|
||||
average_delay: Duration,
|
||||
use_legacy_surb_format: bool,
|
||||
topology: &NymRouteProvider,
|
||||
_disable_mix_hops: bool, // TODO: support SURBs with no mix hops after changes to surb format / construction
|
||||
) -> Result<Self, NymTopologyError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
|
||||
@@ -272,6 +272,10 @@ impl Fragment {
|
||||
self.payload
|
||||
}
|
||||
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
/// Tries to recover `Fragment` from slice of bytes extracted from received sphinx packet.
|
||||
/// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata
|
||||
/// is malformed or self-contradictory, for example if current_fragment > total_fragments.
|
||||
|
||||
@@ -53,6 +53,7 @@ where
|
||||
average_ack_delay,
|
||||
topology,
|
||||
packet_type,
|
||||
false, // make sure mix hops are enabled
|
||||
)?)
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ impl NymMessage {
|
||||
chunking::number_of_required_fragments(serialized_len, plaintext_per_packet);
|
||||
|
||||
// by chunking I mean that currently the fragments hold variable amount of plaintext in them (I wish I had time to rewrite it...)
|
||||
log::trace!(
|
||||
tracing::trace!(
|
||||
"this message will use {serialized_len} bytes of PLAINTEXT (This does not account for Ack or chunking overhead). \
|
||||
With {packet_size:?} PacketSize ({plaintext_per_packet} of usable plaintext available) it will require {num_fragments} packet(s).",
|
||||
);
|
||||
@@ -242,7 +242,7 @@ impl NymMessage {
|
||||
|
||||
let wasted_space_percentage =
|
||||
(space_left as f32 / (bytes.len() + 1 + space_left) as f32) * 100.0;
|
||||
log::trace!(
|
||||
tracing::trace!(
|
||||
"Padding {self_display}: {} of raw plaintext bytes are required. \
|
||||
They're going to be put into {packets_used} sphinx packets with {space_left} bytes \
|
||||
of leftover space. {wasted_space_percentage:.1}% of packet capacity is going to \
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use crate::message::{NymMessage, ACK_OVERHEAD, OUTFOX_ACK_OVERHEAD};
|
||||
use crate::NymPayloadBuilder;
|
||||
use log::debug;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_crypto::Digest;
|
||||
use nym_sphinx_acknowledgements::surb_ack::SurbAck;
|
||||
@@ -19,6 +18,7 @@ use nym_sphinx_types::{Delay, NymPacket};
|
||||
use nym_topology::{NymRouteProvider, NymTopologyError};
|
||||
use rand::{CryptoRng, Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use tracing::*;
|
||||
|
||||
use nym_sphinx_chunking::monitoring;
|
||||
use std::time::Duration;
|
||||
@@ -52,6 +52,10 @@ pub trait FragmentPreparer {
|
||||
type Rng: CryptoRng + Rng;
|
||||
|
||||
fn use_legacy_sphinx_format(&self) -> bool;
|
||||
fn mix_hops_disabled(&self) -> bool {
|
||||
// Unless otherwise configured, mix hops are enabled
|
||||
false
|
||||
}
|
||||
|
||||
fn deterministic_route_selection(&self) -> bool;
|
||||
fn rng(&mut self) -> &mut Self::Rng;
|
||||
@@ -69,6 +73,7 @@ pub trait FragmentPreparer {
|
||||
) -> Result<SurbAck, NymTopologyError> {
|
||||
let ack_delay = self.average_ack_delay();
|
||||
let use_legacy_sphinx_format = self.use_legacy_sphinx_format();
|
||||
let disable_mix_hops = self.mix_hops_disabled();
|
||||
|
||||
SurbAck::construct(
|
||||
self.rng(),
|
||||
@@ -79,6 +84,7 @@ pub trait FragmentPreparer {
|
||||
ack_delay,
|
||||
topology,
|
||||
packet_type,
|
||||
disable_mix_hops,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -101,6 +107,8 @@ pub trait FragmentPreparer {
|
||||
packet_sender: &Recipient,
|
||||
packet_type: PacketType,
|
||||
) -> Result<PreparedFragment, NymTopologyError> {
|
||||
debug!("Preparing reply chunk for sending");
|
||||
|
||||
// each reply attaches the digest of the encryption key so that the recipient could
|
||||
// lookup correct key for decryption,
|
||||
let reply_overhead = ReplySurbKeyDigestAlgorithm::output_size();
|
||||
@@ -222,15 +230,17 @@ pub trait FragmentPreparer {
|
||||
Err(_e) => return Err(NymTopologyError::PayloadBuilder),
|
||||
};
|
||||
|
||||
// generate pseudorandom route for the packet
|
||||
log::trace!("Preparing chunk for sending");
|
||||
let route = if self.deterministic_route_selection() {
|
||||
log::trace!("using deterministic route selection");
|
||||
// generate pseudorandom route for the packet. Unless mix hops are disabled then build an empty route.
|
||||
trace!("Preparing chunk for sending");
|
||||
let route = if self.mix_hops_disabled() {
|
||||
topology.empty_route_to_egress(destination)?
|
||||
} else if self.deterministic_route_selection() {
|
||||
trace!("using deterministic route selection");
|
||||
let seed = fragment_header.seed().wrapping_mul(self.nonce());
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(seed as u64);
|
||||
topology.random_route_to_egress(&mut rng, destination)?
|
||||
} else {
|
||||
log::trace!("using pseudorandom route selection");
|
||||
trace!("using pseudorandom route selection");
|
||||
let mut rng = self.rng();
|
||||
topology.random_route_to_egress(&mut rng, destination)?
|
||||
};
|
||||
@@ -316,6 +326,12 @@ pub struct MessagePreparer<R> {
|
||||
use_legacy_sphinx_format: bool,
|
||||
|
||||
nonce: i32,
|
||||
|
||||
/// Indicates whether to mix hops or not. If mix hops are enabled, traffic
|
||||
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing
|
||||
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly
|
||||
/// from the entry gateway to the exit gateway, bypassing the mix nodes.
|
||||
pub disable_mix_hops: bool,
|
||||
}
|
||||
|
||||
impl<R> MessagePreparer<R>
|
||||
@@ -329,6 +345,7 @@ where
|
||||
average_packet_delay: Duration,
|
||||
average_ack_delay: Duration,
|
||||
use_legacy_sphinx_format: bool,
|
||||
disable_mix_hops: bool,
|
||||
) -> Self {
|
||||
let mut rng = rng;
|
||||
let nonce = rng.gen();
|
||||
@@ -340,6 +357,7 @@ where
|
||||
average_ack_delay,
|
||||
use_legacy_sphinx_format,
|
||||
nonce,
|
||||
disable_mix_hops,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +373,8 @@ where
|
||||
topology: &NymRouteProvider,
|
||||
) -> Result<Vec<ReplySurb>, NymTopologyError> {
|
||||
let mut reply_surbs = Vec::with_capacity(amount);
|
||||
let disabled_mix_hops = self.mix_hops_disabled();
|
||||
|
||||
for _ in 0..amount {
|
||||
let reply_surb = ReplySurb::construct(
|
||||
&mut self.rng,
|
||||
@@ -362,6 +382,7 @@ where
|
||||
self.average_packet_delay,
|
||||
use_legacy_reply_surb_format,
|
||||
topology,
|
||||
disabled_mix_hops, // TODO: support SURBs with no mix hops after changes to surb format / construction
|
||||
)?;
|
||||
reply_surbs.push(reply_surb)
|
||||
}
|
||||
@@ -442,6 +463,10 @@ where
|
||||
impl<R: CryptoRng + Rng> FragmentPreparer for MessagePreparer<R> {
|
||||
type Rng = R;
|
||||
|
||||
fn mix_hops_disabled(&self) -> bool {
|
||||
self.disable_mix_hops
|
||||
}
|
||||
|
||||
fn use_legacy_sphinx_format(&self) -> bool {
|
||||
self.use_legacy_sphinx_format
|
||||
}
|
||||
|
||||
@@ -176,6 +176,17 @@ impl NymRouteProvider {
|
||||
.random_route_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles)
|
||||
}
|
||||
|
||||
/// Returns a route directly to the egress point, which can be any known node
|
||||
pub fn empty_route_to_egress(
|
||||
&self,
|
||||
egress_identity: NodeIdentity,
|
||||
) -> Result<Vec<SphinxNode>, NymTopologyError> {
|
||||
let egress = self
|
||||
.topology
|
||||
.egress_node_by_identity(egress_identity, self.ignore_egress_epoch_roles)?;
|
||||
Ok(vec![egress])
|
||||
}
|
||||
|
||||
pub fn random_path_to_egress<R>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
|
||||
@@ -191,6 +191,12 @@ pub struct TrafficWasm {
|
||||
|
||||
/// Controls whether the sent packets should use outfox as opposed to the default sphinx.
|
||||
pub use_outfox: bool,
|
||||
|
||||
/// Indicates whether to mix hops or not. If mix hops are enabled, traffic
|
||||
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing
|
||||
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly
|
||||
/// from the entry gateway to the exit gateway, bypassing the mix nodes.
|
||||
pub disable_mix_hops: bool,
|
||||
}
|
||||
|
||||
impl Default for TrafficWasm {
|
||||
@@ -224,6 +230,7 @@ impl From<TrafficWasm> for ConfigTraffic {
|
||||
secondary_packet_size: use_extended_packet_size,
|
||||
use_legacy_sphinx_format: traffic.use_legacy_sphinx_format,
|
||||
packet_type,
|
||||
disable_mix_hops: traffic.disable_mix_hops,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,6 +248,7 @@ impl From<ConfigTraffic> for TrafficWasm {
|
||||
use_legacy_sphinx_format: traffic.use_legacy_sphinx_format,
|
||||
use_extended_packet_size: traffic.secondary_packet_size.is_some(),
|
||||
use_outfox: traffic.packet_type == PacketType::Outfox,
|
||||
disable_mix_hops: traffic.disable_mix_hops,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@ impl From<TrafficWasmOverride> for TrafficWasm {
|
||||
.use_extended_packet_size
|
||||
.unwrap_or(def.use_extended_packet_size),
|
||||
use_outfox: value.use_outfox.unwrap_or(def.use_outfox),
|
||||
disable_mix_hops: false, // not configured from js config override yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,10 +112,10 @@ impl PeerController {
|
||||
request_tx.clone(),
|
||||
&task_client,
|
||||
);
|
||||
let public_key = public_key.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle.run().await {
|
||||
log::error!("Peer handle shut down ungracefully - {e}");
|
||||
}
|
||||
handle.run().await;
|
||||
log::debug!("Peer handle shut down for {public_key}");
|
||||
});
|
||||
}
|
||||
let bw_storage_managers = bw_storage_managers
|
||||
@@ -223,10 +223,10 @@ impl PeerController {
|
||||
if let Ok(host_information) = self.wg_api.inner.read_interface_data() {
|
||||
*self.host_information.write().await = host_information;
|
||||
}
|
||||
let public_key = peer.public_key.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle.run().await {
|
||||
log::error!("Peer handle shut down ungracefully - {e}");
|
||||
}
|
||||
handle.run().await;
|
||||
log::debug!("Peer handle shut down for {public_key}");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use tokio::sync::{mpsc, RwLock};
|
||||
use tokio_stream::{wrappers::IntervalStream, StreamExt};
|
||||
|
||||
pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManager>>;
|
||||
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 30 days
|
||||
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60); // 1 hour
|
||||
|
||||
pub struct PeerHandle {
|
||||
public_key: Key,
|
||||
@@ -79,9 +79,30 @@ impl PeerHandle {
|
||||
kernel_peer: &Peer,
|
||||
) -> Result<bool, Error> {
|
||||
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
|
||||
if kernel_peer.last_handshake.is_none()
|
||||
&& SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER
|
||||
{
|
||||
let success = self.remove_peer().await?;
|
||||
self.peer_storage_manager.remove_peer();
|
||||
tracing::debug!(
|
||||
"Peer {} has not been active for more then {} seconds, removing it",
|
||||
kernel_peer.public_key.to_string(),
|
||||
AUTO_REMOVE_AFTER.as_secs()
|
||||
);
|
||||
return Ok(!success);
|
||||
}
|
||||
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
|
||||
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
|
||||
.ok_or(Error::InconsistentConsumedBytes)?
|
||||
.unwrap_or_else(|| {
|
||||
// if gateway restarted, the kernel values restart from 0
|
||||
// and we should restart from 0 in storage as well
|
||||
if let Some(peer_information) =
|
||||
self.peer_storage_manager.peer_information.as_mut()
|
||||
{
|
||||
peer_information.force_sync = true;
|
||||
}
|
||||
kernel_peer.rx_bytes + kernel_peer.tx_bytes
|
||||
})
|
||||
.try_into()
|
||||
.map_err(|_| Error::InconsistentConsumedBytes)?;
|
||||
if spent_bandwidth > 0 {
|
||||
@@ -93,6 +114,10 @@ impl PeerHandle {
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tracing::debug!(
|
||||
"Peer {} is out of bandwidth, removing it",
|
||||
kernel_peer.public_key.to_string()
|
||||
);
|
||||
let success = self.remove_peer().await?;
|
||||
self.peer_storage_manager.remove_peer();
|
||||
return Ok(!success);
|
||||
@@ -121,30 +146,57 @@ impl PeerHandle {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), Error> {
|
||||
async fn continue_checking(&mut self) -> Result<bool, Error> {
|
||||
let Some(kernel_peer) = self
|
||||
.host_information
|
||||
.read()
|
||||
.await
|
||||
.peers
|
||||
.get(&self.public_key)
|
||||
.cloned()
|
||||
else {
|
||||
// the host information hasn't beed updated yet
|
||||
return Ok(true);
|
||||
};
|
||||
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
|
||||
log::debug!(
|
||||
"Peer {:?} not in storage anymore, shutting down handle",
|
||||
self.public_key
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
if !self.active_peer(&storage_peer, &kernel_peer).await? {
|
||||
log::debug!(
|
||||
"Peer {:?} is not active anymore, shutting down handle",
|
||||
self.public_key
|
||||
);
|
||||
Ok(false)
|
||||
} else {
|
||||
// Update storage values
|
||||
self.peer_storage_manager.sync_storage_peer().await?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = self.timeout_check_interval.next() => {
|
||||
let Some(kernel_peer) = self
|
||||
.host_information
|
||||
.read()
|
||||
.await
|
||||
.peers
|
||||
.get(&self.public_key)
|
||||
.cloned() else {
|
||||
// the host information hasn't beed updated yet
|
||||
continue;
|
||||
};
|
||||
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
|
||||
log::debug!("Peer {:?} not in storage anymore, shutting down handle", self.public_key);
|
||||
return Ok(());
|
||||
};
|
||||
if !self.active_peer(&storage_peer, &kernel_peer).await? {
|
||||
log::debug!("Peer {:?} doesn't have bandwidth anymore, shutting down handle", self.public_key);
|
||||
return Ok(());
|
||||
} else {
|
||||
// Update storage values
|
||||
self.peer_storage_manager.sync_storage_peer().await?;
|
||||
match self.continue_checking().await {
|
||||
Ok(true) => continue,
|
||||
Ok(false) => return,
|
||||
Err(err) => {
|
||||
match self.remove_peer().await {
|
||||
Ok(true) => {
|
||||
tracing::debug!("Removed peer due to error {err}");
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Could not remove peer yet, we'll try again later. If this message persists, the gateway might need to be restarted");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +211,5 @@ impl PeerHandle {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ pub(crate) struct PeerInformation {
|
||||
pub(crate) last_synced: OffsetDateTime,
|
||||
|
||||
pub(crate) bytes_delta_since_sync: u64,
|
||||
pub(crate) force_sync: bool,
|
||||
}
|
||||
|
||||
impl PeerInformation {
|
||||
@@ -103,10 +104,14 @@ impl PeerInformation {
|
||||
peer,
|
||||
last_synced: OffsetDateTime::now_utc(),
|
||||
bytes_delta_since_sync: 0,
|
||||
force_sync: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_sync(&self, cfg: PeerFlushingBehaviourConfig) -> bool {
|
||||
if self.force_sync {
|
||||
return true;
|
||||
}
|
||||
if self.bytes_delta_since_sync >= cfg.peer_max_delta_flushing_amount {
|
||||
return true;
|
||||
}
|
||||
@@ -134,5 +139,6 @@ impl PeerInformation {
|
||||
pub(crate) fn resync_peer_with_storage(&mut self) {
|
||||
self.bytes_delta_since_sync = 0;
|
||||
self.last_synced = OffsetDateTime::now_utc();
|
||||
self.force_sync = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,15 @@ Our `prebuild` script relies on the following:
|
||||
|
||||
Otherwise make sure to have `node` installed.
|
||||
|
||||
#### Binary dependencies
|
||||
If you don't want/need build our binaries in Rust localy you can download them from latest Release page on [github](https://github.com/nymtech/nym/releases) into target/release in root of this repo.
|
||||
{'nym-node', 'nym-api', 'nymvisor'}
|
||||
|
||||
Make them executable
|
||||
```bash
|
||||
chmod +x target/release/*
|
||||
```
|
||||
|
||||
### Serve Local (Hot Reload)
|
||||
```sh
|
||||
pnpm i
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
808_623_916
|
||||
809_964_694
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1_028_488
|
||||
1_031_281
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
404_311_958
|
||||
404_982_347
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
| **Item** | **Description** | **Amount in NYM** |
|
||||
|:-------------------|:------------------------------------------------------|--------------------:|
|
||||
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 191_376_083 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 190_035_305 |
|
||||
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 808_623_916 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 1_028_488 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 809_964_694 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 1_031_281 |
|
||||
|
||||
@@ -1 +1 @@
|
||||
Thursday, April 3rd 2025, 13:40:32 UTC
|
||||
Wednesday, April 16th 2025, 12:32:08 UTC
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { LandingPage } from '../components/landing-page.tsx'
|
||||
|
||||
<LandingPage />
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Steps } from 'nextra/components';
|
||||
import { VarInfo } from 'components/variable-info.tsx';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { MyTab } from 'components/generic-tabs.tsx';
|
||||
import {Callout} from 'nextra/components';
|
||||
|
||||
# Pre-built Binaries
|
||||
|
||||
@@ -9,6 +10,10 @@ This page is for operators who prefer to download ready made binaries. The [Gith
|
||||
|
||||
If the pre-built binaries don't work or are unavailable for your system, you will need to build the platform yourself.
|
||||
|
||||
<Callout type="info" emoji="ℹ️">
|
||||
**[Nym release binaries](https://github.com/nymtech/nym/releases) no longer work on distributions based on Debian bullseye/sid (11) like Ubuntu 20.04 LTS and older! Please upgrade your sever to Debian bookworm (Debian 12) or Ubuntu 22.04 (and newer)!** Alternatively [compile the binaries from source](building-nym.mdx).
|
||||
</Callout>
|
||||
|
||||
## Setup Binaries
|
||||
|
||||
<VarInfo />
|
||||
@@ -35,12 +40,12 @@ In case you want to download binary to your current working directory, drop `<PA
|
||||
<strong>One-liner</strong>,
|
||||
]} defaultIndex="0">
|
||||
<MyTab>
|
||||
To see your binary `sha256sum` hash, run:
|
||||
To see your binary `sha256sum` hash, run:
|
||||
```sh
|
||||
sha256sum <BINARY>
|
||||
```
|
||||
```
|
||||
```sh
|
||||
# for example
|
||||
# for example
|
||||
# sha256sum ./nym-wallet_1.2.15_amd64.AppImage
|
||||
# or
|
||||
# sha256sum ./nym-node
|
||||
@@ -49,7 +54,7 @@ sha256sum <BINARY>
|
||||
- Open it with your text editor or print its content with `cat hashes.json`
|
||||
- Check it if your binary `sha256sum` output is in `hashes.json` by using the `sha256sum` and searching for it or using `grep` command:
|
||||
```
|
||||
grep -i <SHA_HASH>
|
||||
grep -i <SHA_HASH>
|
||||
```
|
||||
</MyTab>
|
||||
<MyTab>
|
||||
|
||||
@@ -47,6 +47,83 @@ This page displays a full list of all the changes during our release cycle from
|
||||
|
||||
<VarInfo />
|
||||
|
||||
## `v2025.7-tex`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.9-tex)
|
||||
- [`nym-node`](nodes/nym-node.mdx) version `1.9.0`
|
||||
|
||||
```shell
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-04-15T14:36:52.729991996Z
|
||||
Build Version: 1.9.0
|
||||
Commit SHA: 08b6be93c49e8c225e74ffabb5529493bd4b13b6
|
||||
Commit Date: 2025-04-15T15:29:46.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
|
||||
### Operators Updates & Tools
|
||||
|
||||
- **[Nym release binaries](https://github.com/nymtech/nym/releases) no longer work on distributions Debian bullseye/sid (11) / Ubuntu 20.04 LTS and older! Please upgrade your sever to Debian bookworm (Debian 12) / Ubuntu 22.04 (and newer)!** Alternatively compile the binaries from source.
|
||||
|
||||
- The Nym Squad League Winter Season has concluded. Changes are coming to NSL, including the new NSL council, monthly payments for contributions, revamped reports, and more. Details available in the [Winter Season report](https://forum.nym.com/t/nym-squad-league-winter-season-report/1265).
|
||||
|
||||
- [New scripts to inititialise and configure multiple VMs](nodes/preliminary-steps/vps-setup/advanced#setting-up-virtual-machines): KVM setup for virtualising nodes proved to be a good approach for admins orchestrating multiple nodes on a dedicated or bare metal machines. Now there is an option of running bash scripts speeding up the process of VM initialisation and configuration, while leaving some flexibility via interactive prompts.
|
||||
|
||||
### Features
|
||||
|
||||
- [Move all workflows on ubuntu-20 to ubuntu-22](https://github.com/nymtech/nym/pull/5455): Ubuntu 20.04 runners are scheduled to be removed from set of github hosted runners. Now is as good a time as any to finally take the plunge to switch away from ubuntu-20.04 to the shiny oh so modern ubuntu-22.04 runners
|
||||
|
||||
- [Bump `elliptic` from `6.5.5` to `6.6.1`](https://github.com/nymtech/nym/pull/5483): Bumps [`elliptic`](https://github.com/indutny/elliptic)
|
||||
|
||||
- [Bump `blake3` from `1.6.1` to `1.7.0`](https://github.com/nymtech/nym/pull/5658): Bumps [`blake3`](https://github.com/BLAKE3-team/BLAKE3)
|
||||
|
||||
- [Mix throughput tester](https://github.com/nymtech/nym/pull/5661): A utility to measure mixing throughput of a node. Especially since there are some design choices with non-obvious performance penalties. For example 1 bloomfilter per connection vs 1 global bloomfilter with additional locking for replay protection.
|
||||
|
||||
- [Update log crate](https://github.com/nymtech/nym/pull/5667): Update `log crate` to latest, fix `format_in_format_args`, fix `to_string_in_format_args`
|
||||
|
||||
- [Bump the patch-updates group across 1 directory with 7 updates](https://github.com/nymtech/nym/pull/5668):
|
||||
|
||||
| Package | From | To |
|
||||
| --- | --- | --- |
|
||||
| [`clap`](https://github.com/clap-rs/clap) | `4.5.32` | `4.5.34` |
|
||||
| [`clap_complete`](https://github.com/clap-rs/clap) | `4.5.46` | `4.5.47` |
|
||||
| [`once_cell`](https://github.com/matklad/once_cell) | `1.21.1` | `1.21.3` |
|
||||
| [`reqwest`](https://github.com/seanmonstar/reqwest) | `0.12.4` | `0.12.15` |
|
||||
| [`tempfile`](https://github.com/Stebalien/tempfile) | `3.19.0` | `3.19.1` |
|
||||
| [`time`](https://github.com/time-rs/time) | `0.3.39` | `0.3.41` |
|
||||
| [`uniffi`](https://github.com/mozilla/uniffi-rs) | `0.29.0` | `0.29.1` |
|
||||
|
||||
- [Improve explorer caching](https://github.com/nymtech/nym/pull/5669): Fix:
|
||||
- `tanstack` caching
|
||||
- graphs data bug
|
||||
- "auto" gas fee for redeem rewards
|
||||
|
||||
- [Update node versions in CI](https://github.com/nymtech/nym/pull/5677)
|
||||
|
||||
- [Bash scripts to init and configure VMs conveniently and update docs](https://github.com/nymtech/nym/pull/5681): KVM setup for virtualising nodes proved to be a good approach for admins orchestrating multiple nodes on a dedicated/bare machines. This PR adds an option of running bash scripts speeding up the process of VM initialisation and configuration, while leaving some flexibility to the users via interactive prompts.
|
||||
- Script to initialise new VM on host machine and prompting user for needed vars
|
||||
- Script to config the VM from within and prompting user for needed vars
|
||||
- Documentation of the above
|
||||
- Lift headers so the main chapters come out in the side menu for easier navigation
|
||||
|
||||
- [`clippy` for `1.86`](https://github.com/nymtech/nym/pull/5685)
|
||||
|
||||
- [Expand `/v3/nym-nodes` with geodata](https://github.com/nymtech/nym/pull/5686): Includes node description and geodata.
|
||||
|
||||
### Bugfix
|
||||
|
||||
- [Minor fixes involving key cloning and hashing](https://github.com/nymtech/nym/pull/5664): Adds fixes for several minor key related issues:
|
||||
- Copying a private keys to perform a DH
|
||||
- Perform DH against the reference to the static key to prevent memory copy of secret key material
|
||||
- Hashing an `x25519` public key without first converting to a canonical representation
|
||||
- Use derived `Hash` implementation which internally [converts to a canonical representation](https://github.com/dalek-cryptography/curve25519-dalek/blob/fbf1fb5339b22eaf9925e520c90f1821f79ef5db/curve25519-dalek/src/montgomery.rs#L103) before hashing
|
||||
|
||||
- [Fix the mac build of the wallet](https://github.com/nymtech/nym/pull/5684)
|
||||
|
||||
## `v2025.6-chuckles`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.8-chuckles)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Callout } from 'nextra/components';
|
||||
import { Steps } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import NymNodeSpecs from 'components/operators/snippets/nym-node-specs.mdx';
|
||||
import TermsConditions from 'components/operators/snippets/tc-info.mdx';
|
||||
import TermsConditions from 'components/operators/snippets/tc-info.mdx'
|
||||
|
||||
# Nym Nodes Operator Guides
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
|
||||
```sh
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-04-01T09:55:02.982234741Z
|
||||
Build Version: 1.8.0
|
||||
Commit SHA: a429d6528e99b878a310b71bdbe6d31923c52d84
|
||||
Commit Date: 2025-04-01T11:41:15.000000000+02:00
|
||||
Build Timestamp: 2025-04-15T14:36:52.729991996Z
|
||||
Build Version: 1.9.0
|
||||
Commit SHA: 08b6be93c49e8c225e74ffabb5529493bd4b13b6
|
||||
Commit Date: 2025-04-15T15:29:46.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.85.1
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="32" height="32"><svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_327_42218)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5625 0H25.4375C29.058 0.0096875 31.9903 2.942 32 6.56225V25.4375C31.9903 29.0579 29.058 31.9902 25.4377 31.9999L6.5625 32C2.94213 31.9902 0.00975 29.0579 0.000125 25.4377L0 6.5625C0.00975 2.942 2.94213 0.0096875 6.56231 0H6.5625Z" fill="#14E76F"></path>
|
||||
<path d="M7.43555 9.49333C7.43555 8.66859 8.10327 8 8.92694 8H14.3569C15.3218 8 16.1663 8.64941 16.4153 9.58293L19.8836 22.5877C19.9479 22.8289 20.3025 22.7823 20.3025 22.5327V8H24.5636V22.5067C24.5636 23.3314 23.8959 24 23.0722 24H17.6021C16.6397 24 15.7967 23.3539 15.5455 22.4237L12.1154 9.72614C12.0504 9.48563 11.6967 9.53269 11.6967 9.78184V24H7.43555V9.49333Z" fill="#242B2D"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="SvgjsClipPath1002">
|
||||
<rect width="32" height="32" fill="white"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
||||
</style></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
@@ -10,7 +10,8 @@ const config: DocsThemeConfig = {
|
||||
const config = useConfig();
|
||||
const { route } = useRouter();
|
||||
const url = process.env.NEXT_PUBLIC_SITE_URL;
|
||||
const image = url + "/nym_logo.jpg";
|
||||
const image = url + "/images/Nym_meta_Image.png";
|
||||
const favicon = url + "/favicon.svg";
|
||||
|
||||
// Define descriptions for different "books"
|
||||
const bookDescriptions: Record<string, string> = {
|
||||
@@ -40,7 +41,7 @@ const config: DocsThemeConfig = {
|
||||
<title>{title}</title>
|
||||
<meta name="author" content="Nym" />
|
||||
<link rel="canonical" href={url + route} />
|
||||
|
||||
<link rel="icon" href={favicon} type="image/svg+xml" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:site_name" content="Nym docs"></meta>
|
||||
<meta name="description" content={description} />
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/nym-validator-client": "0.18.0",
|
||||
"next": "14.2.21",
|
||||
"next": "14.2.26",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
|
||||
@@ -35,13 +35,12 @@
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"chain-registry": "^1.69.64",
|
||||
"cldr-compact-number": "^0.4.0",
|
||||
"d3-scale": "^4.0.2",
|
||||
"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",
|
||||
"next": "^14.2.26",
|
||||
"openapi-fetch": "^0.13.4",
|
||||
"qrcode.react": "^4.1.0",
|
||||
"qs": "^6.14.0",
|
||||
@@ -50,8 +49,6 @@
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-random-avatars": "^1.3.1",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
@@ -60,7 +57,6 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-simple-maps": "^3.0.6",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"lefthook": "^1.8.5",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 218 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 223 KiB |
@@ -1,14 +1,12 @@
|
||||
import { countryCodeMap } from "@/assets/countryCodes";
|
||||
import { addSeconds } from "date-fns";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
CountryDataResponse,
|
||||
CurrentEpochData,
|
||||
ExplorerData,
|
||||
GatewayStatus,
|
||||
IAccountBalancesInfo,
|
||||
IObservatoryNode,
|
||||
IPacketsAndStakingData,
|
||||
NS_NODE,
|
||||
NodeRewardDetails,
|
||||
NymTokenomics,
|
||||
ObservatoryBalance,
|
||||
@@ -17,7 +15,7 @@ import {
|
||||
CURRENT_EPOCH,
|
||||
CURRENT_EPOCH_REWARDS,
|
||||
DATA_OBSERVATORY_BALANCES_URL,
|
||||
NS_API_NODES,
|
||||
DATA_OBSERVATORY_NODES_URL,
|
||||
NYM_ACCOUNT_ADDRESS,
|
||||
NYM_PRICES_API,
|
||||
OBSERVATORY_GATEWAYS_URL,
|
||||
@@ -58,12 +56,15 @@ export const fetchGatewayStatus = async (
|
||||
export const fetchNodeDelegations = async (
|
||||
id: number,
|
||||
): Promise<NodeRewardDetails[]> => {
|
||||
const response = await fetch(`${NS_API_NODES}/${id}/delegations`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
const response = await fetch(
|
||||
`${DATA_OBSERVATORY_NODES_URL}/${id}/delegations`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch delegations");
|
||||
@@ -191,6 +192,38 @@ export const fetchAccountBalance = async (
|
||||
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> => {
|
||||
@@ -206,104 +239,3 @@ export const fetchNymPrice = async (): Promise<NymTokenomics> => {
|
||||
const data: NymTokenomics = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchNSApiNodes = async (): Promise<NS_NODE[]> => {
|
||||
if (!NS_API_NODES) {
|
||||
throw new Error("NS_API_NODES URL is not defined");
|
||||
}
|
||||
|
||||
const allNodes: any[] = [];
|
||||
let page = 0;
|
||||
const PAGE_SIZE = 200;
|
||||
let totalItems = 0;
|
||||
let hasMoreData = true;
|
||||
|
||||
while (hasMoreData) {
|
||||
const response = await fetch(
|
||||
`${NS_API_NODES}?page=${page}&size=${PAGE_SIZE}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch NS API nodes (page ${page}): ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const nodes: any[] = data.items || [];
|
||||
allNodes.push(...nodes);
|
||||
|
||||
// Get total count from response
|
||||
totalItems = data.total || 0;
|
||||
|
||||
// Check if we've fetched all items
|
||||
if (allNodes.length >= totalItems) {
|
||||
hasMoreData = false;
|
||||
} else {
|
||||
page++; // Move to the next page
|
||||
}
|
||||
}
|
||||
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
export const fetchWorldMapCountries = async (): Promise<{
|
||||
countries: CountryDataResponse;
|
||||
totalCountries: number;
|
||||
uniqueLocations: number;
|
||||
totalServers: number;
|
||||
}> => {
|
||||
// Fetch all nodes from the NS API
|
||||
const nodes = await fetchNSApiNodes();
|
||||
|
||||
// Create a map to count nodes by country
|
||||
const countryCounts: Record<string, number> = {};
|
||||
// Set to track unique cities
|
||||
const uniqueCities = new Set<string>();
|
||||
|
||||
// Process each node
|
||||
for (const node of nodes) {
|
||||
// Get the 2-letter country code from the node's geoip data
|
||||
const twoLetterCode = node.geoip?.country;
|
||||
|
||||
if (twoLetterCode) {
|
||||
// Convert to 3-letter country code
|
||||
const threeLetterCode = countryCodeMap[twoLetterCode] || twoLetterCode;
|
||||
|
||||
// Increment the count for this country
|
||||
countryCounts[threeLetterCode] =
|
||||
(countryCounts[threeLetterCode] || 0) + 1;
|
||||
|
||||
// Add city to unique cities set if it exists
|
||||
if (node.geoip?.city) {
|
||||
uniqueCities.add(node.geoip.city);
|
||||
}
|
||||
} else {
|
||||
// If no geoip data, count it as unknown
|
||||
countryCounts[""] = (countryCounts[""] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the counts to the required format
|
||||
const result: CountryDataResponse = {};
|
||||
|
||||
for (const [threeLetterCode, count] of Object.entries(countryCounts)) {
|
||||
result[threeLetterCode] = {
|
||||
ISO3: threeLetterCode,
|
||||
nodes: count,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
countries: result,
|
||||
totalCountries: Object.keys(countryCounts).length,
|
||||
uniqueLocations: uniqueCities.size,
|
||||
totalServers: nodes.length,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -72,59 +72,56 @@ export interface ExplorerData {
|
||||
}
|
||||
|
||||
export type NodeDescription = {
|
||||
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;
|
||||
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;
|
||||
};
|
||||
last_polled: string;
|
||||
host_information: {
|
||||
ip_address: string[];
|
||||
hostname: string;
|
||||
ip_address: [string, string];
|
||||
keys: {
|
||||
ed25519: string;
|
||||
x25519: string;
|
||||
x25519_noise: string | null;
|
||||
};
|
||||
};
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
declared_role: {
|
||||
mixnode: boolean;
|
||||
entry: boolean;
|
||||
exit_nr: boolean;
|
||||
exit_ipr: boolean;
|
||||
};
|
||||
last_polled: string;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number;
|
||||
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;
|
||||
};
|
||||
wireguard: {
|
||||
port: number;
|
||||
public_key: string;
|
||||
ip_packet_router: {
|
||||
address: string;
|
||||
};
|
||||
authenticator: {
|
||||
address: string;
|
||||
};
|
||||
wireguard: string | null;
|
||||
mixnet_websockets: {
|
||||
ws_port: number;
|
||||
wss_port: number | null;
|
||||
};
|
||||
} | null;
|
||||
|
||||
@@ -168,6 +165,15 @@ export type Location = {
|
||||
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 {
|
||||
@@ -201,15 +207,111 @@ export interface IAccountBalancesInfo {
|
||||
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;
|
||||
};
|
||||
block_height: number;
|
||||
cumulative_reward_ratio: string;
|
||||
height: number;
|
||||
node_id: number;
|
||||
owner: string;
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
export type LastProbeResult = {
|
||||
@@ -378,58 +480,3 @@ export type NymTokenomics = {
|
||||
symbol: string;
|
||||
total_supply: number;
|
||||
};
|
||||
|
||||
export type CountryData = {
|
||||
ISO3: string;
|
||||
nodes: number;
|
||||
};
|
||||
|
||||
export interface CountryDataResponse {
|
||||
[threeLetterCountryCode: string]: CountryData;
|
||||
}
|
||||
|
||||
export type NS_NODE = {
|
||||
accepted_tnc: boolean;
|
||||
bonded: boolean;
|
||||
bonding_address: string;
|
||||
description: {
|
||||
details: string;
|
||||
moniker: string;
|
||||
security_contact: string;
|
||||
website: string;
|
||||
};
|
||||
geoip?: {
|
||||
city: string;
|
||||
country: string;
|
||||
ip_address: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
org: string;
|
||||
postal: string;
|
||||
region: string;
|
||||
timezone: 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;
|
||||
} | null;
|
||||
self_description?: NodeDescription;
|
||||
total_stake: string;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,11 @@ export const NYM_ACCOUNT_ADDRESS =
|
||||
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_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 =
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
export const TABLET_WIDTH = "(min-width:700px)";
|
||||
|
||||
export const RECOMMENDED_NODES = [
|
||||
1362, 291, 1719, 1768, 1772, 1512, 896, 1415, 2114, 2010,
|
||||
];
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { WorldMap } from "@/components/worldMap/WorldMap";
|
||||
import { Stack } 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 { CurrentEpochCardWrapper } from "../components/landingPageComponents/CurrentEpochCardWrapper";
|
||||
import { NetworkStakeCardWrapper } from "../components/landingPageComponents/NetworkStakeCardWrapper";
|
||||
import { NoiseCardWrapper } from "../components/landingPageComponents/NoiseCardWrapper";
|
||||
import { StakersNumberCardWrapper } from "../components/landingPageComponents/StakersNumberCardWrapper";
|
||||
import { TokenomicsCardWrapper } from "../components/landingPageComponents/TokenomicsCardWrapper";
|
||||
import { CurrentEpochCard } from "../components/landingPageComponents/CurrentEpochCard";
|
||||
import { NetworkStakeCard } from "../components/landingPageComponents/NetworkStakeCard";
|
||||
import { NoiseCard } from "../components/landingPageComponents/NoiseCard";
|
||||
import { StakersNumberCard } from "../components/landingPageComponents/StakersNumberCard";
|
||||
import { TokenomicsCard } from "../components/landingPageComponents/TokenomicsCard";
|
||||
import NodeTable from "../components/nodeTable/NodeTableWithAction";
|
||||
import NodeAndAddressSearch from "../components/search/NodeAndAddressSearch";
|
||||
|
||||
@@ -17,29 +16,38 @@ export default async function Home() {
|
||||
<ContentLayout>
|
||||
<Stack gap={5}>
|
||||
<NodeAndAddressSearch />
|
||||
<WorldMap />
|
||||
</Stack>
|
||||
<Grid container columnSpacing={5} rowSpacing={5}>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Network Overview" />
|
||||
<SectionHeading title="Noise Generating Network Overview" />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<NoiseCard />
|
||||
</Grid>
|
||||
<NoiseCardWrapper />
|
||||
<Grid
|
||||
container
|
||||
columnSpacing={5}
|
||||
rowSpacing={5}
|
||||
size={{ xs: 12, sm: 6, lg: 3 }}
|
||||
>
|
||||
<StakersNumberCardWrapper />
|
||||
<CurrentEpochCardWrapper />
|
||||
<Grid size={12}>
|
||||
<StakersNumberCard />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<CurrentEpochCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<NetworkStakeCardWrapper />
|
||||
<TokenomicsCardWrapper />
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<NetworkStakeCard />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
|
||||
<TokenomicsCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container rowSpacing={5}>
|
||||
<Grid container>
|
||||
<Grid size={12}>
|
||||
<SectionHeading title="Nym Servers" />
|
||||
<SectionHeading title="Nym Nodes" />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<NodeTable />
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
// Map of 2-letter country codes to 3-letter country codes
|
||||
export const countryCodeMap: Record<string, string> = {
|
||||
AF: "AFG", // Afghanistan
|
||||
AL: "ALB", // Albania
|
||||
DZ: "DZA", // Algeria
|
||||
AD: "AND", // Andorra
|
||||
AO: "AGO", // Angola
|
||||
AG: "ATG", // Antigua and Barbuda
|
||||
AR: "ARG", // Argentina
|
||||
AM: "ARM", // Armenia
|
||||
AU: "AUS", // Australia
|
||||
AT: "AUT", // Austria
|
||||
AZ: "AZE", // Azerbaijan
|
||||
BS: "BHS", // Bahamas
|
||||
BH: "BHR", // Bahrain
|
||||
BD: "BGD", // Bangladesh
|
||||
BB: "BRB", // Barbados
|
||||
BY: "BLR", // Belarus
|
||||
BE: "BEL", // Belgium
|
||||
BZ: "BLZ", // Belize
|
||||
BJ: "BEN", // Benin
|
||||
BT: "BTN", // Bhutan
|
||||
BO: "BOL", // Bolivia
|
||||
BA: "BIH", // Bosnia and Herzegovina
|
||||
BW: "BWA", // Botswana
|
||||
BR: "BRA", // Brazil
|
||||
BN: "BRN", // Brunei
|
||||
BG: "BGR", // Bulgaria
|
||||
BF: "BFA", // Burkina Faso
|
||||
BI: "BDI", // Burundi
|
||||
KH: "KHM", // Cambodia
|
||||
CM: "CMR", // Cameroon
|
||||
CA: "CAN", // Canada
|
||||
CV: "CPV", // Cape Verde
|
||||
CF: "CAF", // Central African Republic
|
||||
TD: "TCD", // Chad
|
||||
CL: "CHL", // Chile
|
||||
CN: "CHN", // China
|
||||
CO: "COL", // Colombia
|
||||
KM: "COM", // Comoros
|
||||
CG: "COG", // Congo
|
||||
CR: "CRI", // Costa Rica
|
||||
HR: "HRV", // Croatia
|
||||
CU: "CUB", // Cuba
|
||||
CY: "CYP", // Cyprus
|
||||
CZ: "CZE", // Czech Republic
|
||||
DK: "DNK", // Denmark
|
||||
DJ: "DJI", // Djibouti
|
||||
DM: "DMA", // Dominica
|
||||
DO: "DOM", // Dominican Republic
|
||||
EC: "ECU", // Ecuador
|
||||
EG: "EGY", // Egypt
|
||||
SV: "SLV", // El Salvador
|
||||
GQ: "GNQ", // Equatorial Guinea
|
||||
ER: "ERI", // Eritrea
|
||||
EE: "EST", // Estonia
|
||||
ET: "ETH", // Ethiopia
|
||||
FJ: "FJI", // Fiji
|
||||
FI: "FIN", // Finland
|
||||
FR: "FRA", // France
|
||||
GA: "GAB", // Gabon
|
||||
GM: "GMB", // Gambia
|
||||
GE: "GEO", // Georgia
|
||||
DE: "DEU", // Germany
|
||||
GH: "GHA", // Ghana
|
||||
GR: "GRC", // Greece
|
||||
GD: "GRD", // Grenada
|
||||
GT: "GTM", // Guatemala
|
||||
GN: "GIN", // Guinea
|
||||
GW: "GNB", // Guinea-Bissau
|
||||
GY: "GUY", // Guyana
|
||||
HT: "HTI", // Haiti
|
||||
HN: "HND", // Honduras
|
||||
HU: "HUN", // Hungary
|
||||
IS: "ISL", // Iceland
|
||||
IN: "IND", // India
|
||||
ID: "IDN", // Indonesia
|
||||
IR: "IRN", // Iran
|
||||
IQ: "IRQ", // Iraq
|
||||
IE: "IRL", // Ireland
|
||||
IL: "ISR", // Israel
|
||||
IT: "ITA", // Italy
|
||||
JM: "JAM", // Jamaica
|
||||
JP: "JPN", // Japan
|
||||
JO: "JOR", // Jordan
|
||||
KZ: "KAZ", // Kazakhstan
|
||||
KE: "KEN", // Kenya
|
||||
KI: "KIR", // Kiribati
|
||||
KP: "PRK", // North Korea
|
||||
KR: "KOR", // South Korea
|
||||
KW: "KWT", // Kuwait
|
||||
KG: "KGZ", // Kyrgyzstan
|
||||
LA: "LAO", // Laos
|
||||
LV: "LVA", // Latvia
|
||||
LB: "LBN", // Lebanon
|
||||
LS: "LSO", // Lesotho
|
||||
LR: "LBR", // Liberia
|
||||
LY: "LBY", // Libya
|
||||
LI: "LIE", // Liechtenstein
|
||||
LT: "LTU", // Lithuania
|
||||
LU: "LUX", // Luxembourg
|
||||
MG: "MDG", // Madagascar
|
||||
MW: "MWI", // Malawi
|
||||
MY: "MYS", // Malaysia
|
||||
MV: "MDV", // Maldives
|
||||
ML: "MLI", // Mali
|
||||
MT: "MLT", // Malta
|
||||
MH: "MHL", // Marshall Islands
|
||||
MR: "MRT", // Mauritania
|
||||
MU: "MUS", // Mauritius
|
||||
MX: "MEX", // Mexico
|
||||
FM: "FSM", // Micronesia
|
||||
MD: "MDA", // Moldova
|
||||
MC: "MCO", // Monaco
|
||||
MN: "MNG", // Mongolia
|
||||
ME: "MNE", // Montenegro
|
||||
MA: "MAR", // Morocco
|
||||
MZ: "MOZ", // Mozambique
|
||||
MM: "MMR", // Myanmar
|
||||
NA: "NAM", // Namibia
|
||||
NR: "NRU", // Nauru
|
||||
NP: "NPL", // Nepal
|
||||
NL: "NLD", // Netherlands
|
||||
NZ: "NZL", // New Zealand
|
||||
NI: "NIC", // Nicaragua
|
||||
NE: "NER", // Niger
|
||||
NG: "NGA", // Nigeria
|
||||
NO: "NOR", // Norway
|
||||
OM: "OMN", // Oman
|
||||
PK: "PAK", // Pakistan
|
||||
PW: "PLW", // Palau
|
||||
PA: "PAN", // Panama
|
||||
PG: "PNG", // Papua New Guinea
|
||||
PY: "PRY", // Paraguay
|
||||
PE: "PER", // Peru
|
||||
PH: "PHL", // Philippines
|
||||
PL: "POL", // Poland
|
||||
PT: "PRT", // Portugal
|
||||
QA: "QAT", // Qatar
|
||||
RO: "ROU", // Romania
|
||||
RU: "RUS", // Russia
|
||||
RW: "RWA", // Rwanda
|
||||
KN: "KNA", // Saint Kitts and Nevis
|
||||
LC: "LCA", // Saint Lucia
|
||||
VC: "VCT", // Saint Vincent and the Grenadines
|
||||
WS: "WSM", // Samoa
|
||||
SM: "SMR", // San Marino
|
||||
ST: "STP", // Sao Tome and Principe
|
||||
SA: "SAU", // Saudi Arabia
|
||||
SN: "SEN", // Senegal
|
||||
RS: "SRB", // Serbia
|
||||
SC: "SYC", // Seychelles
|
||||
SL: "SLE", // Sierra Leone
|
||||
SG: "SGP", // Singapore
|
||||
SK: "SVK", // Slovakia
|
||||
SI: "SVN", // Slovenia
|
||||
SB: "SLB", // Solomon Islands
|
||||
SO: "SOM", // Somalia
|
||||
ZA: "ZAF", // South Africa
|
||||
SS: "SSD", // South Sudan
|
||||
ES: "ESP", // Spain
|
||||
LK: "LKA", // Sri Lanka
|
||||
SD: "SDN", // Sudan
|
||||
SR: "SUR", // Suriname
|
||||
SZ: "SWZ", // Swaziland
|
||||
SE: "SWE", // Sweden
|
||||
CH: "CHE", // Switzerland
|
||||
SY: "SYR", // Syria
|
||||
TW: "TWN", // Taiwan
|
||||
TJ: "TJK", // Tajikistan
|
||||
TZ: "TZA", // Tanzania
|
||||
TH: "THA", // Thailand
|
||||
TL: "TLS", // Timor-Leste
|
||||
TG: "TGO", // Togo
|
||||
TO: "TON", // Tonga
|
||||
TT: "TTO", // Trinidad and Tobago
|
||||
TN: "TUN", // Tunisia
|
||||
TR: "TUR", // Turkey
|
||||
TM: "TKM", // Turkmenistan
|
||||
TV: "TUV", // Tuvalu
|
||||
UG: "UGA", // Uganda
|
||||
UA: "UKR", // Ukraine
|
||||
AE: "ARE", // United Arab Emirates
|
||||
GB: "GBR", // United Kingdom
|
||||
US: "USA", // United States
|
||||
UY: "URY", // Uruguay
|
||||
UZ: "UZB", // Uzbekistan
|
||||
VU: "VUT", // Vanuatu
|
||||
VA: "VAT", // Vatican City
|
||||
VE: "VEN", // Venezuela
|
||||
VN: "VNM", // Vietnam
|
||||
YE: "YEM", // Yemen
|
||||
ZM: "ZMB", // Zambia
|
||||
ZW: "ZWE", // Zimbabwe
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { fetchAccountBalance, fetchNymPrice } from "@/app/api";
|
||||
import { Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
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";
|
||||
@@ -45,7 +45,7 @@ const getAllocation = (unyms: number, totalUnyms: number): number => {
|
||||
};
|
||||
|
||||
const calculateStakingRewards = (
|
||||
accumulatedRewards: IRewardDetails[]
|
||||
accumulatedRewards: IRewardDetails[],
|
||||
): number => {
|
||||
if (accumulatedRewards.length > 0) {
|
||||
const totalRewards = accumulatedRewards.reduce((total, rewardDetail) => {
|
||||
@@ -61,8 +61,6 @@ const calculateStakingRewards = (
|
||||
|
||||
export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
const { address } = props;
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
const {
|
||||
data: accountInfo,
|
||||
@@ -103,10 +101,7 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
if (isError || priceError || !accountInfo || !nymPrice) {
|
||||
return (
|
||||
<ExplorerCard label="Total value">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to account data.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
@@ -118,7 +113,7 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
|
||||
const totalBalanceUSD = getPriceInUSD(
|
||||
Number(accountInfo.total_value.amount),
|
||||
nymPriceData
|
||||
nymPriceData,
|
||||
);
|
||||
const spendableNYM =
|
||||
accountInfo.balances.length > 0
|
||||
@@ -132,46 +127,46 @@ export const AccountBalancesCard = (props: IAccountBalancesCardProps) => {
|
||||
accountInfo.balances.length > 0
|
||||
? getAllocation(
|
||||
Number(accountInfo.balances[0].amount),
|
||||
Number(accountInfo.total_value.amount)
|
||||
Number(accountInfo.total_value.amount),
|
||||
)
|
||||
: 0;
|
||||
|
||||
const delegationsNYM = getNymsFormated(
|
||||
Number(accountInfo.total_delegations.amount)
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
);
|
||||
const delegationsUSD = getPriceInUSD(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
nymPriceData
|
||||
nymPriceData,
|
||||
);
|
||||
const delegationsAllocation = getAllocation(
|
||||
Number(accountInfo.total_delegations.amount),
|
||||
Number(accountInfo.total_value.amount)
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const operatorRewardsAllocation = getAllocation(
|
||||
Number(accountInfo.operator_rewards?.amount || 0),
|
||||
Number(accountInfo.total_value.amount)
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const operatorRewardsNYM = getNymsFormated(
|
||||
Number(accountInfo.operator_rewards?.amount || 0)
|
||||
Number(accountInfo.operator_rewards?.amount || 0),
|
||||
);
|
||||
|
||||
const operatorRewardsUSD = getPriceInUSD(
|
||||
Number(accountInfo.operator_rewards?.amount || 0),
|
||||
nymPriceData
|
||||
nymPriceData,
|
||||
);
|
||||
|
||||
const claimableNYM = getNymsFormated(
|
||||
Number(accountInfo.claimable_rewards.amount)
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
);
|
||||
const claimableUSD = getPriceInUSD(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
nymPriceData
|
||||
nymPriceData,
|
||||
);
|
||||
const claimableAllocation = getAllocation(
|
||||
Number(accountInfo.claimable_rewards.amount),
|
||||
Number(accountInfo.total_value.amount)
|
||||
Number(accountInfo.total_value.amount),
|
||||
);
|
||||
|
||||
const stakingRewards =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { fetchAccountBalance } from "@/app/api";
|
||||
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
@@ -13,8 +13,6 @@ interface IAccountInfoCardProps {
|
||||
|
||||
export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
const { address } = props;
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["accountBalance", address],
|
||||
@@ -40,10 +38,7 @@ export const AccountInfoCard = (props: IAccountInfoCardProps) => {
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="Total NYM">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to account data.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -10,20 +9,18 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function AccountPageButtonGroup({ address }: Props) {
|
||||
const { data: nsApiNodes = [], isError: isNSApiNodesError } = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
const { data: nymNodes, isError } = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (!nsApiNodes || isNSApiNodesError) return null;
|
||||
if (!nymNodes || isError) return null;
|
||||
|
||||
const nymNode = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.bonding_address === address
|
||||
);
|
||||
const nymNode = nymNodes.find((node) => node.bonding_address === address);
|
||||
|
||||
if (!nymNode) return null;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const BlogArticlesCards = async ({
|
||||
const blogArticle = JSON.parse(fileContent);
|
||||
return {
|
||||
...blogArticle,
|
||||
link: `/onboarding/${filename.replace(".json", "")}`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ type BlogArticle = {
|
||||
image: string;
|
||||
iconLight: string;
|
||||
iconDark: string;
|
||||
link: string;
|
||||
attributes: {
|
||||
blogAuthors: string[];
|
||||
date: Date;
|
||||
@@ -24,6 +23,8 @@ type BlogArticle = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type BlogArticleWithLink = BlogArticle;
|
||||
export type BlogArticleWithLink = BlogArticle & {
|
||||
link: string;
|
||||
};
|
||||
|
||||
export default BlogArticle;
|
||||
|
||||
@@ -68,12 +68,7 @@ const ExplorerHeroCard = ({
|
||||
const iconSrc = isDarkMode ? iconDarkSrc : iconLightSrc;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={link}
|
||||
sx={{ textDecoration: "none", height: "100%" }}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Link href={link} sx={{ textDecoration: "none", height: "100%" }}>
|
||||
<Card sx={dynamicCardStyles} elevation={0}>
|
||||
<CardHeader
|
||||
title={
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useCopyToClipboard } from "@uidotdev/usehooks";
|
||||
import { useEffect } from "react";
|
||||
import CopyFile from "../icons/CopyFile";
|
||||
import CopyFileDark from "../icons/CopyFileDark";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
|
||||
const CLEAR_AFTER_MS = 10_000;
|
||||
|
||||
@@ -35,8 +34,12 @@ const CopyToClipboard = ({
|
||||
|
||||
if (hasCopied) {
|
||||
return (
|
||||
<Typography sx={{ color: isDarkMode ? "base.white" : "pine.950" }}>
|
||||
<CheckIcon fontSize="small" />
|
||||
<Typography
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
variant="h6"
|
||||
color="textSecondary"
|
||||
>
|
||||
Copied
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function Footer() {
|
||||
const locale = "en";
|
||||
const footerData = await getFooter(locale);
|
||||
const legalContent1 =
|
||||
"Nym Noise Generating Network Explorer, V 2.2.0 Public Beta release.";
|
||||
"Nym Noise Generating Network Explorer, V 2.1.0 Public Beta release.";
|
||||
const legalContent2 = footerData?.attributes?.legalContent2 || false;
|
||||
const footerLinkBlocks = footerData?.attributes?.linkBlocks || [];
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
"use client";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ConditionalCardWrapperProps {
|
||||
children: ReactNode;
|
||||
size?:
|
||||
| number
|
||||
| { xs?: number; sm?: number; md?: number; lg?: number; xl?: number };
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export const ConditionalCardWrapper = ({
|
||||
children,
|
||||
size,
|
||||
visible = true,
|
||||
}: ConditionalCardWrapperProps) => {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Grid size={size}>{children}</Grid>;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type EpochResponseData,
|
||||
useEpochContext,
|
||||
} from "@/providers/EpochProvider";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { differenceInMinutes, format } from "date-fns";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -34,13 +34,10 @@ export const CurrentEpochCard = () => {
|
||||
const [endTime, setEndTime] = useState("");
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
const updateState = useCallback((data: NonNullable<EpochResponseData>) => {
|
||||
const { startTime, endTime } = getStartEndTime(
|
||||
data.current_epoch_start,
|
||||
data.current_epoch_end
|
||||
data.current_epoch_end,
|
||||
);
|
||||
const progress = calulateProgress(data.current_epoch_end);
|
||||
|
||||
@@ -68,28 +65,30 @@ export const CurrentEpochCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="body3" fontWeight="light">
|
||||
Failed to load data
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<Typography variant="body3" fontWeight="light">
|
||||
No data available
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (epochStatus === "pending") {
|
||||
return (
|
||||
<ExplorerCard label="Current mixnet epoch">
|
||||
<Typography
|
||||
variant="body3"
|
||||
fontWeight="light"
|
||||
height={80}
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="body3" fontWeight="light" height={80}>
|
||||
Waiting for next epoch to start...
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"use client";
|
||||
import { useEpochContext } from "@/providers/EpochProvider";
|
||||
import { CurrentEpochCard } from "./CurrentEpochCard";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
|
||||
export const CurrentEpochCardWrapper = () => {
|
||||
const { data, isError, isLoading, epochStatus } = useEpochContext();
|
||||
|
||||
// Determine if the card should be visible
|
||||
// Show the card if we have data and it's not in a pending state, or if we're still loading
|
||||
const isVisible =
|
||||
!isError && (data || isLoading) && epochStatus !== "pending";
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={12} visible={isVisible}>
|
||||
<CurrentEpochCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -33,14 +33,21 @@ export const NetworkStakeCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Don't display the card if there's an error or insufficient data
|
||||
if (
|
||||
isStakingError ||
|
||||
!packetsAndStaking ||
|
||||
!Array.isArray(packetsAndStaking) ||
|
||||
packetsAndStaking.length < 10
|
||||
) {
|
||||
return null;
|
||||
if (isStakingError || !packetsAndStaking) {
|
||||
return (
|
||||
<ExplorerCard label="Current network stake">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const packetsAndStakingData: ExplorerData["packetsAndStakingData"] =
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
import { fetchNoise } from "@/app/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { NetworkStakeCard } from "./NetworkStakeCard";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
|
||||
export const NetworkStakeCardWrapper = () => {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
// Determine if the card should be visible
|
||||
const isVisible =
|
||||
!isLoading && !isError && data && Array.isArray(data) && data.length >= 10;
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={{ xs: 12, sm: 6, lg: 3 }} visible={isVisible}>
|
||||
<NetworkStakeCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -41,21 +41,31 @@ export const NoiseCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Don't display the card if there's an error or insufficient data
|
||||
if (isError || !data || !Array.isArray(data) || data.length < 10) {
|
||||
return null;
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<ExplorerCard label="Mixnet traffic">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={238} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
const todaysData = data[data.length - 2];
|
||||
const yesterdaysData = data[data.length - 3];
|
||||
|
||||
const noiseLast24H =
|
||||
(todaysData?.total_packets_sent || 0) +
|
||||
(todaysData?.total_packets_received || 0);
|
||||
todaysData.total_packets_sent + todaysData.total_packets_received;
|
||||
|
||||
const noisePrevious24H =
|
||||
(yesterdaysData?.total_packets_sent || 0) +
|
||||
(yesterdaysData?.total_packets_received || 0);
|
||||
yesterdaysData.total_packets_sent + yesterdaysData.total_packets_received;
|
||||
|
||||
const formatNoiseVolume = (packets: number): string => {
|
||||
if (packets < 0) {
|
||||
@@ -97,9 +107,8 @@ export const NoiseCard = () => {
|
||||
.slice(0, -1)
|
||||
.map((item: IPacketsAndStakingData) => {
|
||||
return {
|
||||
date_utc: item?.date_utc,
|
||||
numericData:
|
||||
(item?.total_packets_sent || 0) + (item?.total_packets_received || 0),
|
||||
date_utc: item.date_utc,
|
||||
numericData: item.total_packets_sent + item.total_packets_received,
|
||||
};
|
||||
})
|
||||
.filter((item) => item.numericData >= 2_500_000_000);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
import { fetchNoise } from "@/app/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { NoiseCard } from "./NoiseCard";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
|
||||
export const NoiseCardWrapper = () => {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
// Determine if the card should be visible
|
||||
const isVisible =
|
||||
!isLoading && !isError && data && Array.isArray(data) && data.length >= 10;
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={{ xs: 12, sm: 6, lg: 3 }} visible={isVisible}>
|
||||
<NoiseCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,18 @@
|
||||
"use client";
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
export const StakersNumberCard = () => {
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: () => fetchObservatoryNodes(),
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
@@ -22,7 +22,7 @@ export const StakersNumberCard = () => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
if (isNSApiNodesLoading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Number of delegations">
|
||||
<Skeleton variant="text" height={90} />
|
||||
@@ -30,11 +30,11 @@ export const StakersNumberCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isNSApiNodesError || !nsApiNodes) {
|
||||
if (isError || !nymNodes) {
|
||||
return (
|
||||
<ExplorerCard label="Number of delegations">
|
||||
<Typography
|
||||
variant="h5"
|
||||
variant="h3"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
Failed to load node data.
|
||||
@@ -43,13 +43,13 @@ export const StakersNumberCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const getActiveStakersNumber = (nodes: NS_NODE[]): number => {
|
||||
const getActiveStakersNumber = (nodes: IObservatoryNode[]): number => {
|
||||
return nodes.reduce(
|
||||
(sum, node) => sum + (node.rewarding_details?.unique_delegations || 0),
|
||||
0
|
||||
(sum, node) => sum + node.rewarding_details.unique_delegations,
|
||||
0,
|
||||
);
|
||||
};
|
||||
const allStakers = getActiveStakersNumber(nsApiNodes);
|
||||
const allStakers = getActiveStakersNumber(nymNodes);
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Number of delegations">
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"use client";
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { StakersNumberCard } from "./StakersNumberCard";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
|
||||
export const StakersNumberCardWrapper = () => {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
// Determine if the card should be visible
|
||||
const isVisible =
|
||||
!isLoading && !isError && data && Array.isArray(data) && data.length > 0;
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={12} visible={isVisible}>
|
||||
<StakersNumberCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { fetchEpochRewards, fetchNoise, fetchNymPrice } from "@/app/api";
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ExplorerData, NymTokenomics } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -9,8 +9,6 @@ import ExplorerListItem from "../list/ListItem";
|
||||
import { TitlePrice } from "../price/TitlePrice";
|
||||
|
||||
export const TokenomicsCard = () => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
const {
|
||||
data: nymPrice,
|
||||
isLoading,
|
||||
@@ -71,10 +69,7 @@ export const TokenomicsCard = () => {
|
||||
) {
|
||||
return (
|
||||
<ExplorerCard label="Tokenomics overview">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
Failed to load tokenomics overview.
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={80} />
|
||||
@@ -103,7 +98,7 @@ export const TokenomicsCard = () => {
|
||||
function calculateTVL(
|
||||
epochRewards: ExplorerData["currentEpochRewardsData"],
|
||||
nymPriceData: NymTokenomics,
|
||||
packetsAndStaking: ExplorerData["packetsAndStakingData"]
|
||||
packetsAndStaking: ExplorerData["packetsAndStakingData"],
|
||||
): number {
|
||||
const lastTotalStake =
|
||||
packetsAndStaking[packetsAndStaking.length - 1]?.total_stake || 0;
|
||||
@@ -114,7 +109,7 @@ export const TokenomicsCard = () => {
|
||||
);
|
||||
}
|
||||
const TVL = formatBigNum(
|
||||
calculateTVL(epochRewardsData, nymPrice, packetsAndStakingData)
|
||||
calculateTVL(epochRewardsData, nymPrice, packetsAndStakingData),
|
||||
);
|
||||
|
||||
const dataRows = [
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"use client";
|
||||
import { fetchEpochRewards, fetchNoise, fetchNymPrice } from "@/app/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { TokenomicsCard } from "./TokenomicsCard";
|
||||
import { ConditionalCardWrapper } from "./ConditionalCardWrapper";
|
||||
|
||||
export const TokenomicsCardWrapper = () => {
|
||||
const {
|
||||
data: nymPrice,
|
||||
isLoading: isPriceLoading,
|
||||
isError: isPriceError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymPrice"],
|
||||
queryFn: fetchNymPrice,
|
||||
staleTime: 10 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const {
|
||||
data: epochRewards,
|
||||
isLoading: isEpochLoading,
|
||||
isError: isEpochError,
|
||||
} = useQuery({
|
||||
queryKey: ["epochRewards"],
|
||||
queryFn: fetchEpochRewards,
|
||||
staleTime: 10 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const {
|
||||
data: packetsAndStaking,
|
||||
isLoading: isStakingLoading,
|
||||
isError: isStakingError,
|
||||
} = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
staleTime: 10 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
// Determine if the card should be visible
|
||||
const isLoading = isPriceLoading || isEpochLoading || isStakingLoading;
|
||||
const hasError = isPriceError || isEpochError || isStakingError;
|
||||
const hasData =
|
||||
nymPrice &&
|
||||
epochRewards &&
|
||||
packetsAndStaking &&
|
||||
Array.isArray(packetsAndStaking) &&
|
||||
packetsAndStaking.length >= 2;
|
||||
|
||||
const isVisible = !hasError && (hasData || isLoading);
|
||||
|
||||
return (
|
||||
<ConditionalCardWrapper size={{ xs: 12, sm: 6, lg: 3 }} visible={isVisible}>
|
||||
<TokenomicsCard />
|
||||
</ConditionalCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,339 +0,0 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
Slider,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
|
||||
import FilterAltIcon from "@mui/icons-material/FilterAlt";
|
||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||
import PieChartIcon from "@mui/icons-material/PieChart";
|
||||
import PercentIcon from "@mui/icons-material/Percent";
|
||||
import NodeFilterButtonGroup from "../toggleButton/NodeFilterButtonGroup";
|
||||
import { RECOMMENDED_NODES } from "@/app/constants";
|
||||
|
||||
type AdvancedFiltersProps = {
|
||||
uptime: [number, number];
|
||||
setUptime: (value: [number, number]) => void;
|
||||
saturation: [number, number];
|
||||
setSaturation: (value: [number, number]) => void;
|
||||
profitMargin: [number, number];
|
||||
setProfitMargin: (value: [number, number]) => void;
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
maxSaturation?: number;
|
||||
activeFilter: "all" | "mixnodes" | "gateways" | "recommended";
|
||||
setActiveFilter: (
|
||||
filter: "all" | "mixnodes" | "gateways" | "recommended"
|
||||
) => void;
|
||||
nodeCounts: {
|
||||
all: number;
|
||||
mixnodes: number;
|
||||
gateways: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function AdvancedFilters({
|
||||
uptime,
|
||||
setUptime,
|
||||
saturation,
|
||||
setSaturation,
|
||||
profitMargin,
|
||||
setProfitMargin,
|
||||
open,
|
||||
setOpen,
|
||||
maxSaturation = 100,
|
||||
activeFilter,
|
||||
setActiveFilter,
|
||||
nodeCounts,
|
||||
}: AdvancedFiltersProps) {
|
||||
const theme = useTheme();
|
||||
const green = "#14e76f"; // from theme colours
|
||||
|
||||
const marksPercent: { value: number }[] = [{ value: 0 }, { value: 100 }];
|
||||
const marksSaturation: { value: number }[] = [
|
||||
{ value: 0 },
|
||||
{ value: maxSaturation },
|
||||
];
|
||||
|
||||
const panel = (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 3,
|
||||
p: 2,
|
||||
borderRadius: 3,
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontStyle: "italic",
|
||||
color:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.common.black
|
||||
: theme.palette.common.white,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
Advanced filtering mode is active
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 3,
|
||||
background: theme.palette.background.default,
|
||||
mb: { xs: 2, sm: 0 },
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
<AccessTimeIcon
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.common.white
|
||||
: theme.palette.common.black,
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.text.primary, fontSize: 17 }}
|
||||
>
|
||||
Uptime
|
||||
</Typography>
|
||||
<Box flexGrow={1} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.primary.main, fontSize: 17 }}
|
||||
>
|
||||
{uptime[0]}% - {uptime[1]}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={uptime}
|
||||
onChange={(_, v) => setUptime(v as [number, number])}
|
||||
valueLabelDisplay="off"
|
||||
min={0}
|
||||
max={100}
|
||||
marks={marksPercent}
|
||||
sx={{
|
||||
color: green,
|
||||
height: 8,
|
||||
"& .MuiSlider-thumb": {
|
||||
width: 24,
|
||||
height: 24,
|
||||
backgroundColor: green,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 3,
|
||||
background: theme.palette.background.default,
|
||||
mb: { xs: 2, sm: 0 },
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
<PieChartIcon
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.common.white
|
||||
: theme.palette.common.black,
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.text.primary, fontSize: 17 }}
|
||||
>
|
||||
Saturation
|
||||
</Typography>
|
||||
<Box flexGrow={1} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.primary.main, fontSize: 17 }}
|
||||
>
|
||||
{saturation[0]}% - {saturation[1]}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={saturation}
|
||||
onChange={(_, v) => setSaturation(v as [number, number])}
|
||||
valueLabelDisplay="off"
|
||||
min={0}
|
||||
max={maxSaturation}
|
||||
marks={marksSaturation}
|
||||
sx={{
|
||||
color: green,
|
||||
height: 8,
|
||||
"& .MuiSlider-thumb": {
|
||||
width: 24,
|
||||
height: 24,
|
||||
backgroundColor: green,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} sx={{ mx: { sm: "auto" } }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 3,
|
||||
background: theme.palette.background.default,
|
||||
mb: { xs: 2, sm: 0 },
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
<PercentIcon
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.common.white
|
||||
: theme.palette.common.black,
|
||||
mr: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.text.primary, fontSize: 17 }}
|
||||
>
|
||||
Profit Margin
|
||||
</Typography>
|
||||
<Box flexGrow={1} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: theme.palette.primary.main, fontSize: 17 }}
|
||||
>
|
||||
{profitMargin[0]}% - {profitMargin[1]}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Slider
|
||||
value={profitMargin}
|
||||
onChange={(_, v) => setProfitMargin(v as [number, number])}
|
||||
valueLabelDisplay="off"
|
||||
min={0}
|
||||
max={100}
|
||||
marks={marksPercent}
|
||||
sx={{
|
||||
color: green,
|
||||
height: 8,
|
||||
"& .MuiSlider-thumb": {
|
||||
width: 24,
|
||||
height: 24,
|
||||
backgroundColor: green,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "stretch", sm: "center" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: { xs: "100%", sm: "auto" } }}>
|
||||
<NodeFilterButtonGroup
|
||||
size="medium"
|
||||
options={[
|
||||
{
|
||||
label: `Recommended servers (${RECOMMENDED_NODES.length})`,
|
||||
isSelected: activeFilter === "recommended",
|
||||
value: "recommended",
|
||||
},
|
||||
{
|
||||
label: `All servers (${nodeCounts.all})`,
|
||||
isSelected: activeFilter === "all",
|
||||
value: "all",
|
||||
},
|
||||
{
|
||||
label: `Mixnodes (${nodeCounts.mixnodes})`,
|
||||
isSelected: activeFilter === "mixnodes",
|
||||
value: "mixnodes",
|
||||
},
|
||||
{
|
||||
label: `Gateways (${nodeCounts.gateways})`,
|
||||
isSelected: activeFilter === "gateways",
|
||||
value: "gateways",
|
||||
},
|
||||
]}
|
||||
onPage={activeFilter}
|
||||
onFilterChange={setActiveFilter}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
startIcon={
|
||||
<FilterAltIcon
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === "light"
|
||||
? `${theme.palette.common.black} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => setOpen && setOpen(!open)}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
color:
|
||||
theme.palette.mode === "light"
|
||||
? `${theme.palette.common.black} !important`
|
||||
: `${theme.palette.common.white} !important`,
|
||||
borderColor:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.common.black
|
||||
: theme.palette.common.white,
|
||||
background: "none",
|
||||
fontWeight: 500,
|
||||
fontSize: 16,
|
||||
"&:hover, &:focus": {
|
||||
background:
|
||||
theme.palette.mode === "light"
|
||||
? "rgba(0,0,0,0.04)"
|
||||
: "rgba(255,255,255,0.05)",
|
||||
borderColor:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.grey[400]
|
||||
: theme.palette.common.white,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Advanced Filters
|
||||
</Button>
|
||||
</Box>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
{panel}
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
@@ -30,7 +29,6 @@ import StakeModal from "../staking/StakeModal";
|
||||
import { fee } from "../staking/schemas";
|
||||
import ConnectWallet from "../wallet/ConnectWallet";
|
||||
import type { MappedNymNode, MappedNymNodes } from "./NodeTableWithAction";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
|
||||
const ColumnHeading = ({
|
||||
children,
|
||||
@@ -102,7 +100,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
{ nodeId },
|
||||
fee,
|
||||
"Delegation from Nym Explorer V2",
|
||||
uNymFunds
|
||||
uNymFunds,
|
||||
);
|
||||
setSelectedNodeForStaking(undefined);
|
||||
setInfoModalProps({
|
||||
@@ -131,7 +129,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[nymClient, handleRefetch]
|
||||
[nymClient, handleRefetch],
|
||||
);
|
||||
|
||||
const handleOnSelectStake = useCallback(
|
||||
@@ -160,7 +158,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
identityKey: node.identity_key,
|
||||
});
|
||||
},
|
||||
[isWalletConnected]
|
||||
[isWalletConnected],
|
||||
);
|
||||
|
||||
const columns: MRT_ColumnDef<MappedNymNode>[] = useMemo(
|
||||
@@ -191,6 +189,54 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
Cell: ({ row }) => <Favorite address={row.original.owner} />,
|
||||
},
|
||||
|
||||
{
|
||||
id: "name",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Name</ColumnHeading>,
|
||||
accessorKey: "name",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.name || "-"}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "id",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Node ID</ColumnHeading>,
|
||||
accessorKey: "nodeId",
|
||||
size: 90,
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.nodeId}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "identity_key",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Identity Key</ColumnHeading>,
|
||||
accessorKey: "identity_key",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body5">{row.original.identity_key}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "qos",
|
||||
header: "Qlt of Service",
|
||||
align: "center",
|
||||
accessorKey: "qualityOfService",
|
||||
size: 100,
|
||||
Header: <ColumnHeading>Qlt of Service</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{row.original.qualityOfService.toFixed()}%
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "location",
|
||||
header: "Location",
|
||||
@@ -209,98 +255,6 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "name",
|
||||
header: "",
|
||||
size: 210,
|
||||
|
||||
Header: <ColumnHeading>Node</ColumnHeading>,
|
||||
accessorKey: "name",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.name || "-"}</Typography>
|
||||
<Tooltip
|
||||
title={row.original.identity_key}
|
||||
placement="bottom"
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
maxWidth: "none",
|
||||
whiteSpace: "nowrap",
|
||||
bgcolor: isDarkMode ? "#374042" : "#E5E7EB",
|
||||
color: isDarkMode ? "#FFFFFF" : "#000000",
|
||||
"& .MuiTooltip-arrow": {
|
||||
color: isDarkMode ? "#374042" : "#E5E7EB",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
sx={{ height: "24px" }}
|
||||
>
|
||||
<Typography
|
||||
variant="body5"
|
||||
sx={{
|
||||
height: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{row.original.identity_key.length > 17
|
||||
? `${row.original.identity_key.slice(0, 10)}...${row.original.identity_key.slice(-8)}`
|
||||
: row.original.identity_key}
|
||||
</Typography>
|
||||
<CopyToClipboard text={row.original.identity_key} />
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "id",
|
||||
header: "",
|
||||
Header: <ColumnHeading>Node ID</ColumnHeading>,
|
||||
accessorKey: "nodeId",
|
||||
size: 90,
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.nodeId}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: "qos",
|
||||
header: "Qlt of Service",
|
||||
align: "center",
|
||||
accessorKey: "qualityOfService",
|
||||
size: 100,
|
||||
Header: <ColumnHeading>Uptime</ColumnHeading>,
|
||||
Cell: ({ row }) => {
|
||||
const value = row.original.qualityOfService;
|
||||
let color = "#000000";
|
||||
|
||||
if (value >= 80) {
|
||||
color = "#22C55E"; // green
|
||||
} else if (value >= 50) {
|
||||
color = "#F59E0B"; // amber/orange-yellow
|
||||
} else {
|
||||
color = "#EF4444"; // red
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color, fontWeight: 400 }}>
|
||||
{value.toFixed()}%
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "stakeSaturation",
|
||||
header: "Stake saturation",
|
||||
@@ -308,58 +262,9 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
size: 120,
|
||||
|
||||
Header: <ColumnHeading>Saturation</ColumnHeading>,
|
||||
Cell: ({ row }) => {
|
||||
const value = row.original.stakeSaturation;
|
||||
let color = "#000000";
|
||||
|
||||
if (value > 100) {
|
||||
color = "#EF4444";
|
||||
} else if (value >= 75) {
|
||||
color = "#22C55E";
|
||||
} else if (value >= 25) {
|
||||
color = "#F59E0B";
|
||||
} else {
|
||||
color = "#EF4444";
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography variant="body4" sx={{ color, fontWeight: 400 }}>
|
||||
{value}%
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "selfBond",
|
||||
header: "Self-bond",
|
||||
accessorKey: "selfBond",
|
||||
Header: <ColumnHeading>Self-bond</ColumnHeading>,
|
||||
Cell: ({ row }) => {
|
||||
const value = row.original.selfBond;
|
||||
let color = isDarkMode ? "#FFFFFF" : "#000000";
|
||||
|
||||
if (value === 0) {
|
||||
color = "#EF4444";
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant="body4"
|
||||
sx={{ color, fontWeight: value === 0 ? 400 : 300 }}
|
||||
>
|
||||
{row.original.selfBond} NYM
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "operatingCosts",
|
||||
header: "Operating costs",
|
||||
accessorKey: "operatingCosts",
|
||||
Header: <ColumnHeading>Operating costs</ColumnHeading>,
|
||||
Cell: ({ row }) => (
|
||||
<Typography variant="body4">
|
||||
{row.original.operatingCosts} NYM
|
||||
{row.original.stakeSaturation}%
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
@@ -398,7 +303,7 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[isWalletConnected, handleOnSelectStake, favorites, isDarkMode]
|
||||
[isWalletConnected, handleOnSelectStake, favorites],
|
||||
);
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
@@ -505,12 +410,8 @@ const NodeTable = ({ nodes }: { nodes: MappedNymNodes }) => {
|
||||
},
|
||||
},
|
||||
muiTableBodyRowProps: ({ row }) => ({
|
||||
onClick: (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
window.open(`/explorer/nym-node/${row.original.nodeId}`, "_blank");
|
||||
} else {
|
||||
router.push(`/nym-node/${row.original.nodeId}`);
|
||||
}
|
||||
onClick: () => {
|
||||
router.push(`/nym-node/${row.original.nodeId}`);
|
||||
},
|
||||
hover: true,
|
||||
sx: {
|
||||
|
||||
@@ -3,18 +3,15 @@
|
||||
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||
import type { ExplorerData, NS_NODE } from "../../app/api/types";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
|
||||
import { countryName } from "../../utils/countryName";
|
||||
import NodeTable from "./NodeTable";
|
||||
import { useState, useEffect } from "react";
|
||||
import AdvancedFilters from "./AdvancedFilters";
|
||||
import { RECOMMENDED_NODES } from "@/app/constants";
|
||||
|
||||
// Utility function to calculate node saturation point
|
||||
function getNodeSaturationPoint(
|
||||
totalStake: number,
|
||||
stakeSaturationPoint: string
|
||||
stakeSaturationPoint: string,
|
||||
): number {
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
|
||||
@@ -28,129 +25,39 @@ function getNodeSaturationPoint(
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
|
||||
const mappedNSApiNodes = (
|
||||
nodes: NS_NODE[],
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"]
|
||||
const mappedNymNodes = (
|
||||
nodes: IObservatoryNode[],
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"],
|
||||
) =>
|
||||
nodes
|
||||
.map((node) => {
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
+node.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point
|
||||
);
|
||||
nodes.map((node) => {
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
node.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
const cleanMoniker = DOMPurify.sanitize(node.description.moniker).replace(
|
||||
/&/g,
|
||||
"&"
|
||||
);
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
node.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
const selfBondFormatted = node.original_pledge
|
||||
? Number(node.original_pledge) / 1_000_000
|
||||
: 0;
|
||||
return {
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.description.auxiliary_details.location || null,
|
||||
countryName:
|
||||
countryName(node.description.auxiliary_details.location) || null,
|
||||
profitMarginPercentage:
|
||||
+node.rewarding_details.cost_params.profit_margin_percent * 100,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: nodeSaturationPoint,
|
||||
qualityOfService: +node.uptime * 100,
|
||||
};
|
||||
});
|
||||
|
||||
const operatingCostsFormatted = node.rewarding_details
|
||||
? Number(
|
||||
node.rewarding_details.cost_params.interval_operating_cost.amount
|
||||
) / 1_000_000
|
||||
: 0;
|
||||
|
||||
return {
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.geoip?.country || null,
|
||||
countryName: countryName(node.geoip?.country || null) || null,
|
||||
selfBond: selfBondFormatted,
|
||||
operatingCosts: operatingCostsFormatted,
|
||||
profitMarginPercentage: node.rewarding_details
|
||||
? +node.rewarding_details.cost_params.profit_margin_percent * 100
|
||||
: 0,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: nodeSaturationPoint,
|
||||
qualityOfService: +node.uptime * 100,
|
||||
mixnode: node.self_description?.declared_role.mixnode === true,
|
||||
gateway:
|
||||
node.self_description?.declared_role.entry === true ||
|
||||
node.self_description?.declared_role.exit_ipr === true ||
|
||||
node.self_description?.declared_role.exit_nr === true,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Handle null country names by putting them at the end
|
||||
if (!a.countryName && !b.countryName) return 0;
|
||||
if (!a.countryName) return 1;
|
||||
if (!b.countryName) return -1;
|
||||
|
||||
// Sort alphabetically by country name
|
||||
return a.countryName.localeCompare(b.countryName);
|
||||
});
|
||||
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNSApiNodes>;
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
|
||||
export type MappedNymNode = MappedNymNodes[0];
|
||||
|
||||
const NodeTableWithAction = () => {
|
||||
// All hooks at the top!
|
||||
const [activeFilter, setActiveFilter] = useState<
|
||||
"all" | "mixnodes" | "gateways" | "recommended"
|
||||
>(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableActiveFilter");
|
||||
return (
|
||||
(stored as "all" | "mixnodes" | "gateways" | "recommended") ||
|
||||
"recommended"
|
||||
);
|
||||
});
|
||||
const [uptime, setUptime] = useState<[number, number]>(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableUptime");
|
||||
return stored ? JSON.parse(stored) : [0, 100];
|
||||
});
|
||||
const [saturation, setSaturation] = useState<[number, number]>([0, 100]);
|
||||
const [profitMargin, setProfitMargin] = useState<[number, number]>(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableProfitMargin");
|
||||
return stored ? JSON.parse(stored) : [0, 100];
|
||||
});
|
||||
const [advancedOpen, setAdvancedOpen] = useState(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableAdvancedOpen");
|
||||
return stored ? JSON.parse(stored) : false;
|
||||
});
|
||||
|
||||
// Wrapper functions to handle filter changes and sessionStorage
|
||||
const handleActiveFilterChange = (
|
||||
newFilter: "all" | "mixnodes" | "gateways" | "recommended"
|
||||
) => {
|
||||
setActiveFilter(newFilter);
|
||||
sessionStorage.setItem("nodeTableActiveFilter", newFilter);
|
||||
};
|
||||
|
||||
const handleUptimeChange = (newUptime: [number, number]) => {
|
||||
setUptime(newUptime);
|
||||
sessionStorage.setItem("nodeTableUptime", JSON.stringify(newUptime));
|
||||
};
|
||||
|
||||
const handleSaturationChange = (newSaturation: [number, number]) => {
|
||||
setSaturation(newSaturation);
|
||||
sessionStorage.setItem(
|
||||
"nodeTableSaturation",
|
||||
JSON.stringify(newSaturation)
|
||||
);
|
||||
};
|
||||
|
||||
const handleProfitMarginChange = (newProfitMargin: [number, number]) => {
|
||||
setProfitMargin(newProfitMargin);
|
||||
sessionStorage.setItem(
|
||||
"nodeTableProfitMargin",
|
||||
JSON.stringify(newProfitMargin)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAdvancedOpenChange = (newAdvancedOpen: boolean) => {
|
||||
setAdvancedOpen(newAdvancedOpen);
|
||||
sessionStorage.setItem(
|
||||
"nodeTableAdvancedOpen",
|
||||
JSON.stringify(newAdvancedOpen)
|
||||
);
|
||||
};
|
||||
|
||||
// Use React Query to fetch epoch rewards
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
@@ -167,48 +74,20 @@ const NodeTableWithAction = () => {
|
||||
|
||||
// Use React Query to fetch Nym nodes
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes = [],
|
||||
isLoading: isNodesLoading,
|
||||
isError: isNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
// Map nodes with rewards data
|
||||
const nsApiNodesData = epochRewardsData
|
||||
? mappedNSApiNodes(nsApiNodes || [], epochRewardsData)
|
||||
: [];
|
||||
|
||||
// Calculate max saturation from all nodes
|
||||
const maxSaturation = Math.max(
|
||||
100,
|
||||
...nsApiNodesData.map((n) => n.stakeSaturation || 0)
|
||||
);
|
||||
|
||||
// Initialize saturation from sessionStorage or set to maxSaturation when data is loaded
|
||||
useEffect(() => {
|
||||
const stored = sessionStorage.getItem("nodeTableSaturation");
|
||||
if (stored) {
|
||||
setSaturation(JSON.parse(stored));
|
||||
} else if (nsApiNodesData.length > 0) {
|
||||
setSaturation([0, maxSaturation]);
|
||||
}
|
||||
}, [maxSaturation, nsApiNodesData.length]);
|
||||
|
||||
// Calculate node counts for each type
|
||||
const nodeCounts = {
|
||||
all: nsApiNodesData.length,
|
||||
mixnodes: nsApiNodesData.filter((node) => node.mixnode).length,
|
||||
gateways: nsApiNodesData.filter((node) => node.gateway).length,
|
||||
};
|
||||
|
||||
// Handle loading state
|
||||
if (isEpochLoading || isNSApiNodesLoading) {
|
||||
if (isEpochLoading || isNodesLoading) {
|
||||
return (
|
||||
<Card sx={{ height: "100%", mt: 5 }}>
|
||||
<CardContent>
|
||||
@@ -222,7 +101,7 @@ const NodeTableWithAction = () => {
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isEpochError || isNSApiNodesError) {
|
||||
if (isEpochError || isNodesError) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
@@ -233,67 +112,14 @@ const NodeTableWithAction = () => {
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
|
||||
if (!epochRewardsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 1: Filter nodes by type
|
||||
const typeFilteredNodes = nsApiNodesData.filter((node) => {
|
||||
switch (activeFilter) {
|
||||
case "mixnodes":
|
||||
return node.mixnode;
|
||||
case "gateways":
|
||||
return node.gateway;
|
||||
case "recommended":
|
||||
return RECOMMENDED_NODES.includes(node.nodeId);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const data = mappedNymNodes(nymNodes || [], epochRewardsData);
|
||||
|
||||
// Step 2: If advanced filters are open, apply them only if sliders are not at default
|
||||
const isDefault = {
|
||||
uptime: uptime[0] === 0 && uptime[1] === 100,
|
||||
saturation: saturation[0] === 0 && saturation[1] === maxSaturation,
|
||||
profitMargin: profitMargin[0] === 0 && profitMargin[1] === 100,
|
||||
};
|
||||
const filteredNodes = advancedOpen
|
||||
? typeFilteredNodes.filter((node) => {
|
||||
const uptimeMatch =
|
||||
isDefault.uptime ||
|
||||
(node.qualityOfService >= uptime[0] &&
|
||||
node.qualityOfService <= uptime[1]);
|
||||
const saturationMatch =
|
||||
isDefault.saturation ||
|
||||
(node.stakeSaturation >= saturation[0] &&
|
||||
node.stakeSaturation <= saturation[1]);
|
||||
const profitMarginMatch =
|
||||
isDefault.profitMargin ||
|
||||
(node.profitMarginPercentage >= profitMargin[0] &&
|
||||
node.profitMarginPercentage <= profitMargin[1]);
|
||||
return uptimeMatch && saturationMatch && profitMarginMatch;
|
||||
})
|
||||
: typeFilteredNodes;
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<AdvancedFilters
|
||||
open={advancedOpen}
|
||||
setOpen={handleAdvancedOpenChange}
|
||||
uptime={uptime}
|
||||
setUptime={handleUptimeChange}
|
||||
saturation={saturation}
|
||||
setSaturation={handleSaturationChange}
|
||||
profitMargin={profitMargin}
|
||||
setProfitMargin={handleProfitMarginChange}
|
||||
maxSaturation={maxSaturation}
|
||||
activeFilter={activeFilter}
|
||||
setActiveFilter={handleActiveFilterChange}
|
||||
nodeCounts={nodeCounts}
|
||||
/>
|
||||
<NodeTable nodes={filteredNodes} />
|
||||
</Stack>
|
||||
);
|
||||
return <NodeTable nodes={data} />;
|
||||
};
|
||||
|
||||
export default NodeTableWithAction;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchNSApiNodes } from "../../app/api";
|
||||
import { fetchObservatoryNodes } from "../../app/api";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import CopyToClipboard from "../copyToClipboard/CopyToClipboard";
|
||||
@@ -14,25 +14,22 @@ type Props = {
|
||||
};
|
||||
|
||||
export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
if (isNSApiNodesLoading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Skeleton variant="text" height={90} />
|
||||
@@ -45,13 +42,10 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!nsApiNodes || isNSApiNodesError) {
|
||||
if (isError || !nymNodes) {
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
@@ -61,21 +55,16 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId
|
||||
);
|
||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
||||
} else {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId)
|
||||
);
|
||||
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
|
||||
const selfBond = nodeInfo.original_pledge
|
||||
? formatBigNum(Number(nodeInfo.original_pledge) / 1_000_000)
|
||||
: 0;
|
||||
const selfBond = formatBigNum(
|
||||
Number(nodeInfo.rewarding_details.operator) / 1_000_000,
|
||||
);
|
||||
const selfBondFormatted = `${selfBond} NYM`;
|
||||
|
||||
return (
|
||||
@@ -96,13 +85,9 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
variant="body4"
|
||||
sx={{ wordWrap: "break-word", maxWidth: "90%" }}
|
||||
>
|
||||
{nodeInfo.bonding_address
|
||||
? nodeInfo.bonding_address
|
||||
: "Node not bonded"}
|
||||
{nodeInfo.bonding_address}
|
||||
</Typography>
|
||||
{nodeInfo.bonding_address && (
|
||||
<CopyToClipboard text={nodeInfo.bonding_address} />
|
||||
)}
|
||||
<CopyToClipboard text={nodeInfo.bonding_address} />
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
@@ -128,14 +113,12 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
}
|
||||
/>
|
||||
|
||||
{nodeInfo.rewarding_details && (
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Nr. of stakers"
|
||||
value={nodeInfo.rewarding_details.unique_delegations.toString()}
|
||||
/>
|
||||
)}
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Nr. of stakers"
|
||||
value={nodeInfo.rewarding_details.unique_delegations.toString()}
|
||||
/>
|
||||
<ExplorerListItem row label="Self bonded" value={selfBondFormatted} />
|
||||
</Stack>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -46,7 +46,6 @@ const DelegationsTable = ({ id }: Props) => {
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
|
||||
const columns: MRT_ColumnDef<NodeRewardDetails>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -56,7 +55,7 @@ const DelegationsTable = ({ id }: Props) => {
|
||||
accessorKey: "height",
|
||||
Cell: ({ row }) => (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="body4">{row.original.block_height}</Typography>
|
||||
<Typography variant="body4">{row.original.height}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
@@ -87,7 +86,7 @@ const DelegationsTable = ({ id }: Props) => {
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
[],
|
||||
);
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NodeDataCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
@@ -30,22 +30,19 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
||||
|
||||
// Fetch node information
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
if (isEpochLoading || isNSApiNodesLoading) {
|
||||
if (isEpochLoading || isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="text" height={50} />
|
||||
@@ -56,13 +53,10 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isEpochError || isNSApiNodesError || !nsApiNodes || !epochRewardsData) {
|
||||
if (isEpochError || isError || !nymNodes || !epochRewardsData) {
|
||||
return (
|
||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
@@ -72,23 +66,17 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId
|
||||
);
|
||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
||||
} else {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId)
|
||||
);
|
||||
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const softwareUpdateTime = nodeInfo.self_description
|
||||
? format(
|
||||
new Date(nodeInfo.self_description.build_information.build_timestamp),
|
||||
"dd/MM/yyyy"
|
||||
)
|
||||
: "N/A";
|
||||
const softwareUpdateTime = format(
|
||||
new Date(nodeInfo.description.build_information.build_timestamp),
|
||||
"dd/MM/yyyy",
|
||||
);
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Nym node data" sx={{ height: "100%" }}>
|
||||
@@ -102,21 +90,13 @@ export const NodeDataCard = ({ paramId }: Props) => {
|
||||
row
|
||||
divider
|
||||
label="Host"
|
||||
value={
|
||||
nodeInfo.self_description
|
||||
? nodeInfo.self_description.host_information.ip_address.toString()
|
||||
: "N/A"
|
||||
}
|
||||
value={nodeInfo.description.host_information.ip_address.toString()}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
divider
|
||||
label="Version"
|
||||
value={
|
||||
nodeInfo.self_description
|
||||
? nodeInfo.self_description.build_information.build_version
|
||||
: "N/A"
|
||||
}
|
||||
value={nodeInfo.description.build_information.build_version}
|
||||
/>
|
||||
<ExplorerListItem
|
||||
row
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import DelegationsTable from "./DelegationsTable";
|
||||
@@ -12,17 +12,15 @@ type Props = {
|
||||
};
|
||||
|
||||
const NodeDelegationsCard = ({ paramId }: Props) => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes,
|
||||
isError,
|
||||
isLoading,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
@@ -30,20 +28,16 @@ const NodeDelegationsCard = ({ paramId }: Props) => {
|
||||
});
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId
|
||||
);
|
||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
||||
} else {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId)
|
||||
);
|
||||
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const id = nodeInfo.node_id;
|
||||
|
||||
if (isNSApiNodesLoading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="text" height={50} />
|
||||
@@ -54,13 +48,10 @@ const NodeDelegationsCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isNSApiNodesError) {
|
||||
if (isError) {
|
||||
return (
|
||||
<ExplorerCard label="Delegations" sx={{ height: "100%" }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load delegations. Please try again later.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { fetchNSApiNodes } from "@/app/api";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { fetchObservatoryNodes } from "@/app/api";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -10,32 +10,27 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function NodePageButtonGroup({ paramId }: Props) {
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
|
||||
const { data: nsApiNodes = [], isError: isNSApiNodesError } = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
const { data: nymNodes, isError } = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (!nsApiNodes || isNSApiNodesError) return null;
|
||||
if (!nymNodes || isError) return null;
|
||||
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId
|
||||
);
|
||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
||||
} else {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId)
|
||||
);
|
||||
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
|
||||
}
|
||||
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
if (nodeInfo.bonding_address)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { formatBigNum } from "@/utils/formatBigNumbers";
|
||||
import { Skeleton, Typography, useTheme } from "@mui/material";
|
||||
import { Skeleton, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||
import type { NS_NODE } from "../../app/api/types";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import type { IObservatoryNode, RewardingDetails } from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
|
||||
@@ -13,9 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
|
||||
// Fetch epoch rewards
|
||||
const {
|
||||
@@ -33,19 +31,19 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
|
||||
// Fetch node information
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
if (isEpochLoading || isNSApiNodesLoading) {
|
||||
if (isEpochLoading || isLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="text" height={50} />
|
||||
@@ -56,13 +54,10 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isEpochError || isNSApiNodesError || !nsApiNodes || !epochRewardsData) {
|
||||
if (isEpochError || isError || !nymNodes || !epochRewardsData) {
|
||||
return (
|
||||
<ExplorerCard label="Node parameters" sx={{ height: "100%" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
@@ -71,13 +66,9 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId
|
||||
);
|
||||
nodeInfo = nymNodes.find((node) => node.identity_key === paramId);
|
||||
} else {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId)
|
||||
);
|
||||
nodeInfo = nymNodes.find((node) => node.node_id === Number(paramId));
|
||||
}
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
@@ -86,25 +77,22 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
const totalStakeFormatted = `${totalStake} NYM`;
|
||||
|
||||
// Extract reward details
|
||||
const rewardDetails: RewardingDetails = nodeInfo.rewarding_details;
|
||||
|
||||
const profitMarginPercent = nodeInfo.rewarding_details
|
||||
? Number(nodeInfo.rewarding_details.cost_params.profit_margin_percent) * 100
|
||||
: 0;
|
||||
const profitMarginPercent =
|
||||
Number(rewardDetails.cost_params.profit_margin_percent) * 100;
|
||||
const profitMarginPercentFormated = `${profitMarginPercent}%`;
|
||||
|
||||
const operatingCosts = nodeInfo.rewarding_details
|
||||
? Number(
|
||||
nodeInfo.rewarding_details.cost_params.interval_operating_cost.amount
|
||||
) / 1_000_000
|
||||
: 0;
|
||||
const operatingCosts =
|
||||
Number(rewardDetails.cost_params.interval_operating_cost.amount) /
|
||||
1_000_000;
|
||||
const operatingCostsFormated = `${operatingCosts.toString()} NYM`;
|
||||
|
||||
const getNodeSaturationPoint = (
|
||||
nodeTotalStake: string,
|
||||
stakeSaturationPoint: string
|
||||
totalStake: number,
|
||||
stakeSaturationPoint: string,
|
||||
): string => {
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
const totalStake = Number.parseFloat(nodeTotalStake);
|
||||
|
||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||
throw new Error("Invalid stake saturation point provided");
|
||||
@@ -117,7 +105,7 @@ export const NodeParametersCard = ({ paramId }: Props) => {
|
||||
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
nodeInfo.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { useChain } from "@cosmos-kit/react";
|
||||
import {
|
||||
Box,
|
||||
@@ -14,7 +14,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RandomAvatar } from "react-random-avatars";
|
||||
import { fetchNSApiNodes } from "../../app/api";
|
||||
import { fetchObservatoryNodes } from "../../app/api";
|
||||
import { COSMOS_KIT_USE_CHAIN } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -31,7 +31,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
const theme = useTheme();
|
||||
|
||||
const { isWalletConnected } = useChain(COSMOS_KIT_USE_CHAIN);
|
||||
@@ -47,12 +47,12 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
|
||||
// Fetch node info
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes,
|
||||
isLoading: isLoadingNymNodes,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
@@ -62,13 +62,9 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
// get node info based on wether it's dentity_key or node_id
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === paramId
|
||||
);
|
||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
||||
} else {
|
||||
nodeInfo = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.node_id === Number(paramId)
|
||||
);
|
||||
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
|
||||
}
|
||||
|
||||
const handleOnSelectStake = useCallback(() => {
|
||||
@@ -100,7 +96,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
}
|
||||
}, [isWalletConnected, nodeInfo]);
|
||||
|
||||
if (isNSApiNodesLoading) {
|
||||
if (isLoadingNymNodes) {
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<Skeleton variant="rectangular" height={80} width={80} />
|
||||
@@ -109,11 +105,11 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
if (isNSApiNodesError || !nsApiNodes) {
|
||||
if (isError || !nymNodes) {
|
||||
return (
|
||||
<ExplorerCard label="Nym Node" sx={{ height: "100%" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
variant="h3"
|
||||
sx={{
|
||||
color: theme.palette.mode === "dark" ? "base.white" : "pine.950",
|
||||
}}
|
||||
@@ -141,7 +137,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
{ nodeId },
|
||||
fee,
|
||||
"Delegation from Nym Explorer V2",
|
||||
uNymFunds
|
||||
uNymFunds,
|
||||
);
|
||||
setSelectedNodeForStaking(undefined);
|
||||
setInfoModalProps({
|
||||
@@ -168,13 +164,12 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const cleanMoniker = DOMPurify.sanitize(nodeInfo.description.moniker).replace(
|
||||
/&/g,
|
||||
"&"
|
||||
);
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
nodeInfo?.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
const cleanDescription = DOMPurify.sanitize(
|
||||
nodeInfo.description.details
|
||||
nodeInfo?.self_description.details,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
// get full country name
|
||||
@@ -202,7 +197,7 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
>
|
||||
{cleanMoniker || "Moniker"}
|
||||
</Typography>
|
||||
{nodeInfo.geoip?.country && (
|
||||
{nodeInfo.description.auxiliary_details.location && (
|
||||
<Box display={"flex"} gap={1}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
@@ -215,8 +210,10 @@ export const NodeProfileCard = ({ paramId }: Props) => {
|
||||
|
||||
<Box>
|
||||
<CountryFlag
|
||||
countryCode={nodeInfo.geoip?.country || ""}
|
||||
countryName={countryName(nodeInfo.geoip?.country || "")}
|
||||
countryCode={nodeInfo.description.auxiliary_details.location}
|
||||
countryName={countryName(
|
||||
nodeInfo.description.auxiliary_details.location,
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { Chip, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { Chip, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
fetchEpochRewards,
|
||||
fetchGatewayStatus,
|
||||
fetchNSApiNodes,
|
||||
fetchObservatoryNodes,
|
||||
} from "../../app/api";
|
||||
import type {
|
||||
IObservatoryNode,
|
||||
LastProbeResult,
|
||||
NodeDescription,
|
||||
NS_NODE,
|
||||
} from "../../app/api/types";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
import ExplorerListItem from "../list/ListItem";
|
||||
@@ -32,7 +32,7 @@ const roleMapping: Record<DeclaredRoleKey, RoleString> = {
|
||||
};
|
||||
|
||||
const getNodeRoles = (
|
||||
declaredRoles: NodeDescriptionNotNull["declared_role"]
|
||||
declaredRoles: NodeDescriptionNotNull["declared_role"],
|
||||
): RoleString[] => {
|
||||
return Object.entries(declaredRoles)
|
||||
.filter(([, isActive]) => isActive)
|
||||
@@ -81,7 +81,7 @@ function calculateConfigScoreStars(probeResult: LastProbeResult): number {
|
||||
|
||||
if (as_entry) {
|
||||
const entryScore = [as_entry.can_connect, as_entry.can_route].filter(
|
||||
Boolean
|
||||
Boolean,
|
||||
).length;
|
||||
|
||||
return entryScore === 2 ? 4 : entryScore === 1 ? 2 : 1;
|
||||
@@ -148,23 +148,21 @@ function calculateWireguardPerformance(probeResult: LastProbeResult): number {
|
||||
}
|
||||
|
||||
export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
let nodeInfo: NS_NODE | undefined;
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
let nodeInfo: IObservatoryNode | undefined;
|
||||
|
||||
// Fetch node info
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const {
|
||||
data: epochRewardsData,
|
||||
isLoading: isEpochLoading,
|
||||
@@ -179,19 +177,19 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
});
|
||||
|
||||
if (paramId.length > 10) {
|
||||
nodeInfo = nsApiNodes.find((node) => node.identity_key === paramId);
|
||||
nodeInfo = nymNodes?.find((node) => node.identity_key === paramId);
|
||||
} else {
|
||||
nodeInfo = nsApiNodes.find((node) => node.node_id === Number(paramId));
|
||||
nodeInfo = nymNodes?.find((node) => node.node_id === Number(paramId));
|
||||
} // Extract node roles once `nodeInfo` is available
|
||||
|
||||
const nodeRoles = nodeInfo?.self_description
|
||||
? getNodeRoles(nodeInfo.self_description.declared_role)
|
||||
const nodeRoles = nodeInfo
|
||||
? getNodeRoles(nodeInfo.description.declared_role)
|
||||
: [];
|
||||
|
||||
// Define whether to fetch gateway status
|
||||
const shouldFetchGatewayStatus = nodeRoles.some((role) =>
|
||||
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role)
|
||||
["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role),
|
||||
);
|
||||
|
||||
// Fetch gateway status only if `shouldFetchGatewayStatus` is true
|
||||
const { data: gatewayStatus } = useQuery({
|
||||
queryKey: ["gatewayStatus", nodeInfo?.identity_key],
|
||||
@@ -202,7 +200,7 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
if (isNSApiNodesLoading || isEpochLoading) {
|
||||
if (isLoading || isEpochLoading) {
|
||||
return (
|
||||
<ExplorerCard label="Node role & performance">
|
||||
<Skeleton variant="text" height={70} />
|
||||
@@ -212,20 +210,15 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isNSApiNodesError || !nsApiNodes || !epochRewardsData || isEpochError) {
|
||||
if (isError || !nymNodes || !epochRewardsData || isEpochError) {
|
||||
return (
|
||||
<ExplorerCard label="Node role & performance">
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
>
|
||||
<Typography variant="h3" sx={{ color: "pine.950" }}>
|
||||
Failed to load node data.
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
|
||||
const NodeRoles = nodeRoles.map((role) => (
|
||||
<Stack key={role} direction="row" gap={1}>
|
||||
@@ -233,6 +226,8 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
</Stack>
|
||||
));
|
||||
|
||||
if (!nodeInfo) return null;
|
||||
|
||||
const qualityOfServiceStars = nodeInfo?.uptime
|
||||
? calculateQualityOfServiceStars(nodeInfo.uptime)
|
||||
: gatewayStatus
|
||||
@@ -252,10 +247,9 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
|
||||
// Function to calculate active set probability
|
||||
const getActiveSetProbability = (
|
||||
nodeTotalStake: string,
|
||||
stakeSaturationPoint: string
|
||||
totalStake: number,
|
||||
stakeSaturationPoint: string,
|
||||
): string => {
|
||||
const totalStake = Number.parseFloat(nodeTotalStake);
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
|
||||
if (Number.isNaN(saturation) || saturation <= 0) {
|
||||
@@ -274,7 +268,7 @@ export const NodeRoleCard = ({ paramId }: Props) => {
|
||||
};
|
||||
const activeSetProb = getActiveSetProbability(
|
||||
nodeInfo.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
import type { NS_NODE } from "@/app/api/types";
|
||||
import { NYM_ACCOUNT_ADDRESS } from "@/app/api/urls";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Search } from "@mui/icons-material";
|
||||
import {
|
||||
Autocomplete,
|
||||
@@ -13,22 +12,22 @@ import {
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { fetchNSApiNodes } from "../../app/api";
|
||||
import { fetchObservatoryNodes } from "../../app/api";
|
||||
import { NYM_ACCOUNT_ADDRESS } from "@/app/api/urls";
|
||||
|
||||
const NodeAndAddressSearch = () => {
|
||||
const router = useRouter();
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchOptions, setSearchOptions] = useState<NS_NODE[]>([]);
|
||||
const [searchOptions, setSearchOptions] = useState<IObservatoryNode[]>([]);
|
||||
|
||||
// Use React Query to fetch nodes
|
||||
|
||||
const { data: nsApiNodes = [], isLoading: isNSApiNodesLoading } = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
const { data: nymNodes = [], isLoading: isLoadingNodes } = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
@@ -67,9 +66,9 @@ const NodeAndAddressSearch = () => {
|
||||
}
|
||||
} else {
|
||||
// Check if it's a node identity key
|
||||
if (nsApiNodes) {
|
||||
const matchingNode = nsApiNodes.find(
|
||||
(node: NS_NODE) => node.identity_key === inputValue
|
||||
if (nymNodes) {
|
||||
const matchingNode = nymNodes.find(
|
||||
(node) => node.identity_key === inputValue
|
||||
);
|
||||
|
||||
if (matchingNode) {
|
||||
@@ -105,8 +104,10 @@ const NodeAndAddressSearch = () => {
|
||||
|
||||
// Filter nodes by moniker if input is not empty
|
||||
if (value.trim() !== "") {
|
||||
const filteredNodes = nsApiNodes.filter((node: NS_NODE) =>
|
||||
node.description.moniker?.toLowerCase().includes(value.toLowerCase())
|
||||
const filteredNodes = nymNodes.filter((node) =>
|
||||
node.self_description?.moniker
|
||||
?.toLowerCase()
|
||||
.includes(value.toLowerCase())
|
||||
);
|
||||
setSearchOptions(filteredNodes);
|
||||
} else {
|
||||
@@ -117,7 +118,7 @@ const NodeAndAddressSearch = () => {
|
||||
// Handle node selection from dropdown
|
||||
const handleNodeSelect = (
|
||||
event: React.SyntheticEvent,
|
||||
value: string | NS_NODE | null
|
||||
value: string | IObservatoryNode | null
|
||||
) => {
|
||||
if (value && typeof value !== "string") {
|
||||
setIsLoading(true); // Show loading spinner
|
||||
@@ -131,9 +132,9 @@ const NodeAndAddressSearch = () => {
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
options={searchOptions}
|
||||
getOptionLabel={(option: string | NS_NODE) => {
|
||||
getOptionLabel={(option: string | IObservatoryNode) => {
|
||||
if (typeof option === "string") return option;
|
||||
return option.description.moniker || "";
|
||||
return option.self_description?.moniker || "";
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
if (typeof option === "string" || typeof value === "string")
|
||||
@@ -145,10 +146,10 @@ const NodeAndAddressSearch = () => {
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
key={`${option.node_id}-${option.description.moniker || ""}`}
|
||||
key={`${option.node_id}-${option.self_description?.moniker || ""}`}
|
||||
style={{ fontSize: "0.875rem" }}
|
||||
>
|
||||
{option.description.moniker || "Unnamed Node"}
|
||||
{option.self_description?.moniker || "Unnamed Node"}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
@@ -181,7 +182,7 @@ const NodeAndAddressSearch = () => {
|
||||
/>
|
||||
)}
|
||||
onChange={handleNodeSelect}
|
||||
loading={isNSApiNodesLoading}
|
||||
loading={isLoadingNodes}
|
||||
loadingText="Loading nodes..."
|
||||
noOptionsText="No nodes found"
|
||||
slotProps={{
|
||||
|
||||
@@ -38,7 +38,6 @@ import StakeModal from "./StakeModal";
|
||||
import type { MappedNymNode, MappedNymNodes } from "./StakeTableWithAction";
|
||||
import { fee } from "./schemas";
|
||||
|
||||
|
||||
type DelegationWithNodeDetails = {
|
||||
node: MappedNymNode | undefined;
|
||||
delegation: Delegation;
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import { Card, CardContent, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { fetchEpochRewards, fetchNSApiNodes } from "../../app/api";
|
||||
import type { ExplorerData, NS_NODE } from "../../app/api/types";
|
||||
import { fetchEpochRewards, fetchObservatoryNodes } from "../../app/api";
|
||||
import type { ExplorerData, IObservatoryNode } from "../../app/api/types";
|
||||
import { countryName } from "../../utils/countryName";
|
||||
import StakeTable from "./StakeTable";
|
||||
|
||||
// Utility function to calculate node saturation point
|
||||
function getNodeSaturationPoint(
|
||||
totalStake: number,
|
||||
stakeSaturationPoint: string
|
||||
stakeSaturationPoint: string,
|
||||
): number {
|
||||
const saturation = Number.parseFloat(stakeSaturationPoint);
|
||||
|
||||
@@ -24,46 +24,35 @@ function getNodeSaturationPoint(
|
||||
}
|
||||
|
||||
// Map nodes with rewards data
|
||||
const mappedNSApiNodes = (
|
||||
nodes: NS_NODE[],
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"]
|
||||
const mappedNymNodes = (
|
||||
nodes: IObservatoryNode[],
|
||||
epochRewardsData: ExplorerData["currentEpochRewardsData"],
|
||||
) =>
|
||||
nodes
|
||||
.map((node) => {
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
+node.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point
|
||||
);
|
||||
nodes.map((node) => {
|
||||
const nodeSaturationPoint = getNodeSaturationPoint(
|
||||
node.total_stake,
|
||||
epochRewardsData.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
const cleanMoniker = DOMPurify.sanitize(node.description.moniker).replace(
|
||||
/&/g,
|
||||
"&"
|
||||
);
|
||||
const cleanMoniker = DOMPurify.sanitize(
|
||||
node.self_description.moniker,
|
||||
).replace(/&/g, "&");
|
||||
|
||||
return {
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.geoip?.country || null,
|
||||
countryName: countryName(node.geoip?.country || null) || null,
|
||||
profitMarginPercentage: node.rewarding_details
|
||||
? +node.rewarding_details.cost_params.profit_margin_percent * 100
|
||||
: 0,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: nodeSaturationPoint,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Handle null country names by putting them at the end
|
||||
if (!a.countryName && !b.countryName) return 0;
|
||||
if (!a.countryName) return 1;
|
||||
if (!b.countryName) return -1;
|
||||
return {
|
||||
name: cleanMoniker,
|
||||
nodeId: node.node_id,
|
||||
identity_key: node.identity_key,
|
||||
countryCode: node.description.auxiliary_details.location || null,
|
||||
countryName:
|
||||
countryName(node.description.auxiliary_details.location) || null,
|
||||
profitMarginPercentage:
|
||||
+node.rewarding_details.cost_params.profit_margin_percent * 100,
|
||||
owner: node.bonding_address,
|
||||
stakeSaturation: +nodeSaturationPoint || 0,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort alphabetically by country name
|
||||
return a.countryName.localeCompare(b.countryName);
|
||||
});
|
||||
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNSApiNodes>;
|
||||
export type MappedNymNodes = ReturnType<typeof mappedNymNodes>;
|
||||
export type MappedNymNode = MappedNymNodes[0];
|
||||
|
||||
const StakeTableWithAction = () => {
|
||||
@@ -83,12 +72,12 @@ const StakeTableWithAction = () => {
|
||||
|
||||
// Use React Query to fetch Nym nodes
|
||||
const {
|
||||
data: nsApiNodes = [],
|
||||
isLoading: isNSApiNodesLoading,
|
||||
isError: isNSApiNodesError,
|
||||
data: nymNodes = [],
|
||||
isLoading: isNodesLoading,
|
||||
isError: isNodesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nsApiNodes"],
|
||||
queryFn: fetchNSApiNodes,
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false, // Prevents unnecessary refetching
|
||||
refetchOnReconnect: false,
|
||||
@@ -96,7 +85,7 @@ const StakeTableWithAction = () => {
|
||||
});
|
||||
|
||||
// Handle loading state
|
||||
if (isEpochLoading || isNSApiNodesLoading) {
|
||||
if (isEpochLoading || isNodesLoading) {
|
||||
return (
|
||||
<Card sx={{ height: "100%", mt: 5 }}>
|
||||
<CardContent>
|
||||
@@ -110,7 +99,7 @@ const StakeTableWithAction = () => {
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isEpochError || isNSApiNodesError) {
|
||||
if (isEpochError || isNodesError) {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Typography variant="h5" sx={{ color: "pine.600", letterSpacing: 0.7 }}>
|
||||
@@ -126,9 +115,9 @@ const StakeTableWithAction = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nsApiNodesData = mappedNSApiNodes(nsApiNodes || [], epochRewardsData);
|
||||
const data = mappedNymNodes(nymNodes || [], epochRewardsData);
|
||||
|
||||
return <StakeTable nodes={nsApiNodesData} />;
|
||||
return <StakeTable nodes={data} />;
|
||||
};
|
||||
|
||||
export default StakeTableWithAction;
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Delegation } from "@nymproject/contract-clients/Mixnet.types";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useState } from "react";
|
||||
import { fetchTotalStakerRewards } from "../../app/api";
|
||||
import type { NodeRewardDetails } from "../../app/api/types";
|
||||
import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from "../../config";
|
||||
import { useNymClient } from "../../hooks/useNymClient";
|
||||
import Loading from "../loading";
|
||||
@@ -18,7 +19,7 @@ import RedeemRewardsModal from "../redeemRewards/RedeemRewardsModal";
|
||||
const fetchDelegations = async (
|
||||
address: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
nymClient: any
|
||||
nymClient: any,
|
||||
): Promise<Delegation[]> => {
|
||||
const data = await nymClient.getDelegatorDelegations({ delegator: address });
|
||||
return data.delegations;
|
||||
@@ -83,10 +84,10 @@ const SubHeaderRowActions = () => {
|
||||
const client = await SigningCosmWasmClient.connectWithSigner(
|
||||
"https://rpc.nymtech.net/",
|
||||
signer,
|
||||
{ gasPrice }
|
||||
{ gasPrice },
|
||||
);
|
||||
|
||||
const messages = delegations.map((delegation: Delegation) => ({
|
||||
const messages = delegations.map((delegation: NodeRewardDetails) => ({
|
||||
contractAddress: NYM_MIXNET_CONTRACT,
|
||||
funds: [],
|
||||
msg: {
|
||||
@@ -100,7 +101,7 @@ const SubHeaderRowActions = () => {
|
||||
address,
|
||||
messages,
|
||||
"auto",
|
||||
"Redeeming all rewards"
|
||||
"Redeeming all rewards",
|
||||
);
|
||||
// Success state
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
"use client";
|
||||
import { Button, ButtonGroup, Stack } from "@mui/material";
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
isSelected: boolean;
|
||||
value: "all" | "mixnodes" | "gateways" | "recommended";
|
||||
};
|
||||
|
||||
type Options = [Option, Option, Option, Option];
|
||||
|
||||
const NodeFilterButtonGroup = ({
|
||||
size = "small",
|
||||
options,
|
||||
onPage,
|
||||
onFilterChange,
|
||||
}: {
|
||||
size?: "small" | "medium" | "large";
|
||||
options: Options;
|
||||
onPage: string;
|
||||
onFilterChange: (
|
||||
filter: "all" | "mixnodes" | "gateways" | "recommended"
|
||||
) => void;
|
||||
}) => {
|
||||
const handleClick = (
|
||||
value: "all" | "mixnodes" | "gateways" | "recommended"
|
||||
) => {
|
||||
if (onPage === value) return;
|
||||
onFilterChange(value);
|
||||
};
|
||||
|
||||
const getMobileButtonStyles = (isSelected: boolean) => ({
|
||||
color: isSelected ? "primary.contrastText" : "text.primary",
|
||||
"&:hover": {
|
||||
bgcolor: isSelected ? "primary.main" : "",
|
||||
},
|
||||
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||
width: "100%",
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
});
|
||||
|
||||
const getDesktopButtonStyles = (isSelected: boolean) => ({
|
||||
color: isSelected ? "primary.contrastText" : "text.primary",
|
||||
"&:hover": {
|
||||
bgcolor: isSelected ? "primary.main" : "",
|
||||
},
|
||||
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile view - Stack */}
|
||||
<Stack
|
||||
spacing={1.5}
|
||||
sx={{
|
||||
display: { xs: "flex", sm: "none" },
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option.label}
|
||||
onClick={() => handleClick(option.value)}
|
||||
sx={getMobileButtonStyles(option.isSelected)}
|
||||
variant="outlined"
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Desktop view - ButtonGroup */}
|
||||
<ButtonGroup size={size} sx={{ display: { xs: "none", sm: "flex" } }}>
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option.label}
|
||||
onClick={() => handleClick(option.value)}
|
||||
sx={getDesktopButtonStyles(option.isSelected)}
|
||||
variant="outlined"
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeFilterButtonGroup;
|
||||
@@ -1,406 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { scaleLinear } from "d3-scale";
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
Geography,
|
||||
ZoomableGroup,
|
||||
} from "react-simple-maps";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { fetchWorldMapCountries } from "@/app/api";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import RemoveIcon from "@mui/icons-material/Remove";
|
||||
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
||||
import { IconButton, Skeleton, Typography, Box } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { CountryDataResponse } from "../../app/api/types";
|
||||
import MAP_TOPOJSON from "../../assets/world-110m.json";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
|
||||
const mapPlaceholderDark = "/explorer/map-placeholder-dark.png";
|
||||
const mapPlaceholderLight = "/explorer/map-placeholder-light.png";
|
||||
|
||||
export const WorldMap = (): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
const [position, setPosition] = React.useState<{
|
||||
coordinates: [number, number];
|
||||
zoom: number;
|
||||
}>({ coordinates: [0, 0], zoom: 1 });
|
||||
|
||||
const {
|
||||
data: {
|
||||
countries = [],
|
||||
totalCountries = 0,
|
||||
uniqueLocations = 0,
|
||||
totalServers = 0,
|
||||
} = {
|
||||
countries: [],
|
||||
totalCountries: 0,
|
||||
uniqueLocations: 0,
|
||||
totalServers: 0,
|
||||
},
|
||||
isLoading: isLoadingCountries,
|
||||
isError: isCountriesError,
|
||||
} = useQuery({
|
||||
queryKey: ["nymNodesCountries"],
|
||||
queryFn: fetchWorldMapCountries,
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const [tooltipContent, setTooltipContent] = React.useState<string>("");
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleMouseLeave = () => setTooltipContent("");
|
||||
return () => {
|
||||
handleMouseLeave();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const colorScale = React.useMemo(() => {
|
||||
if (countries) {
|
||||
const heighestNumberOfNodes = Math.max(
|
||||
...Object.values(countries).map((country) => country.nodes)
|
||||
);
|
||||
return scaleLinear<string, string>()
|
||||
.domain([
|
||||
0,
|
||||
1,
|
||||
heighestNumberOfNodes / 4,
|
||||
heighestNumberOfNodes / 2,
|
||||
heighestNumberOfNodes,
|
||||
])
|
||||
.range(
|
||||
isDarkMode
|
||||
? [
|
||||
theme.palette.pine[950],
|
||||
"#0F5A2E", // Dark green
|
||||
"#147A3D", // Medium green
|
||||
"#1A994C", // Light green
|
||||
theme.palette.accent.main,
|
||||
]
|
||||
: [
|
||||
theme.palette.pine[300],
|
||||
"#0F5A2E", // Dark green
|
||||
"#147A3D", // Medium green
|
||||
"#1A994C", // Light green
|
||||
theme.palette.accent.main,
|
||||
]
|
||||
)
|
||||
.unknown(isDarkMode ? theme.palette.pine[950] : theme.palette.pine[25]);
|
||||
}
|
||||
return () =>
|
||||
isDarkMode ? theme.palette.pine[950] : theme.palette.pine[25];
|
||||
}, [countries, theme.palette.pine, theme.palette.accent, isDarkMode]);
|
||||
|
||||
if (isLoadingCountries) {
|
||||
return (
|
||||
<ExplorerCard label="Nym server locations" sx={{ width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
aspectRatio: "16/7",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={isDarkMode ? mapPlaceholderDark : mapPlaceholderLight}
|
||||
alt="World Map Placeholder"
|
||||
fill
|
||||
style={{ objectFit: "contain" }}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCountriesError) {
|
||||
return (
|
||||
<ExplorerCard label="Nym server locations" sx={{ width: "100%" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
Failed to load data
|
||||
</Typography>
|
||||
<Skeleton variant="text" height={500} />
|
||||
</ExplorerCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExplorerCard
|
||||
label="Nym server locations"
|
||||
sx={{
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
p: { xs: 2, sm: 3 },
|
||||
"& > .MuiCardContent-root": {
|
||||
height: {
|
||||
xs: "200px",
|
||||
sm: "auto",
|
||||
},
|
||||
aspectRatio: {
|
||||
xs: "unset",
|
||||
sm: "16/7",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
margin: "0 auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<ComposableMap
|
||||
data-tip=""
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? "#000000" : theme.palette.pine[25],
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
viewBox="0 0 800 400"
|
||||
projection="geoMercator"
|
||||
projectionConfig={{
|
||||
scale: 130,
|
||||
}}
|
||||
>
|
||||
<ZoomableGroup
|
||||
center={position.coordinates}
|
||||
zoom={position.zoom}
|
||||
minZoom={1}
|
||||
maxZoom={8}
|
||||
translateExtent={[
|
||||
[-800, -400],
|
||||
[800, 400],
|
||||
]}
|
||||
onMoveEnd={({
|
||||
coordinates,
|
||||
zoom,
|
||||
}: {
|
||||
coordinates: [number, number];
|
||||
zoom: number;
|
||||
}) => {
|
||||
setPosition({ coordinates, zoom });
|
||||
}}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Geographies geography={MAP_TOPOJSON}>
|
||||
{({ geographies }: { geographies: GeoJSON.Feature[] }) =>
|
||||
geographies.map((geo) => {
|
||||
const d = Array.isArray(countries)
|
||||
? { nodes: 0 }
|
||||
: (countries as CountryDataResponse)[
|
||||
geo.properties?.ISO_A3 as string
|
||||
] || { nodes: 0 };
|
||||
return (
|
||||
<Geography
|
||||
key={`${geo.properties?.ISO_A3 || ""}-${geo.id}-${
|
||||
geo.properties?.NAME_LONG || ""
|
||||
}`}
|
||||
geography={geo}
|
||||
fill={colorScale(d?.nodes || 0)}
|
||||
stroke={
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.pine[800]
|
||||
: theme.palette.pine[200]
|
||||
}
|
||||
strokeWidth={0.2}
|
||||
data-tooltip-id="map-tooltip"
|
||||
onMouseEnter={() => {
|
||||
const { NAME_LONG } = geo.properties as {
|
||||
NAME_LONG: string;
|
||||
};
|
||||
setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltipContent("");
|
||||
}}
|
||||
style={{
|
||||
hover: countries
|
||||
? {
|
||||
fill: theme.palette.accent.main,
|
||||
outline: "white",
|
||||
cursor: "pointer",
|
||||
}
|
||||
: undefined,
|
||||
default: {
|
||||
outline: "none",
|
||||
},
|
||||
pressed: {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
zIndex: 1000,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(0,0,0,0.5)"
|
||||
: "rgba(255,255,255,0.5)",
|
||||
padding: "4px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPosition((prev) => ({
|
||||
...prev,
|
||||
zoom: Math.min(prev.zoom + 0.5, 8),
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setPosition((prev) => ({
|
||||
...prev,
|
||||
zoom: Math.max(prev.zoom - 0.5, 1),
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RemoveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setPosition({ coordinates: [0, 0], zoom: 1 })}
|
||||
sx={{
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: isDarkMode
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RestartAltIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Box>
|
||||
<Tooltip
|
||||
id="map-tooltip"
|
||||
content={tooltipContent}
|
||||
float={true}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
padding: "4px 8px",
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.pine[800]
|
||||
: theme.palette.pine[200],
|
||||
color:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.base.white
|
||||
: theme.palette.pine[950],
|
||||
borderRadius: "4px",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
/>
|
||||
</ExplorerCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: {
|
||||
xs: "1fr",
|
||||
sm: "repeat(3, 1fr)",
|
||||
},
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<ExplorerCard label="Nym servers around the world">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{totalServers}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
<ExplorerCard label="Countries with Nym servers">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{totalCountries}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
<ExplorerCard label="Nym Server Locations">
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
color: isDarkMode ? "base.white" : "pine.950",
|
||||
letterSpacing: 0.7,
|
||||
}}
|
||||
>
|
||||
{uniqueLocations}
|
||||
</Typography>
|
||||
</ExplorerCard>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,6 @@
|
||||
"iconLight": "explorerCard",
|
||||
"iconDark": "explorerCardDark",
|
||||
"image": "/explorer/images/Network.webp",
|
||||
"link": "https://nym.com/blog/welcome-to-explorer",
|
||||
"overview": {
|
||||
"content": [
|
||||
{
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"iconLight": "stakeCard",
|
||||
"iconDark": "stakeCardDark",
|
||||
"image": "/explorer/images/stake-article.webp",
|
||||
"link": "https://nym.com/blog/stake-Nym-tokens",
|
||||
"overview": {
|
||||
"content": [
|
||||
{
|
||||
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
declare module "*.json" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
declare module "react-simple-maps";
|
||||
-1
@@ -1 +0,0 @@
|
||||
declare module 'react-tooltip';
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.56"
|
||||
version = "1.1.55"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.7-tex] (2025-04-14)
|
||||
|
||||
- Fix the mac build of the wallet ([#5684])
|
||||
- Allow copy and paste on logins fields for the wallet ([#5699])
|
||||
- Tauri V2 - Wallet Migration ([#5687])
|
||||
|
||||
[#5684]: https://github.com/nymtech/nym/pull/5684
|
||||
[#5699]: https://github.com/nymtech/nym/pull/5699
|
||||
[#5687]: https://github.com/nymtech/nym/pull/5687
|
||||
|
||||
## [2025.6-chuckles] (2025-04-01)
|
||||
|
||||
- Wallet-revamp to be in line with new nym-theming ([#5653])
|
||||
|
||||
Generated
+14
-14
@@ -1328,9 +1328,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.14"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -4668,9 +4668,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.71"
|
||||
version = "0.10.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
@@ -4700,9 +4700,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.106"
|
||||
version = "0.9.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
|
||||
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -7079,9 +7079,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint"
|
||||
version = "0.40.2"
|
||||
version = "0.40.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bed57bd840305efef8e83b752dd63dde3bb0fc1c35b6490f0a4de3a0ffd8dd9"
|
||||
checksum = "ab2972a56891bc9173eaebdd51290bc848e448aa281881f3a4712bd8fe1899b3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"digest 0.10.7",
|
||||
@@ -7109,9 +7109,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-config"
|
||||
version = "0.40.2"
|
||||
version = "0.40.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38aeb44e9e275b3afe31a4f3302dfc5aa1e0aa488a22abb7577634b22b144a22"
|
||||
checksum = "651cb680d41e586bb688cc17ab0b6dfe2e62a959c5742d1351fe0b084cd75d59"
|
||||
dependencies = [
|
||||
"flex-error",
|
||||
"serde",
|
||||
@@ -7123,9 +7123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-proto"
|
||||
version = "0.40.2"
|
||||
version = "0.40.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a218320a6e92ac731ac094169cb742e3203a3ab24d30417b72cfdaab5d50c67"
|
||||
checksum = "ca44b9eaaa98e8440fbafe5593b8a32a1c395c094ac566b3eb50497f8bd2bee0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"flex-error",
|
||||
@@ -7138,9 +7138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-rpc"
|
||||
version = "0.40.2"
|
||||
version = "0.40.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93ffcf862b519894f8afa79e5be73e376c3367cbd2bbe0eaf9c653ab01cd8b20"
|
||||
checksum = "9367678af1a9fc6c064fab8b5d05f03c0fc243fe411581c00c7eed83f8ced380"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
|
||||
Reference in New Issue
Block a user