Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03e082725e | |||
| 3e3307887e | |||
| d85683aaa8 | |||
| e89ed985fc | |||
| 45ebd7c37a | |||
| 3506020e55 | |||
| eca77d684b | |||
| dc4353c682 | |||
| e1d8069967 | |||
| 060726b7a3 | |||
| f72ccb0f0d | |||
| 2ff6bfbdd8 | |||
| d4f0b4772b | |||
| 04e652441e | |||
| ccf5990bc7 | |||
| 3d7c9ee2b8 | |||
| bc5bb271d8 | |||
| 158e3cb073 | |||
| 36fb0eba29 | |||
| 1b37e85418 | |||
| 6a93497c8f | |||
| b8ee3465f8 | |||
| a7dfb36a84 | |||
| 14a7b5bdc8 | |||
| ffbd76539a | |||
| bdcc19e86a | |||
| 7929bac685 | |||
| f590aad42c | |||
| ec23f3dcb7 | |||
| 9644eb4329 | |||
| 7a4c6e4ed4 | |||
| d5ad504104 | |||
| d684f6d7ae | |||
| 1ba6444e72 | |||
| dc08b1170a | |||
| f0d9703587 | |||
| c08efef8ed | |||
| 6d29774744 | |||
| af6bab7703 | |||
| 7033f92d82 | |||
| 128dfa6d81 | |||
| 0b0ec075bb | |||
| cca4d21e7c |
@@ -79,6 +79,9 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Build release contracts
|
||||
run: make wasm
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-contracts-') && github.event_name == 'release' }}
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-contracts-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: [self-hosted, custom-runner-linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -19,6 +19,9 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Build release contracts
|
||||
run: make wasm
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
name: CI for Network Explorer API
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
NETWORK: mainnet
|
||||
|
||||
jobs:
|
||||
publish-nym:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-explorer-api-') && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check the release tag starts with `nym-explorer-api-`
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
script: |
|
||||
core.setFailed('Release tag did not start with nym-explorer-api-...')
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Build all explorer-api
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path explorer-api/Cargo.toml --workspace --release
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
target/release/explorer-api
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/explorer-api
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Publish Nym CLI binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
NETWORK: mainnet
|
||||
|
||||
jobs:
|
||||
publish-nym-cli:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-cli-') && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Check the release tag starts with `nym-cli-`
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
script: |
|
||||
core.setFailed('Release tag did not start with nym-cli-...')
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Build binary
|
||||
run: make build-nym-cli
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: nym-cli-${{ matrix.platform }}
|
||||
path: |
|
||||
target/release/nym-cli*
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/nym-cli
|
||||
@@ -10,7 +10,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release' }}
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -10,7 +10,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release' }}
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -16,7 +16,7 @@ env:
|
||||
|
||||
jobs:
|
||||
publish-nym:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release' }}
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -10,7 +10,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release' }}
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -9,7 +9,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release' }}
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -4,6 +4,40 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v1.1.12] (2023-03-07)
|
||||
|
||||
- Fix generated docs for mixnet and vesting contract on docs.rs ([#3093])
|
||||
- Introduce a way of injecting topology into the client ([#3044])
|
||||
- Update mixnet TypeScript client methods #1 ([#2783])
|
||||
- Update tooltips for routing and average score ([#3133])
|
||||
- update selected service provider description style ([#3128])
|
||||
|
||||
[#3093]: https://github.com/nymtech/nym/issues/3093
|
||||
[#3044]: https://github.com/nymtech/nym/issues/3044
|
||||
[#2783]: https://github.com/nymtech/nym/issues/2783
|
||||
[#3133]: https://github.com/nymtech/nym/pull/3133
|
||||
[#3128]: https://github.com/nymtech/nym/pull/3128
|
||||
|
||||
## [v1.1.11] (2023-02-28)
|
||||
|
||||
- Fix empty dealer set loop ([#3105])
|
||||
- The nym-api db.sqlite is broken when trying to run against it it in `enabled-credentials-mode true` there is an ordering issue with migrations when using the credential binary to purchase bandwidth ([#3100])
|
||||
- Feature/latency based gateway selection ([#3081])
|
||||
- Fix the credential binary to handle transactions to sleep when in non-inProgress epochs ([#3057])
|
||||
- Publish mixnet contract to crates.io ([#1919])
|
||||
- Publish vesting contract to crates.io ([#1920])
|
||||
- Feature/update checker to use master ([#3097])
|
||||
- Feature/improve binary checks ([#3094])
|
||||
|
||||
[#3105]: https://github.com/nymtech/nym/issues/3105
|
||||
[#3100]: https://github.com/nymtech/nym/issues/3100
|
||||
[#3081]: https://github.com/nymtech/nym/pull/3081
|
||||
[#3057]: https://github.com/nymtech/nym/issues/3057
|
||||
[#1919]: https://github.com/nymtech/nym/issues/1919
|
||||
[#1920]: https://github.com/nymtech/nym/issues/1920
|
||||
[#3097]: https://github.com/nymtech/nym/pull/3097
|
||||
[#3094]: https://github.com/nymtech/nym/pull/3094
|
||||
|
||||
## [v1.1.10] (2023-02-21)
|
||||
|
||||
- Verloc listener causing mixnode unexpected shutdown ([#3038])
|
||||
|
||||
Generated
+33
-66
@@ -677,7 +677,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "client-core"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dashmap 5.4.0",
|
||||
@@ -705,6 +705,8 @@ dependencies = [
|
||||
"time 0.3.17",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite 0.14.0",
|
||||
"tungstenite 0.13.0",
|
||||
"url",
|
||||
"validator-client",
|
||||
"wasm-bindgen",
|
||||
@@ -1711,31 +1713,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "0.8.1"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9"
|
||||
checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91a4ec26efacf4aeff80887a175a419493cb6f8b5480d26387eb0bd038976187"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"enum-iterator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1800,7 +1782,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "explorer-api"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap 4.1.4",
|
||||
@@ -2205,9 +2187,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.16.1"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
|
||||
checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -2915,9 +2897,9 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.2+1.5.1"
|
||||
version = "0.13.5+1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
||||
checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3279,7 +3261,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.11"
|
||||
version = "1.1.12"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3360,7 +3342,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-bin-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"clap 4.1.4",
|
||||
"clap_complete",
|
||||
@@ -3369,7 +3351,7 @@ dependencies = [
|
||||
"pretty_env_logger",
|
||||
"semver 0.11.0",
|
||||
"serde",
|
||||
"vergen 7.5.1",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3389,7 +3371,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.13.1",
|
||||
@@ -3447,7 +3429,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"clap 4.1.4",
|
||||
"client-core",
|
||||
@@ -3496,7 +3478,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-std",
|
||||
@@ -3508,7 +3490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"aes 0.8.2",
|
||||
"blake3",
|
||||
@@ -3541,7 +3523,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-gateway"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3599,7 +3581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-std",
|
||||
@@ -3618,7 +3600,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnode"
|
||||
version = "1.1.11"
|
||||
version = "1.1.12"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
@@ -3673,7 +3655,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"clap 4.1.4",
|
||||
@@ -3711,7 +3693,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-statistics"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"log",
|
||||
@@ -3755,7 +3737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"pem",
|
||||
]
|
||||
@@ -3774,6 +3756,7 @@ dependencies = [
|
||||
"nym-network-defaults",
|
||||
"nym-sphinx",
|
||||
"nym-task",
|
||||
"nym-topology",
|
||||
"pretty_env_logger",
|
||||
"rand 0.7.3",
|
||||
"tap",
|
||||
@@ -3786,7 +3769,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"clap 4.1.4",
|
||||
"client-core",
|
||||
@@ -3946,7 +3929,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-sphinx-types"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"sphinx-packet",
|
||||
]
|
||||
@@ -3981,6 +3964,7 @@ dependencies = [
|
||||
name = "nym-topology"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bs58",
|
||||
"log",
|
||||
"nym-bin-common",
|
||||
@@ -4030,12 +4014,12 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen 5.1.17",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"nym-contracts-common",
|
||||
@@ -6914,30 +6898,13 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "5.1.17"
|
||||
version = "7.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e"
|
||||
checksum = "447f9238a4553957277b3ee09d80babeae0811f1b3baefb093de1c0448437a37"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"enum-iterator 0.8.1",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
"rustversion",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "7.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"enum-iterator 1.2.0",
|
||||
"enum-iterator",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.63"
|
||||
async-trait = "0.1.64"
|
||||
cfg-if = "1.0.0"
|
||||
dotenv = "0.15.0"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
@@ -126,6 +126,8 @@ fmt-wasm-client:
|
||||
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown
|
||||
wasm-opt -Os contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm -o contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
|
||||
wasm-opt -Os contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm -o contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
|
||||
|
||||
mixnet-opt: wasm
|
||||
cd contracts/mixnet && make opt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "client-core"
|
||||
version = "1.1.10"
|
||||
version = "1.1.12"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
@@ -8,7 +8,7 @@ rust-version = "1.66"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { version = "0.1.58" }
|
||||
async-trait = { workspace = true }
|
||||
dirs = "4.0"
|
||||
dashmap = "5.4.0"
|
||||
futures = "0.3"
|
||||
@@ -20,6 +20,7 @@ serde_json = "1.0.89"
|
||||
tap = "1.0.1"
|
||||
thiserror = "1.0.34"
|
||||
url = { version ="2.2", features = ["serde"] }
|
||||
tungstenite = { version = "0.13.0", default-features = false }
|
||||
tokio = { version = "1.24.1", features = ["macros"]}
|
||||
time = "0.3.17"
|
||||
|
||||
@@ -44,6 +45,9 @@ features = ["time"]
|
||||
version = "1.24.1"
|
||||
features = ["time"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
|
||||
version = "0.14"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
version = "0.6.2"
|
||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
||||
@@ -65,6 +69,7 @@ features = ["futures"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils]
|
||||
path = "../../common/wasm-utils"
|
||||
features = ["websocket"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.time]
|
||||
version = "0.3.17"
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyCon
|
||||
use crate::client::replies::reply_storage::{
|
||||
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
|
||||
};
|
||||
use crate::client::topology_control::nym_api_provider::NymApiTopologyProvider;
|
||||
use crate::client::topology_control::{
|
||||
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
|
||||
};
|
||||
@@ -37,10 +38,12 @@ use nym_sphinx::addressing::nodes::NodeIdentity;
|
||||
use nym_sphinx::receiver::ReconstructedMessage;
|
||||
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tap::TapFallible;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use validator_client::nyxd::CosmWasmClient;
|
||||
|
||||
@@ -90,6 +93,7 @@ impl ClientOutput {
|
||||
pub struct ClientState {
|
||||
pub shared_lane_queue_lengths: LaneQueueLengths,
|
||||
pub reply_controller_sender: ReplyControllerSender,
|
||||
pub topology_accessor: TopologyAccessor,
|
||||
}
|
||||
|
||||
pub enum ClientInputStatus {
|
||||
@@ -154,6 +158,7 @@ pub struct BaseClientBuilder<'a, B, C: Clone> {
|
||||
nym_api_endpoints: Vec<Url>,
|
||||
reply_storage_backend: B,
|
||||
|
||||
custom_topology_provider: Option<Box<dyn TopologyProvider>>,
|
||||
bandwidth_controller: Option<BandwidthController<C>>,
|
||||
key_manager: KeyManager,
|
||||
}
|
||||
@@ -177,6 +182,7 @@ where
|
||||
bandwidth_controller,
|
||||
reply_storage_backend,
|
||||
key_manager,
|
||||
custom_topology_provider: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,11 +201,17 @@ where
|
||||
disabled_credentials: credentials_toggle.is_disabled(),
|
||||
nym_api_endpoints,
|
||||
reply_storage_backend,
|
||||
custom_topology_provider: None,
|
||||
bandwidth_controller,
|
||||
key_manager,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_topology_provider(mut self, provider: Box<dyn TopologyProvider>) -> Self {
|
||||
self.custom_topology_provider = Some(provider);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_mix_recipient(&self) -> Recipient {
|
||||
Recipient::new(
|
||||
*self.key_manager.identity_keypair().public_key(),
|
||||
@@ -304,7 +316,7 @@ where
|
||||
}
|
||||
let gateway_address = self.gateway_config.gateway_listener.clone();
|
||||
if gateway_address.is_empty() {
|
||||
return Err(ClientCoreError::GatwayAddressUnknown);
|
||||
return Err(ClientCoreError::GatewayAddressUnknown);
|
||||
}
|
||||
|
||||
let gateway_identity = identity::PublicKey::from_base58_string(gateway_id)
|
||||
@@ -341,25 +353,38 @@ where
|
||||
Ok(gateway_client)
|
||||
}
|
||||
|
||||
fn setup_topology_provider(
|
||||
custom_provider: Option<Box<dyn TopologyProvider>>,
|
||||
nym_api_urls: Vec<Url>,
|
||||
) -> Box<dyn TopologyProvider> {
|
||||
// if no custom provider was ... provided ..., create one using nym-api
|
||||
custom_provider.unwrap_or_else(|| {
|
||||
Box::new(NymApiTopologyProvider::new(
|
||||
nym_api_urls,
|
||||
env!("CARGO_PKG_VERSION").to_string(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// future responsible for periodically polling directory server and updating
|
||||
// the current global view of topology
|
||||
async fn start_topology_refresher(
|
||||
nym_api_urls: Vec<Url>,
|
||||
topology_provider: Box<dyn TopologyProvider>,
|
||||
refresh_rate: Duration,
|
||||
topology_accessor: TopologyAccessor,
|
||||
shutdown: TaskClient,
|
||||
) -> Result<(), ClientCoreError> {
|
||||
let topology_refresher_config = TopologyRefresherConfig::new(
|
||||
nym_api_urls,
|
||||
refresh_rate,
|
||||
env!("CARGO_PKG_VERSION").to_string(),
|
||||
let topology_refresher_config = TopologyRefresherConfig::new(refresh_rate);
|
||||
|
||||
let mut topology_refresher = TopologyRefresher::new(
|
||||
topology_refresher_config,
|
||||
topology_accessor,
|
||||
topology_provider,
|
||||
);
|
||||
let mut topology_refresher =
|
||||
TopologyRefresher::new(topology_refresher_config, topology_accessor);
|
||||
// before returning, block entire runtime to refresh the current network view so that any
|
||||
// components depending on topology would see a non-empty view
|
||||
info!("Obtaining initial network topology");
|
||||
topology_refresher.refresh().await;
|
||||
topology_refresher.try_refresh().await;
|
||||
|
||||
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
|
||||
log::error!(
|
||||
@@ -468,8 +493,12 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
let topology_provider = Self::setup_topology_provider(
|
||||
self.custom_topology_provider.take(),
|
||||
self.nym_api_endpoints,
|
||||
);
|
||||
Self::start_topology_refresher(
|
||||
self.nym_api_endpoints.clone(),
|
||||
topology_provider,
|
||||
self.debug_config.topology_refresh_rate,
|
||||
shared_topology_accessor.clone(),
|
||||
task_manager.subscribe(),
|
||||
@@ -530,7 +559,7 @@ where
|
||||
self.debug_config,
|
||||
self.key_manager.ack_key(),
|
||||
self_address,
|
||||
shared_topology_accessor,
|
||||
shared_topology_accessor.clone(),
|
||||
sphinx_message_sender,
|
||||
task_manager.subscribe(),
|
||||
);
|
||||
@@ -554,6 +583,7 @@ where
|
||||
client_state: ClientState {
|
||||
shared_lane_queue_lengths,
|
||||
reply_controller_sender,
|
||||
topology_accessor: shared_topology_accessor,
|
||||
},
|
||||
task_manager,
|
||||
})
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::spawn_future;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::params::DEFAULT_NUM_MIX_HOPS;
|
||||
use nym_topology::{nym_topology_from_detailed, NymTopology, NymTopologyError};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use url::Url;
|
||||
|
||||
// I'm extremely curious why compiler NEVER complained about lack of Debug here before
|
||||
#[derive(Debug)]
|
||||
pub struct TopologyAccessorInner(Option<NymTopology>);
|
||||
|
||||
impl AsRef<Option<NymTopology>> for TopologyAccessorInner {
|
||||
fn as_ref(&self) -> &Option<NymTopology> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TopologyAccessorInner {
|
||||
fn new() -> Self {
|
||||
TopologyAccessorInner(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, new: Option<NymTopology>) {
|
||||
self.0 = new;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyReadPermit<'a> {
|
||||
permit: RwLockReadGuard<'a, TopologyAccessorInner>,
|
||||
}
|
||||
|
||||
impl<'a> Deref for TopologyReadPermit<'a> {
|
||||
type Target = TopologyAccessorInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.permit
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TopologyReadPermit<'a> {
|
||||
/// Using provided topology read permit, tries to get an immutable reference to the underlying
|
||||
/// topology. For obvious reasons the lifetime of the topology reference is bound to the permit.
|
||||
pub(super) fn try_get_valid_topology_ref(
|
||||
&'a self,
|
||||
ack_recipient: &Recipient,
|
||||
packet_recipient: Option<&Recipient>,
|
||||
) -> Result<&'a NymTopology, NymTopologyError> {
|
||||
// 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through?
|
||||
let topology = self
|
||||
.permit
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.ok_or(NymTopologyError::EmptyNetworkTopology)?;
|
||||
|
||||
// 2. does it have any mixnode at all?
|
||||
// 3. does it have any gateways at all?
|
||||
// 4. does it have a mixnode on each layer?
|
||||
topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?;
|
||||
|
||||
// 5. does it contain OUR gateway (so that we could create an ack packet)?
|
||||
if !topology.gateway_exists(ack_recipient.gateway()) {
|
||||
return Err(NymTopologyError::NonExistentGatewayError {
|
||||
identity_key: ack_recipient.gateway().to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 6. for our target recipient, does it contain THEIR gateway (so that we could create
|
||||
if let Some(recipient) = packet_recipient {
|
||||
if !topology.gateway_exists(recipient.gateway()) {
|
||||
return Err(NymTopologyError::NonExistentGatewayError {
|
||||
identity_key: recipient.gateway().to_base58_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(topology)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RwLockReadGuard<'a, TopologyAccessorInner>> for TopologyReadPermit<'a> {
|
||||
fn from(read_permit: RwLockReadGuard<'a, TopologyAccessorInner>) -> Self {
|
||||
TopologyReadPermit {
|
||||
permit: read_permit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TopologyAccessor {
|
||||
// `RwLock` *seems to* be the better approach for this as write access is only requested every
|
||||
// few seconds, while reads are needed every single packet generated.
|
||||
// However, proper benchmarks will be needed to determine if `RwLock` is indeed a better
|
||||
// approach than a `Mutex`
|
||||
inner: Arc<RwLock<TopologyAccessorInner>>,
|
||||
}
|
||||
|
||||
impl TopologyAccessor {
|
||||
pub fn new() -> Self {
|
||||
TopologyAccessor {
|
||||
inner: Arc::new(RwLock::new(TopologyAccessorInner::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_read_permit(&self) -> TopologyReadPermit<'_> {
|
||||
self.inner.read().await.into()
|
||||
}
|
||||
|
||||
async fn update_global_topology(&self, new_topology: Option<NymTopology>) {
|
||||
self.inner.write().await.update(new_topology);
|
||||
}
|
||||
|
||||
// only used by the client at startup to get a slightly more reasonable error message
|
||||
// (currently displays as unused because health checker is disabled due to required changes)
|
||||
pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> {
|
||||
match &self.inner.read().await.0 {
|
||||
None => Err(NymTopologyError::EmptyNetworkTopology),
|
||||
Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TopologyAccessor {
|
||||
fn default() -> Self {
|
||||
TopologyAccessor::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyRefresherConfig {
|
||||
nym_api_urls: Vec<Url>,
|
||||
refresh_rate: Duration,
|
||||
client_version: String,
|
||||
}
|
||||
|
||||
impl TopologyRefresherConfig {
|
||||
pub fn new(nym_api_urls: Vec<Url>, refresh_rate: Duration, client_version: String) -> Self {
|
||||
TopologyRefresherConfig {
|
||||
nym_api_urls,
|
||||
refresh_rate,
|
||||
client_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyRefresher {
|
||||
validator_client: validator_client::client::NymApiClient,
|
||||
client_version: String,
|
||||
|
||||
nym_api_urls: Vec<Url>,
|
||||
topology_accessor: TopologyAccessor,
|
||||
refresh_rate: Duration,
|
||||
|
||||
currently_used_api: usize,
|
||||
was_latest_valid: bool,
|
||||
}
|
||||
|
||||
impl TopologyRefresher {
|
||||
pub fn new(mut cfg: TopologyRefresherConfig, topology_accessor: TopologyAccessor) -> Self {
|
||||
cfg.nym_api_urls.shuffle(&mut thread_rng());
|
||||
|
||||
TopologyRefresher {
|
||||
validator_client: validator_client::client::NymApiClient::new(
|
||||
cfg.nym_api_urls[0].clone(),
|
||||
),
|
||||
client_version: cfg.client_version,
|
||||
nym_api_urls: cfg.nym_api_urls,
|
||||
topology_accessor,
|
||||
refresh_rate: cfg.refresh_rate,
|
||||
currently_used_api: 0,
|
||||
was_latest_valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn use_next_nym_api(&mut self) {
|
||||
if self.nym_api_urls.len() == 1 {
|
||||
warn!("There's only a single nym API available - it won't be possible to use a different one");
|
||||
return;
|
||||
}
|
||||
|
||||
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
|
||||
self.validator_client
|
||||
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
|
||||
}
|
||||
|
||||
/// Verifies whether nodes a reasonably distributed among all mix layers.
|
||||
///
|
||||
/// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3.
|
||||
/// However, this is a rather unrealistic expectation, instead we check whether there exists
|
||||
/// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `topology`: active topology constructed from validator api data
|
||||
fn check_layer_distribution(&self, active_topology: &NymTopology) -> bool {
|
||||
let mixes = active_topology.mixes();
|
||||
let mixnodes_count = active_topology.num_mixnodes();
|
||||
|
||||
if active_topology.gateways().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// trivial check to see if have at least a single node on each layer (regardless of active set size)
|
||||
if mixes.get(&1).is_none() || mixes.get(&2).is_none() || mixes.get(&3).is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let upper_bound = (mixnodes_count as f32 * 0.66) as usize;
|
||||
let lower_bound = (mixnodes_count as f32 * 0.15) as usize;
|
||||
|
||||
let layer1 = mixes.get(&1).unwrap().len();
|
||||
let layer2 = mixes.get(&2).unwrap().len();
|
||||
let layer3 = mixes.get(&3).unwrap().len();
|
||||
|
||||
if layer1 < lower_bound || layer1 > upper_bound {
|
||||
warn!(
|
||||
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
|
||||
mixnodes_count, layer1, layer2, layer3
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if layer2 < lower_bound || layer2 > upper_bound {
|
||||
warn!(
|
||||
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
|
||||
mixnodes_count, layer1, layer2, layer3
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if layer3 < lower_bound || layer3 > upper_bound {
|
||||
warn!(
|
||||
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
|
||||
mixnodes_count, layer1, layer2, layer3
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
async fn get_current_compatible_topology(&self) -> Option<NymTopology> {
|
||||
// TODO: optimization for the future:
|
||||
// only refresh mixnodes on timer and refresh gateways only when
|
||||
// we have to send to a new, unknown, gateway
|
||||
|
||||
let mixnodes = match self.validator_client.get_cached_active_mixnodes().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network mixnodes - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
let gateways = match self.validator_client.get_cached_gateways().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network gateways - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(gateways) => gateways,
|
||||
};
|
||||
|
||||
let topology = nym_topology_from_detailed(mixnodes, gateways)
|
||||
.filter_system_version(&self.client_version);
|
||||
|
||||
if !self.check_layer_distribution(&topology) {
|
||||
warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used.");
|
||||
None
|
||||
} else {
|
||||
Some(topology)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh(&mut self) {
|
||||
trace!("Refreshing the topology");
|
||||
let new_topology = self.get_current_compatible_topology().await;
|
||||
|
||||
if new_topology.is_none() {
|
||||
self.use_next_nym_api();
|
||||
}
|
||||
|
||||
if new_topology.is_none() && self.was_latest_valid {
|
||||
// if we failed to grab this topology, but the one before it was alright, let's assume
|
||||
// validator had a tiny hiccup and use the old data
|
||||
warn!("we're going to keep on using the old topology for this iteration");
|
||||
self.was_latest_valid = false;
|
||||
return;
|
||||
} else if new_topology.is_some() {
|
||||
self.was_latest_valid = true;
|
||||
}
|
||||
|
||||
self.topology_accessor
|
||||
.update_global_topology(new_topology)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> {
|
||||
self.topology_accessor.ensure_is_routable().await
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
|
||||
spawn_future(async move {
|
||||
debug!("Started TopologyRefresher with graceful shutdown support");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
|
||||
self.refresh_rate,
|
||||
));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut interval =
|
||||
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.next() => {
|
||||
self.refresh().await;
|
||||
},
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("TopologyRefresher: Received shutdown");
|
||||
},
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
log::debug!("TopologyRefresher: Exiting");
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::params::DEFAULT_NUM_MIX_HOPS;
|
||||
use nym_topology::{NymTopology, NymTopologyError};
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Notify, RwLock, RwLockReadGuard};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TopologyAccessorInner {
|
||||
controlled_manually: AtomicBool,
|
||||
released_manual_control: Notify,
|
||||
// `RwLock` *seems to* be the better approach for this as write access is only requested every
|
||||
// few seconds, while reads are needed every single packet generated.
|
||||
// However, proper benchmarks will be needed to determine if `RwLock` is indeed a better
|
||||
// approach than a `Mutex`
|
||||
topology: RwLock<Option<NymTopology>>,
|
||||
}
|
||||
|
||||
impl TopologyAccessorInner {
|
||||
fn new() -> Self {
|
||||
TopologyAccessorInner {
|
||||
controlled_manually: AtomicBool::new(false),
|
||||
released_manual_control: Notify::new(),
|
||||
topology: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(&self, new: Option<NymTopology>) {
|
||||
*self.topology.write().await = new;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyReadPermit<'a> {
|
||||
permit: RwLockReadGuard<'a, Option<NymTopology>>,
|
||||
}
|
||||
|
||||
impl<'a> Deref for TopologyReadPermit<'a> {
|
||||
type Target = Option<NymTopology>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.permit
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TopologyReadPermit<'a> {
|
||||
/// Using provided topology read permit, tries to get an immutable reference to the underlying
|
||||
/// topology. For obvious reasons the lifetime of the topology reference is bound to the permit.
|
||||
pub(crate) fn try_get_valid_topology_ref(
|
||||
&'a self,
|
||||
ack_recipient: &Recipient,
|
||||
packet_recipient: Option<&Recipient>,
|
||||
) -> Result<&'a NymTopology, NymTopologyError> {
|
||||
// 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through?
|
||||
let topology = self
|
||||
.permit
|
||||
.as_ref()
|
||||
.ok_or(NymTopologyError::EmptyNetworkTopology)?;
|
||||
|
||||
// 2. does it have any mixnode at all?
|
||||
// 3. does it have any gateways at all?
|
||||
// 4. does it have a mixnode on each layer?
|
||||
topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?;
|
||||
|
||||
// 5. does it contain OUR gateway (so that we could create an ack packet)?
|
||||
if !topology.gateway_exists(ack_recipient.gateway()) {
|
||||
return Err(NymTopologyError::NonExistentGatewayError {
|
||||
identity_key: ack_recipient.gateway().to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 6. for our target recipient, does it contain THEIR gateway (so that we could create
|
||||
if let Some(recipient) = packet_recipient {
|
||||
if !topology.gateway_exists(recipient.gateway()) {
|
||||
return Err(NymTopologyError::NonExistentGatewayError {
|
||||
identity_key: recipient.gateway().to_base58_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(topology)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RwLockReadGuard<'a, Option<NymTopology>>> for TopologyReadPermit<'a> {
|
||||
fn from(read_permit: RwLockReadGuard<'a, Option<NymTopology>>) -> Self {
|
||||
TopologyReadPermit {
|
||||
permit: read_permit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TopologyAccessor {
|
||||
inner: Arc<TopologyAccessorInner>,
|
||||
}
|
||||
|
||||
impl TopologyAccessor {
|
||||
pub fn new() -> Self {
|
||||
TopologyAccessor {
|
||||
inner: Arc::new(TopologyAccessorInner::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn controlled_manually(&self) -> bool {
|
||||
self.inner.controlled_manually.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub async fn get_read_permit(&self) -> TopologyReadPermit<'_> {
|
||||
self.inner.topology.read().await.into()
|
||||
}
|
||||
|
||||
pub(crate) async fn update_global_topology(&self, new_topology: Option<NymTopology>) {
|
||||
self.inner.update(new_topology).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_released_manual_control(&self) {
|
||||
self.inner.released_manual_control.notified().await
|
||||
}
|
||||
|
||||
pub async fn current_topology(&self) -> Option<NymTopology> {
|
||||
self.inner.topology.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn manually_change_topology(&self, new_topology: NymTopology) {
|
||||
self.inner.controlled_manually.store(true, Ordering::SeqCst);
|
||||
self.inner.update(Some(new_topology)).await;
|
||||
}
|
||||
|
||||
pub fn release_manual_control(&self) {
|
||||
self.inner
|
||||
.controlled_manually
|
||||
.store(false, Ordering::SeqCst);
|
||||
self.inner.released_manual_control.notify_waiters();
|
||||
}
|
||||
|
||||
// only used by the client at startup to get a slightly more reasonable error message
|
||||
// (currently displays as unused because health checker is disabled due to required changes)
|
||||
pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> {
|
||||
match self.inner.topology.read().await.deref() {
|
||||
None => Err(NymTopologyError::EmptyNetworkTopology),
|
||||
Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TopologyAccessor {
|
||||
fn default() -> Self {
|
||||
TopologyAccessor::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::spawn_future;
|
||||
pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use nym_topology::NymTopologyError;
|
||||
use std::time::Duration;
|
||||
|
||||
mod accessor;
|
||||
pub(crate) mod nym_api_provider;
|
||||
|
||||
// TODO: move it to config later
|
||||
const MAX_FAILURE_COUNT: usize = 10;
|
||||
|
||||
pub struct TopologyRefresherConfig {
|
||||
refresh_rate: Duration,
|
||||
}
|
||||
|
||||
impl TopologyRefresherConfig {
|
||||
pub fn new(refresh_rate: Duration) -> Self {
|
||||
TopologyRefresherConfig { refresh_rate }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyRefresher {
|
||||
topology_provider: Box<dyn TopologyProvider>,
|
||||
topology_accessor: TopologyAccessor,
|
||||
|
||||
refresh_rate: Duration,
|
||||
consecutive_failure_count: usize,
|
||||
}
|
||||
|
||||
impl TopologyRefresher {
|
||||
pub fn new(
|
||||
cfg: TopologyRefresherConfig,
|
||||
topology_accessor: TopologyAccessor,
|
||||
topology_provider: Box<dyn TopologyProvider>,
|
||||
) -> Self {
|
||||
TopologyRefresher {
|
||||
topology_provider,
|
||||
topology_accessor,
|
||||
refresh_rate: cfg.refresh_rate,
|
||||
consecutive_failure_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_topology_provider(&mut self, provider: Box<dyn TopologyProvider>) {
|
||||
self.topology_provider = provider;
|
||||
}
|
||||
|
||||
pub async fn try_refresh(&mut self) {
|
||||
trace!("Refreshing the topology");
|
||||
|
||||
if self.topology_accessor.controlled_manually() {
|
||||
info!("topology is being controlled manually - we're going to wait until the control is released...");
|
||||
self.topology_accessor
|
||||
.wait_for_released_manual_control()
|
||||
.await;
|
||||
}
|
||||
|
||||
let new_topology = self.topology_provider.get_new_topology().await;
|
||||
if new_topology.is_none() {
|
||||
warn!("failed to obtain new network topology");
|
||||
}
|
||||
|
||||
if new_topology.is_none() && self.consecutive_failure_count < MAX_FAILURE_COUNT {
|
||||
// if we failed to grab this topology, but the one before it was alright, let's assume
|
||||
// validator had a tiny hiccup and use the old data
|
||||
warn!("we're going to keep on using the old topology for this iteration");
|
||||
self.consecutive_failure_count += 1;
|
||||
return;
|
||||
} else if new_topology.is_some() {
|
||||
self.consecutive_failure_count = 0;
|
||||
}
|
||||
|
||||
self.topology_accessor
|
||||
.update_global_topology(new_topology)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> {
|
||||
self.topology_accessor.ensure_is_routable().await
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
|
||||
spawn_future(async move {
|
||||
debug!("Started TopologyRefresher with graceful shutdown support");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
|
||||
self.refresh_rate,
|
||||
));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut interval =
|
||||
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.next() => {
|
||||
self.try_refresh().await;
|
||||
},
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("TopologyRefresher: Received shutdown");
|
||||
},
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
log::debug!("TopologyRefresher: Exiting");
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{error, warn};
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use nym_topology::{nym_topology_from_detailed, NymTopology, NymTopologyError};
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use url::Url;
|
||||
|
||||
pub(crate) struct NymApiTopologyProvider {
|
||||
validator_client: validator_client::client::NymApiClient,
|
||||
nym_api_urls: Vec<Url>,
|
||||
|
||||
client_version: String,
|
||||
currently_used_api: usize,
|
||||
}
|
||||
|
||||
impl NymApiTopologyProvider {
|
||||
pub(crate) fn new(mut nym_api_urls: Vec<Url>, client_version: String) -> Self {
|
||||
nym_api_urls.shuffle(&mut thread_rng());
|
||||
|
||||
NymApiTopologyProvider {
|
||||
validator_client: validator_client::client::NymApiClient::new(nym_api_urls[0].clone()),
|
||||
nym_api_urls,
|
||||
client_version,
|
||||
currently_used_api: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn use_next_nym_api(&mut self) {
|
||||
if self.nym_api_urls.len() == 1 {
|
||||
warn!("There's only a single nym API available - it won't be possible to use a different one");
|
||||
return;
|
||||
}
|
||||
|
||||
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
|
||||
self.validator_client
|
||||
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
|
||||
}
|
||||
|
||||
/// Verifies whether nodes a reasonably distributed among all mix layers.
|
||||
///
|
||||
/// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3.
|
||||
/// However, this is a rather unrealistic expectation, instead we check whether there exists
|
||||
/// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `topology`: active topology constructed from validator api data
|
||||
fn check_layer_distribution(
|
||||
&self,
|
||||
active_topology: &NymTopology,
|
||||
) -> Result<(), NymTopologyError> {
|
||||
let lower_threshold = 0.15;
|
||||
let upper_threshold = 0.66;
|
||||
active_topology.ensure_even_layer_distribution(lower_threshold, upper_threshold)
|
||||
}
|
||||
|
||||
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
|
||||
let mixnodes = match self.validator_client.get_cached_active_mixnodes().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network mixnodes - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
let gateways = match self.validator_client.get_cached_gateways().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network gateways - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(gateways) => gateways,
|
||||
};
|
||||
|
||||
let topology = nym_topology_from_detailed(mixnodes, gateways)
|
||||
.filter_system_version(&self.client_version);
|
||||
|
||||
if let Err(err) = self.check_layer_distribution(&topology) {
|
||||
warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used: {err}");
|
||||
self.use_next_nym_api();
|
||||
None
|
||||
} else {
|
||||
Some(topology)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hehe, wasm
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl TopologyProvider for NymApiTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
self.get_current_compatible_topology().await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl TopologyProvider for NymApiTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
self.get_current_compatible_topology().await
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_config::defaults::NymNetworkDetails;
|
||||
use nym_config::{NymConfig, OptionalSet, DB_FILE_NAME};
|
||||
use nym_config::{NymConfig, OptionalSet, CRED_DB_FILE_NAME};
|
||||
use nym_sphinx::params::PacketSize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
@@ -579,7 +579,7 @@ impl<T: NymConfig> Client<T> {
|
||||
}
|
||||
|
||||
fn default_database_path(id: &str) -> PathBuf {
|
||||
T::default_data_directory(id).join(DB_FILE_NAME)
|
||||
T::default_data_directory(id).join(CRED_DB_FILE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use gateway_client::error::GatewayClientError;
|
||||
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
|
||||
use nym_topology::gateway::GatewayConversionError;
|
||||
use nym_topology::NymTopologyError;
|
||||
use validator_client::ValidatorClientError;
|
||||
|
||||
@@ -53,7 +54,32 @@ pub enum ClientCoreError {
|
||||
GatewayOwnerUnknown,
|
||||
|
||||
#[error("The address of the gateway is unknown - did you run init?")]
|
||||
GatwayAddressUnknown,
|
||||
GatewayAddressUnknown,
|
||||
|
||||
#[error("The gateway is malformed: {source}")]
|
||||
MalformedGateway {
|
||||
#[from]
|
||||
source: GatewayConversionError,
|
||||
},
|
||||
|
||||
#[error("failed to establish connection to gateway: {source}")]
|
||||
GatewayConnectionFailure {
|
||||
#[from]
|
||||
source: tungstenite::Error,
|
||||
},
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[error("failed to establish gateway connection (wasm)")]
|
||||
GatewayJsConnectionFailure,
|
||||
|
||||
#[error("Gateway connection was abruptly closed")]
|
||||
GatewayConnectionAbruptlyClosed,
|
||||
|
||||
#[error("Timed out while trying to establish gateway connection")]
|
||||
GatewayConnectionTimeout,
|
||||
|
||||
#[error("No ping measurements for the gateway ({identity}) performed")]
|
||||
NoGatewayMeasurements { identity: String },
|
||||
|
||||
#[error("failed to register receiver for reconstructed mixnet messages")]
|
||||
FailedToRegisterReceiver,
|
||||
|
||||
@@ -6,52 +6,223 @@ use crate::{
|
||||
config::{persistence::key_pathfinder::ClientKeyPathfinder, Config},
|
||||
error::ClientCoreError,
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use gateway_client::wasm_mockups::SigningNyxdClient;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use gateway_client::GatewayClient;
|
||||
use gateway_requests::registration::handshake::SharedKeys;
|
||||
use log::{debug, info, trace, warn};
|
||||
use nym_config::NymConfig;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_topology::{filter::VersionFilterable, gateway};
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tap::TapFallible;
|
||||
use tungstenite::Message;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::net::TcpStream;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::Instant;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::connect_async;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use validator_client::nyxd::SigningNyxdClient;
|
||||
|
||||
pub(super) async fn query_gateway_details(
|
||||
validator_servers: Vec<Url>,
|
||||
chosen_gateway_id: Option<identity::PublicKey>,
|
||||
) -> Result<gateway::Node, ClientCoreError> {
|
||||
let nym_api = validator_servers
|
||||
.choose(&mut thread_rng())
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type WsConn = WebSocketStream<MaybeTlsStream<TcpStream>>;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use gateway_client::wasm_mockups::SigningNyxdClient;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_timer::Instant;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_utils::websocket::JSWebsocket;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
type WsConn = JSWebsocket;
|
||||
|
||||
const MEASUREMENTS: usize = 3;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const CONN_TIMEOUT: Duration = Duration::from_millis(1500);
|
||||
const PING_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
struct GatewayWithLatency {
|
||||
gateway: gateway::Node,
|
||||
latency: Duration,
|
||||
}
|
||||
|
||||
impl GatewayWithLatency {
|
||||
fn new(gateway: gateway::Node, latency: Duration) -> Self {
|
||||
GatewayWithLatency { gateway, latency }
|
||||
}
|
||||
}
|
||||
|
||||
async fn current_gateways<R: Rng>(
|
||||
rng: &mut R,
|
||||
nym_apis: Vec<Url>,
|
||||
) -> Result<Vec<gateway::Node>, ClientCoreError> {
|
||||
let nym_api = nym_apis
|
||||
.choose(rng)
|
||||
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
|
||||
let validator_client = validator_client::client::NymApiClient::new(nym_api.clone());
|
||||
let client = validator_client::client::NymApiClient::new(nym_api.clone());
|
||||
|
||||
log::trace!("Fetching list of gateways from: {}", nym_api);
|
||||
let gateways = validator_client.get_cached_gateways().await?;
|
||||
|
||||
let gateways = client.get_cached_gateways().await?;
|
||||
let valid_gateways = gateways
|
||||
.into_iter()
|
||||
.filter_map(|gateway| gateway.try_into().ok())
|
||||
.collect::<Vec<gateway::Node>>();
|
||||
|
||||
// we were always filtering by version so I'm not removing that 'feature'
|
||||
let filtered_gateways = valid_gateways.filter_by_version(env!("CARGO_PKG_VERSION"));
|
||||
Ok(filtered_gateways)
|
||||
}
|
||||
|
||||
// if we have chosen particular gateway - use it, otherwise choose a random one.
|
||||
// (remember that in active topology all gateways have at least 100 reputation so should
|
||||
// be working correctly)
|
||||
if let Some(gateway_id) = chosen_gateway_id {
|
||||
filtered_gateways
|
||||
.iter()
|
||||
.find(|gateway| gateway.identity_key == gateway_id)
|
||||
.ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_id.to_string()))
|
||||
.cloned()
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
|
||||
match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await {
|
||||
Err(_elapsed) => Err(ClientCoreError::GatewayConnectionTimeout),
|
||||
Ok(Err(conn_failure)) => Err(conn_failure.into()),
|
||||
Ok(Ok((stream, _))) => Ok(stream),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
|
||||
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
|
||||
}
|
||||
|
||||
async fn measure_latency(gateway: gateway::Node) -> Result<GatewayWithLatency, ClientCoreError> {
|
||||
let addr = gateway.clients_address();
|
||||
trace!(
|
||||
"establishing connection to {} ({addr})...",
|
||||
gateway.identity_key,
|
||||
);
|
||||
let mut stream = connect(&addr).await?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for _ in 0..MEASUREMENTS {
|
||||
let measurement_future = async {
|
||||
let ping_content = vec![1, 2, 3];
|
||||
let start = Instant::now();
|
||||
stream.send(Message::Ping(ping_content.clone())).await?;
|
||||
|
||||
match stream.next().await {
|
||||
Some(Ok(Message::Pong(content))) => {
|
||||
if content == ping_content {
|
||||
let elapsed = Instant::now().duration_since(start);
|
||||
trace!("current ping time: {elapsed:?}");
|
||||
results.push(elapsed);
|
||||
} else {
|
||||
warn!("received a pong message with different content? wtf.")
|
||||
}
|
||||
}
|
||||
Some(Ok(_)) => warn!("received a message that's not a pong!"),
|
||||
Some(Err(err)) => return Err(err.into()),
|
||||
None => return Err(ClientCoreError::GatewayConnectionAbruptlyClosed),
|
||||
}
|
||||
|
||||
Ok::<(), ClientCoreError>(())
|
||||
};
|
||||
|
||||
// thanks to wasm we can't use tokio::time::timeout : (
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let timeout = tokio::time::sleep(PING_TIMEOUT);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
tokio::pin!(timeout);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut timeout = wasm_timer::Delay::new(PING_TIMEOUT);
|
||||
|
||||
tokio::select! {
|
||||
_ = &mut timeout => {
|
||||
warn!("timed out while trying to perform measurement...")
|
||||
}
|
||||
res = measurement_future => res?,
|
||||
}
|
||||
}
|
||||
|
||||
let count = results.len() as u64;
|
||||
if count == 0 {
|
||||
return Err(ClientCoreError::NoGatewayMeasurements {
|
||||
identity: gateway.identity_key.to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let sum: Duration = results.into_iter().sum();
|
||||
let avg = Duration::from_nanos(sum.as_nanos() as u64 / count);
|
||||
|
||||
Ok(GatewayWithLatency::new(gateway, avg))
|
||||
}
|
||||
|
||||
async fn choose_gateway_by_latency<R: Rng>(
|
||||
rng: &mut R,
|
||||
gateways: Vec<gateway::Node>,
|
||||
) -> Result<gateway::Node, ClientCoreError> {
|
||||
info!("choosing gateway by latency...");
|
||||
|
||||
let mut gateways_with_latency = Vec::new();
|
||||
for gateway in gateways {
|
||||
let id = *gateway.identity();
|
||||
trace!("measuring latency to {id}...");
|
||||
let with_latency = match measure_latency(gateway).await {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
warn!("failed to measure {id}: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
"{id} ({}): {:?}",
|
||||
with_latency.gateway.location, with_latency.latency
|
||||
);
|
||||
gateways_with_latency.push(with_latency)
|
||||
}
|
||||
|
||||
let chosen = gateways_with_latency
|
||||
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
|
||||
.expect("invalid selection weight!");
|
||||
|
||||
info!(
|
||||
"chose gateway {} (located at {}) with average latency of {:?}",
|
||||
chosen.gateway.identity_key, chosen.gateway.location, chosen.latency
|
||||
);
|
||||
|
||||
Ok(chosen.gateway.clone())
|
||||
}
|
||||
|
||||
fn uniformly_random_gateway<R: Rng>(
|
||||
rng: &mut R,
|
||||
gateways: Vec<gateway::Node>,
|
||||
) -> Result<gateway::Node, ClientCoreError> {
|
||||
gateways
|
||||
.choose(rng)
|
||||
.ok_or(ClientCoreError::NoGatewaysOnNetwork)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(super) async fn query_gateway_details(
|
||||
validator_servers: Vec<Url>,
|
||||
chosen_gateway_id: Option<identity::PublicKey>,
|
||||
by_latency: bool,
|
||||
) -> Result<gateway::Node, ClientCoreError> {
|
||||
let mut rng = thread_rng();
|
||||
let gateways = current_gateways(&mut rng, validator_servers).await?;
|
||||
|
||||
// if we set an explicit gateway, use that one and nothing else
|
||||
if let Some(explicitly_chosen) = chosen_gateway_id {
|
||||
gateways
|
||||
.into_iter()
|
||||
.find(|gateway| gateway.identity_key == explicitly_chosen)
|
||||
.ok_or_else(|| ClientCoreError::NoGatewayWithId(explicitly_chosen.to_string()))
|
||||
} else if by_latency {
|
||||
choose_gateway_by_latency(&mut rng, gateways).await
|
||||
} else {
|
||||
filtered_gateways
|
||||
.choose(&mut rand::thread_rng())
|
||||
.ok_or(ClientCoreError::NoGatewaysOnNetwork)
|
||||
.cloned()
|
||||
uniformly_random_gateway(&mut rng, gateways)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,9 +77,11 @@ pub async fn register_with_gateway(
|
||||
key_manager: &mut KeyManager,
|
||||
nym_api_endpoints: Vec<Url>,
|
||||
chosen_gateway_id: Option<identity::PublicKey>,
|
||||
by_latency: bool,
|
||||
) -> Result<GatewayEndpointConfig, ClientCoreError> {
|
||||
// Get the gateway details of the gateway we will use
|
||||
let gateway = helpers::query_gateway_details(nym_api_endpoints, chosen_gateway_id).await?;
|
||||
let gateway =
|
||||
helpers::query_gateway_details(nym_api_endpoints, chosen_gateway_id, by_latency).await?;
|
||||
log::debug!("Querying gateway gives: {}", gateway);
|
||||
|
||||
let our_identity = key_manager.identity_keypair();
|
||||
@@ -102,6 +104,7 @@ pub async fn setup_gateway_from_config<C, T>(
|
||||
register_gateway: bool,
|
||||
user_chosen_gateway_id: Option<identity::PublicKey>,
|
||||
config: &Config<T>,
|
||||
by_latency: bool,
|
||||
) -> Result<GatewayEndpointConfig, ClientCoreError>
|
||||
where
|
||||
C: NymConfig + ClientCoreConfigTrait,
|
||||
@@ -117,9 +120,12 @@ where
|
||||
}
|
||||
|
||||
// Else, we preceed by querying the nym-api
|
||||
let gateway =
|
||||
helpers::query_gateway_details(config.get_nym_api_endpoints(), user_chosen_gateway_id)
|
||||
.await?;
|
||||
let gateway = helpers::query_gateway_details(
|
||||
config.get_nym_api_endpoints(),
|
||||
user_chosen_gateway_id,
|
||||
by_latency,
|
||||
)
|
||||
.await?;
|
||||
log::debug!("Querying gateway gives: {}", gateway);
|
||||
|
||||
// If we are not registering, just return this and assume the caller has the keys already and
|
||||
|
||||
@@ -11,7 +11,7 @@ use commands::*;
|
||||
use error::Result;
|
||||
use log::*;
|
||||
use nym_bin_common::completions::fig_generate;
|
||||
use nym_config::{DATA_DIR, DB_FILE_NAME};
|
||||
use nym_config::{CRED_DB_FILE_NAME, DATA_DIR};
|
||||
use nym_network_defaults::{setup_env, NymNetworkDetails};
|
||||
use std::process::exit;
|
||||
use std::time::{Duration, SystemTime};
|
||||
@@ -51,8 +51,11 @@ async fn block_until_coconut_is_available<C: Clone + CosmWasmClient + Send + Syn
|
||||
|
||||
break;
|
||||
} else {
|
||||
// Use 20 additional seconds to avoid the exact moment of going into the final epoch state
|
||||
let secs_until_final = epoch.final_timestamp_secs() + 20 - current_timestamp_secs;
|
||||
// Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries
|
||||
let secs_until_final = epoch
|
||||
.final_timestamp_secs()
|
||||
.saturating_sub(current_timestamp_secs)
|
||||
+ 1;
|
||||
info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final);
|
||||
std::thread::sleep(Duration::from_secs(secs_until_final));
|
||||
}
|
||||
@@ -70,7 +73,10 @@ async fn main() -> Result<()> {
|
||||
|
||||
match args.command {
|
||||
Command::Run(r) => {
|
||||
let db_path = r.client_home_directory.join(DATA_DIR).join(DB_FILE_NAME);
|
||||
let db_path = r
|
||||
.client_home_directory
|
||||
.join(DATA_DIR)
|
||||
.join(CRED_DB_FILE_NAME);
|
||||
let shared_storage = credential_storage::initialise_storage(db_path).await;
|
||||
let recovery_storage = recovery_storage::RecoveryStorage::new(r.recovery_dir)?;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.10"
|
||||
version = "1.1.12"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -101,6 +101,7 @@ impl SocketClient {
|
||||
let ClientState {
|
||||
shared_lane_queue_lengths,
|
||||
reply_controller_sender,
|
||||
..
|
||||
} = client_state;
|
||||
|
||||
let websocket_handler = websocket::HandlerBuilder::new(
|
||||
|
||||
@@ -25,6 +25,11 @@ pub(crate) struct Init {
|
||||
#[clap(long)]
|
||||
gateway: Option<identity::PublicKey>,
|
||||
|
||||
/// Specifies whether the new gateway should be determined based by latency as opposed to being chosen
|
||||
/// uniformly.
|
||||
#[clap(long, conflicts_with = "gateway")]
|
||||
latency_based_selection: bool,
|
||||
|
||||
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
|
||||
/// potentially causing loss of access.
|
||||
#[clap(long)]
|
||||
@@ -143,6 +148,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), ClientError> {
|
||||
register_gateway,
|
||||
user_chosen_gateway_id,
|
||||
config.get_base(),
|
||||
args.latency_based_selection,
|
||||
)
|
||||
.await
|
||||
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.10"
|
||||
version = "1.1.12"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -123,7 +123,7 @@ impl NymClient {
|
||||
|
||||
let ClientState {
|
||||
shared_lane_queue_lengths,
|
||||
reply_controller_sender: _,
|
||||
..
|
||||
} = client_status;
|
||||
|
||||
let authenticator = Authenticator::new(auth_methods, allowed_users);
|
||||
|
||||
@@ -37,6 +37,11 @@ pub(crate) struct Init {
|
||||
#[clap(long)]
|
||||
gateway: Option<identity::PublicKey>,
|
||||
|
||||
/// Specifies whether the new gateway should be determined based by latency as opposed to being chosen
|
||||
/// uniformly.
|
||||
#[clap(long, conflicts_with = "gateway")]
|
||||
latency_based_selection: bool,
|
||||
|
||||
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
|
||||
/// potentially causing loss of access.
|
||||
#[clap(long)]
|
||||
@@ -149,6 +154,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), Socks5ClientError> {
|
||||
register_gateway,
|
||||
user_chosen_gateway_id,
|
||||
config.get_base(),
|
||||
args.latency_based_selection,
|
||||
)
|
||||
.await
|
||||
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-bin-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "Common code for nym binaries"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
@@ -14,7 +14,7 @@ log = { workspace = true }
|
||||
thiserror = "1.0"
|
||||
url = "2.2"
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
async-trait = { version = "0.1.51" }
|
||||
async-trait = { workspace = true }
|
||||
tokio = { version = "1.24.1", features = ["macros"] }
|
||||
|
||||
# internal
|
||||
|
||||
@@ -35,7 +35,7 @@ nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
|
||||
# required for nyxd-client
|
||||
# at some point it might be possible to make it wasm-compatible
|
||||
# perhaps after https://github.com/cosmos/cosmos-rust/pull/97 is resolved (and tendermint-rs is updated)
|
||||
async-trait = { version = "0.1.51", optional = true }
|
||||
async-trait = { workspace = true, optional = true }
|
||||
bip39 = { version = "1", features = ["rand"], optional = true }
|
||||
nym-config = { path = "../../config", optional = true }
|
||||
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", features = ["rpc", "bip32", "cosmwasm"], optional = true}
|
||||
|
||||
@@ -11,9 +11,7 @@ use nym_api_requests::models::{
|
||||
GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
|
||||
RewardEstimationResponse, StakeSaturationResponse,
|
||||
};
|
||||
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::MixId;
|
||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef};
|
||||
pub use nym_mixnet_contract_common::{mixnode::MixNodeDetails, GatewayBond, IdentityKeyRef, MixId};
|
||||
|
||||
#[cfg(feature = "nyxd-client")]
|
||||
use crate::nyxd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
use crate::nyxd::{Gas, GasPrice};
|
||||
pub use cosmrs::Coin as CosmosCoin;
|
||||
pub use cosmwasm_std::Coin as CosmWasmCoin;
|
||||
use cosmwasm_std::{Fraction, Uint128};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::ops::Div;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
pub struct MismatchedDenoms;
|
||||
@@ -19,6 +22,40 @@ pub struct Coin {
|
||||
pub denom: String,
|
||||
}
|
||||
|
||||
impl Div<GasPrice> for Coin {
|
||||
type Output = Gas;
|
||||
|
||||
fn div(self, rhs: GasPrice) -> Self::Output {
|
||||
&self / rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Div<GasPrice> for &'a Coin {
|
||||
type Output = Gas;
|
||||
|
||||
fn div(self, rhs: GasPrice) -> Self::Output {
|
||||
if self.denom != rhs.denom {
|
||||
panic!(
|
||||
"attempted to use two different denoms for gas calculation ({} and {})",
|
||||
self.denom, rhs.denom
|
||||
);
|
||||
}
|
||||
|
||||
// tsk, tsk. somebody tried to divide by zero here!
|
||||
let Some(gas_price_inv) = rhs.amount.inv() else {
|
||||
panic!("attempted to divide by zero!")
|
||||
};
|
||||
|
||||
let implicit_gas_limit = gas_price_inv * Uint128::new(self.amount);
|
||||
if implicit_gas_limit.u128() >= u64::MAX as u128 {
|
||||
u64::MAX
|
||||
} else {
|
||||
implicit_gas_limit.u128() as u64
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Coin {
|
||||
pub fn new<S: Into<String>>(amount: u128, denom: S) -> Self {
|
||||
Coin {
|
||||
@@ -128,3 +165,67 @@ impl CoinConverter for CosmWasmCoin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn division_by_zero_gas_price() {
|
||||
let gas_price: GasPrice = "0unym".parse().unwrap();
|
||||
let amount = Coin::new(123, "unym");
|
||||
let _res = amount / gas_price;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn division_by_gas_price_of_different_denom() {
|
||||
let gas_price: GasPrice = "0.025unyx".parse().unwrap();
|
||||
let amount = Coin::new(123, "unym");
|
||||
let _res = amount / gas_price;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gas_price_division() {
|
||||
let amount = Coin::new(3938, "unym");
|
||||
let gas_price = "0.025unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(157520, res.value());
|
||||
|
||||
let amount = Coin::new(1234567890, "unym");
|
||||
let gas_price = "0.025unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(49382715600, res.value());
|
||||
|
||||
let amount = Coin::new(1, "unym");
|
||||
let gas_price = "0.025unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(40, res.value());
|
||||
|
||||
let amount = Coin::new(150_000_000, "unym");
|
||||
let gas_price = "0.001234unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(121555915721, res.value());
|
||||
|
||||
let amount = Coin::new(150_000_000, "unym");
|
||||
let gas_price = "1unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(150_000_000, res.value());
|
||||
|
||||
let amount = Coin::new(150_000_000, "unym");
|
||||
let gas_price = "1234.56unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(121500, res.value());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gas_price_division_identity() {
|
||||
let amount = Coin::new(1234567890, "unym");
|
||||
let gas_price: GasPrice = "0.025unym".parse().unwrap();
|
||||
let res1 = (&amount) / gas_price.clone();
|
||||
let res2 = &gas_price * res1;
|
||||
|
||||
assert_eq!(amount, Coin::from(res2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::Coin;
|
||||
use crate::nyxd::Gas;
|
||||
use crate::nyxd::{Coin, GasPrice};
|
||||
use cosmrs::{tx, AccountId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
@@ -64,6 +64,12 @@ impl Display for Fee {
|
||||
}
|
||||
|
||||
impl Fee {
|
||||
pub fn manual_with_gas_price(fee: Coin, gas_price: GasPrice) -> Self {
|
||||
let gas_limit = &fee / gas_price;
|
||||
|
||||
Fee::Manual(tx::Fee::from_amount_and_gas(fee.into(), gas_limit))
|
||||
}
|
||||
|
||||
pub fn new_payer_granter_auto(
|
||||
gas_adjustment: Option<GasAdjustment>,
|
||||
payer: Option<AccountId>,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub mod defaults;
|
||||
|
||||
pub const CONFIG_DIR: &str = "config";
|
||||
pub const DATA_DIR: &str = "data";
|
||||
pub const DB_FILE_NAME: &str = "db.sqlite";
|
||||
pub const CRED_DB_FILE_NAME: &str = "credentials_database.db";
|
||||
|
||||
pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
fn template() -> &'static str;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-contracts-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "Common library for Nym cosmwasm contracts"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "Common library for the Nym mixnet contract"
|
||||
rust-version = "1.62"
|
||||
edition = { workspace = true }
|
||||
@@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1"
|
||||
schemars = "0.8"
|
||||
thiserror = "1.0"
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.1.0" }
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.2.0" }
|
||||
serde_json = "1.0.0"
|
||||
humantime-serde = "1.1.1"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "Common library for the Nym vesting contract"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
@@ -9,8 +9,8 @@ repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0"
|
||||
mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.1.0" }
|
||||
mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.2.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
ts-rs = {version = "6.1.2", optional = true}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { version = "0.1.51" }
|
||||
async-trait = { workspace = true }
|
||||
|
||||
log = { workspace = true }
|
||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-crypto"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "Crypto library for the nym mixnet"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
@@ -26,8 +26,8 @@ subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
|
||||
thiserror = "1.0.37"
|
||||
|
||||
# internal
|
||||
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.1.0" }
|
||||
nym-pemstore = { path = "../../common/pemstore", version = "0.1.0" }
|
||||
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0" }
|
||||
nym-pemstore = { path = "../../common/pemstore", version = "0.2.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.2"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "rand")]
|
||||
@@ -131,6 +132,14 @@ impl PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PublicKey {
|
||||
type Err = KeyRecoveryError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
PublicKey::from_base58_string(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
||||
@@ -6,6 +6,6 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { version = "0.1.51" }
|
||||
async-trait = { workspace = true }
|
||||
thiserror = "1.0"
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ nym-sphinx-types = { path = "types" }
|
||||
|
||||
# those dependencies are due to intriducing preparer and receiver. Perpaphs that indicates they should be moved
|
||||
# to separate crate?
|
||||
nym-crypto = { path = "../crypto", version = "0.1.0" }
|
||||
nym-crypto = { path = "../crypto", version = "0.2.0" }
|
||||
nym-topology = { path = "../topology" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-sphinx-types"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "Re-export sphinx packet types"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-pemstore"
|
||||
description = "Store private-public keypairs in PEM format"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# List of published common crates
|
||||
nym-bin-common
|
||||
nym-contracts-common
|
||||
nym-crypto
|
||||
nym-mixnet-contract-common
|
||||
nym-pemstore
|
||||
nym-sphinx-types
|
||||
nym-vesting-contract-common
|
||||
@@ -9,7 +9,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { version = "0.1.51" }
|
||||
async-trait = { workspace = true }
|
||||
log = { workspace = true }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::error::StatsError;
|
||||
use crate::StatsMessage;
|
||||
|
||||
pub const DEFAULT_STATISTICS_SERVICE_ADDRESS: &str = "127.0.0.1";
|
||||
pub const DEFAULT_STATISTICS_SERVICE_PORT: u16 = 8090;
|
||||
pub const DEFAULT_STATISTICS_SERVICE_PORT: u16 = 8091;
|
||||
|
||||
pub const STATISTICS_SERVICE_VERSION: &str = "/v1";
|
||||
pub const STATISTICS_SERVICE_API_STATISTICS: &str = "statistic";
|
||||
|
||||
@@ -16,6 +16,7 @@ bs58 = "0.4"
|
||||
log = { workspace = true }
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
thiserror = "1.0.37"
|
||||
async-trait = { workspace = true, optional = true }
|
||||
|
||||
## internal
|
||||
nym-crypto = { path = "../crypto" }
|
||||
@@ -23,3 +24,7 @@ nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contra
|
||||
nym-sphinx-addressing = { path = "../nymsphinx/addressing" }
|
||||
nym-sphinx-types = { path = "../nymsphinx/types" }
|
||||
nym-bin-common = { path = "../bin-common" }
|
||||
|
||||
[features]
|
||||
default = ["provider-trait"]
|
||||
provider-trait = ["async-trait"]
|
||||
@@ -20,6 +20,9 @@ pub mod filter;
|
||||
pub mod gateway;
|
||||
pub mod mix;
|
||||
|
||||
#[cfg(feature = "provider-trait")]
|
||||
pub mod provider_trait;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum NymTopologyError {
|
||||
#[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")]
|
||||
@@ -39,6 +42,16 @@ pub enum NymTopologyError {
|
||||
|
||||
#[error("No mixnodes available on layer {layer}")]
|
||||
EmptyMixLayer { layer: MixLayer },
|
||||
|
||||
#[error("Uneven layer distribution. Layer {layer} has {nodes} on it, while we expected a value between {lower_bound} and {upper_bound} as we have {total_nodes} nodes in total. Full breakdown: {layer_distribution:?}")]
|
||||
UnevenLayerDistribution {
|
||||
layer: MixLayer,
|
||||
nodes: usize,
|
||||
lower_bound: usize,
|
||||
upper_bound: usize,
|
||||
total_nodes: usize,
|
||||
layer_distribution: Vec<(MixLayer, usize)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -97,7 +110,7 @@ impl NymTopology {
|
||||
}
|
||||
|
||||
pub fn num_mixnodes(&self) -> usize {
|
||||
self.mixes.values().flat_map(|m| m.iter()).count()
|
||||
self.mixes.values().map(|m| m.len()).sum()
|
||||
}
|
||||
|
||||
pub fn mixes_as_vec(&self) -> Vec<mix::Node> {
|
||||
@@ -238,6 +251,46 @@ impl NymTopology {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_even_layer_distribution(
|
||||
&self,
|
||||
lower_threshold: f32,
|
||||
upper_threshold: f32,
|
||||
) -> Result<(), NymTopologyError> {
|
||||
let mixnodes_count = self.num_mixnodes();
|
||||
|
||||
let layers = self
|
||||
.mixes
|
||||
.iter()
|
||||
.map(|(k, v)| (*k, v.len()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if self.gateways.is_empty() {
|
||||
return Err(NymTopologyError::NoGatewaysAvailable);
|
||||
}
|
||||
|
||||
if layers.is_empty() {
|
||||
return Err(NymTopologyError::NoMixnodesAvailable);
|
||||
}
|
||||
|
||||
let upper_bound = (mixnodes_count as f32 * upper_threshold) as usize;
|
||||
let lower_bound = (mixnodes_count as f32 * lower_threshold) as usize;
|
||||
|
||||
for (layer, nodes) in &layers {
|
||||
if nodes < &lower_bound || nodes > &upper_bound {
|
||||
return Err(NymTopologyError::UnevenLayerDistribution {
|
||||
layer: *layer,
|
||||
nodes: *nodes,
|
||||
lower_bound,
|
||||
upper_bound,
|
||||
total_nodes: mixnodes_count,
|
||||
layer_distribution: layers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter_system_version(&self, expected_version: &str) -> Self {
|
||||
self.filter_node_versions(expected_version)
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
use crate::{filter, NetworkAddress};
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_mixnet_contract_common::{Layer, MixId, MixNodeBond};
|
||||
pub use nym_mixnet_contract_common::Layer;
|
||||
use nym_mixnet_contract_common::{MixId, MixNodeBond};
|
||||
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx_types::Node as SphinxNode;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::NymTopology;
|
||||
pub use async_trait::async_trait;
|
||||
|
||||
// hehe, wasm
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
pub trait TopologyProvider: Send {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology>;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
pub trait TopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology>;
|
||||
}
|
||||
|
||||
pub struct HardcodedTopologyProvider {
|
||||
topology: NymTopology,
|
||||
}
|
||||
|
||||
impl HardcodedTopologyProvider {
|
||||
pub fn new(topology: NymTopology) -> Self {
|
||||
HardcodedTopologyProvider { topology }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl TopologyProvider for HardcodedTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
Some(self.topology.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl TopologyProvider for HardcodedTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
Some(self.topology.clone())
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ pub(crate) mod tests {
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::support::tests::fixtures::dealer_details_fixture;
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::GROUP_MEMBERS;
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, GROUP_MEMBERS};
|
||||
use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cw4::Member;
|
||||
@@ -147,6 +147,8 @@ pub(crate) mod tests {
|
||||
.block
|
||||
.time
|
||||
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
|
||||
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
advance_epoch_state(deps.as_mut(), env).unwrap();
|
||||
|
||||
let ret = try_add_dealer(
|
||||
|
||||
@@ -53,6 +53,7 @@ pub(crate) mod tests {
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::support::tests::fixtures::{dealer_details_fixture, dealing_bytes_fixture};
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::add_fixture_dealer;
|
||||
use coconut_dkg_common::dealer::DealerDetails;
|
||||
use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
@@ -80,6 +81,7 @@ pub(crate) mod tests {
|
||||
.block
|
||||
.time
|
||||
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
advance_epoch_state(deps.as_mut(), env).unwrap();
|
||||
|
||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false)
|
||||
|
||||
@@ -89,23 +89,29 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
|
||||
let current_epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||
let next_epoch = if let Some(state) = current_epoch.state.next() {
|
||||
// We are during DKG process
|
||||
let mut new_state = state;
|
||||
if let EpochState::DealingExchange { resharing } = state {
|
||||
let current_dealers = current_dealers()
|
||||
.keys(deps.storage, None, None, Order::Ascending)
|
||||
.collect::<Result<Vec<Addr>, _>>()?;
|
||||
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
|
||||
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
|
||||
THRESHOLD.save(deps.storage, &threshold)?;
|
||||
if !resharing {
|
||||
let replacement_data = InitialReplacementData {
|
||||
initial_dealers: current_dealers,
|
||||
initial_height: None,
|
||||
};
|
||||
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
|
||||
if current_dealers.is_empty() {
|
||||
// If no dealer registered yet, we just stay in the same state until there's at least one
|
||||
new_state = current_epoch.state;
|
||||
} else {
|
||||
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
|
||||
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
|
||||
THRESHOLD.save(deps.storage, &threshold)?;
|
||||
if !resharing {
|
||||
let replacement_data = InitialReplacementData {
|
||||
initial_dealers: current_dealers,
|
||||
initial_height: None,
|
||||
};
|
||||
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Epoch::new(
|
||||
state,
|
||||
new_state,
|
||||
current_epoch.epoch_id,
|
||||
current_epoch.time_configuration,
|
||||
env.block.time,
|
||||
@@ -392,6 +398,14 @@ pub(crate) mod tests {
|
||||
EarlyEpochStateAdvancement(1)
|
||||
);
|
||||
|
||||
env.block.time = env.block.time.plus_seconds(1);
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
epoch.state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
|
||||
// setup dealer details
|
||||
let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1));
|
||||
for details in all_details.iter() {
|
||||
@@ -404,7 +418,7 @@ pub(crate) mod tests {
|
||||
.may_load(&deps.storage)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
env.block.time = env.block.time.plus_seconds(1);
|
||||
env.block.time = env.block.time.plus_seconds(epoch.time_configuration.public_key_submission_time_secs);
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::instantiate;
|
||||
use crate::dealers::storage::current_dealers;
|
||||
use coconut_dkg_common::msg::InstantiateMsg;
|
||||
use coconut_dkg_common::types::DealerDetails;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
|
||||
use cosmwasm_std::{
|
||||
from_binary, to_binary, ContractResult, Empty, MemoryStorage, OwnedDeps, QuerierResult,
|
||||
SystemResult, WasmQuery,
|
||||
from_binary, to_binary, Addr, ContractResult, DepsMut, Empty, MemoryStorage, OwnedDeps,
|
||||
QuerierResult, SystemResult, WasmQuery,
|
||||
};
|
||||
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
|
||||
use lazy_static::lazy_static;
|
||||
@@ -22,6 +24,22 @@ lazy_static! {
|
||||
pub static ref GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(vec![]);
|
||||
}
|
||||
|
||||
pub fn add_fixture_dealer(deps: DepsMut<'_>) {
|
||||
let owner = Addr::unchecked("owner");
|
||||
current_dealers()
|
||||
.save(
|
||||
deps.storage,
|
||||
&owner,
|
||||
&DealerDetails {
|
||||
address: owner.clone(),
|
||||
bte_public_key_with_proof: String::new(),
|
||||
announce_address: String::new(),
|
||||
assigned_index: 100,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn querier_handler(query: &WasmQuery) -> QuerierResult {
|
||||
let bin = match query {
|
||||
WasmQuery::Smart { contract_addr, msg } => {
|
||||
|
||||
@@ -91,7 +91,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::MULTISIG_CONTRACT;
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, MULTISIG_CONTRACT};
|
||||
use coconut_dkg_common::dealer::DealerDetails;
|
||||
use coconut_dkg_common::types::{EpochState, TimeConfiguration};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
@@ -104,6 +104,7 @@ mod tests {
|
||||
let info = mock_info("requester", &[]);
|
||||
let share = "share".to_string();
|
||||
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
@@ -171,6 +172,7 @@ mod tests {
|
||||
.to_string()
|
||||
}
|
||||
);
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
@@ -247,6 +249,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
@@ -292,6 +295,7 @@ mod tests {
|
||||
let share = "share".to_string();
|
||||
let multisig_info = mock_info(MULTISIG_CONTRACT, &[]);
|
||||
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
|
||||
@@ -22,8 +22,8 @@ name = "mixnet_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.1.0" }
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.2.0" }
|
||||
#nym-config = { path = "../../common/config"}
|
||||
|
||||
cosmwasm-std = "1.0.0"
|
||||
@@ -43,7 +43,7 @@ rand_chacha = "0.2"
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
|
||||
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -4,5 +4,10 @@
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
fn main() {
|
||||
vergen(Config::default()).expect("failed to extract build metadata")
|
||||
let mut config = Config::default();
|
||||
if std::env::var("DOCS_RS").is_ok() {
|
||||
// If we don't have access to git information, such as in a docs.rs build, don't error
|
||||
*config.git_mut().skip_if_error_mut() = true;
|
||||
}
|
||||
vergen(config).expect("failed to extract build metadata");
|
||||
}
|
||||
|
||||
@@ -28,9 +28,13 @@ pub(crate) fn query_contract_version() -> ContractBuildInformation {
|
||||
ContractBuildInformation {
|
||||
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
||||
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
|
||||
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
|
||||
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
|
||||
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
|
||||
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
commit_branch: option_env!("VERGEN_GIT_BRANCH")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ name = "vesting_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
|
||||
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common", version = "0.1.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.1.0" }
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
|
||||
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common", version = "0.2.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.2.0" }
|
||||
|
||||
cosmwasm-std = { version = "1.0.0 "}
|
||||
cw-storage-plus = { version = "0.13.4", features = ["iterator"] }
|
||||
@@ -38,4 +38,4 @@ hex = "0.4.3"
|
||||
serde_json = "1.0.66"
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
|
||||
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
|
||||
|
||||
@@ -4,5 +4,10 @@
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
fn main() {
|
||||
let mut config = Config::default();
|
||||
if std::env::var("DOCS_RS").is_ok() {
|
||||
// If we don't have access to git information, such as in a docs.rs build, don't error
|
||||
*config.git_mut().skip_if_error_mut() = true;
|
||||
}
|
||||
vergen(Config::default()).expect("failed to extract build metadata")
|
||||
}
|
||||
|
||||
@@ -743,9 +743,13 @@ pub fn get_contract_version() -> ContractBuildInformation {
|
||||
ContractBuildInformation {
|
||||
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
||||
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
|
||||
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
|
||||
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
|
||||
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
|
||||
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
commit_branch: option_env!("VERGEN_GIT_BRANCH")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "explorer-api"
|
||||
version = "1.1.10"
|
||||
version = "1.1.12"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::mix_node::http::mix_node_make_default_routes;
|
||||
use crate::mix_nodes::http::mix_nodes_make_default_routes;
|
||||
use crate::overview::http::overview_make_default_routes;
|
||||
use crate::ping::http::ping_make_default_routes;
|
||||
use crate::service_providers::http::service_providers_make_default_routes;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use crate::validators::http::validators_make_default_routes;
|
||||
|
||||
@@ -56,6 +57,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
|
||||
"/overview" => overview_make_default_routes(&openapi_settings),
|
||||
"/ping" => ping_make_default_routes(&openapi_settings),
|
||||
"/validators" => validators_make_default_routes(&openapi_settings),
|
||||
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
|
||||
};
|
||||
|
||||
building_rocket
|
||||
|
||||
@@ -23,6 +23,7 @@ mod mix_node;
|
||||
pub(crate) mod mix_nodes;
|
||||
mod overview;
|
||||
mod ping;
|
||||
pub(crate) mod service_providers;
|
||||
mod state;
|
||||
mod tasks;
|
||||
mod validators;
|
||||
|
||||
@@ -11,7 +11,7 @@ use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
use tokio::sync::RwLock;
|
||||
use validator_client::models::SelectionChance;
|
||||
use validator_client::models::{NodePerformance, SelectionChance};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -36,6 +36,7 @@ pub(crate) struct PrettyDetailedMixNodeBond {
|
||||
pub stake_saturation: f32,
|
||||
pub uncapped_saturation: f32,
|
||||
pub avg_uptime: u8,
|
||||
pub node_performance: NodePerformance,
|
||||
pub estimated_operator_apy: f64,
|
||||
pub estimated_delegators_apy: f64,
|
||||
pub operating_cost: Coin,
|
||||
|
||||
@@ -153,6 +153,7 @@ impl ThreadsafeMixNodesCache {
|
||||
layer: node.mixnode_details.bond_information.layer,
|
||||
mix_node: node.mixnode_details.bond_information.mix_node.clone(),
|
||||
avg_uptime: node.performance.round_to_integer(),
|
||||
node_performance: node.node_performance.clone(),
|
||||
stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32,
|
||||
uncapped_saturation: best_effort_small_dec_to_f64(node.uncapped_stake_saturation)
|
||||
as f32,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::service_providers::models::DirectoryService;
|
||||
use okapi::openapi3::OpenApi;
|
||||
use reqwest::Error as ReqwestError;
|
||||
use rocket::{serde::json::Json, Route};
|
||||
use rocket_okapi::settings::OpenApiSettings;
|
||||
|
||||
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
|
||||
"https://nymtech.net/.wellknown/connect/service-providers.json";
|
||||
|
||||
pub fn service_providers_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![settings: get_service_providers]
|
||||
}
|
||||
|
||||
pub async fn get_services() -> Result<Vec<DirectoryService>, ReqwestError> {
|
||||
reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
|
||||
.await?
|
||||
.json::<Vec<DirectoryService>>()
|
||||
.await
|
||||
}
|
||||
|
||||
#[openapi(tag = "service_providers")]
|
||||
#[get("/")]
|
||||
pub(crate) async fn get_service_providers() -> Json<Vec<DirectoryService>> {
|
||||
let result = get_services().await.unwrap();
|
||||
Json(result)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod models;
|
||||
@@ -0,0 +1,16 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DirectoryServiceProvider {
|
||||
pub id: String,
|
||||
pub description: String,
|
||||
pub address: String,
|
||||
pub gateway: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DirectoryService {
|
||||
pub id: String,
|
||||
pub description: String,
|
||||
pub items: Vec<DirectoryServiceProvider>,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nym/network-explorer",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.7",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -121,4 +121,4 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@ export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`;
|
||||
export const BLOCK_API = `${NYM_API_BASE_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
||||
export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this.
|
||||
export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this
|
||||
export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`;
|
||||
|
||||
// errors
|
||||
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
OVERVIEW_API,
|
||||
UPTIME_STORY_API,
|
||||
VALIDATORS_API,
|
||||
SERVICE_PROVIDERS,
|
||||
} from './constants';
|
||||
|
||||
import {
|
||||
@@ -28,8 +29,8 @@ import {
|
||||
ValidatorsResponse,
|
||||
GatewayBondAnnotated,
|
||||
GatewayBond,
|
||||
DirectoryService,
|
||||
} from '../typeDefs/explorer-api';
|
||||
import { toPercentIntegerString } from '../utils';
|
||||
|
||||
function getFromCache(key: string) {
|
||||
const ts = Number(localStorage.getItem('ts'));
|
||||
@@ -63,6 +64,7 @@ export class Api {
|
||||
if (cachedMixnodes) {
|
||||
return cachedMixnodes;
|
||||
}
|
||||
|
||||
const res = await fetch(MIXNODES_API);
|
||||
const json = await res.json();
|
||||
storeInCache('mixnodes', JSON.stringify(json));
|
||||
@@ -94,9 +96,9 @@ export class Api {
|
||||
static fetchGateways = async (): Promise<GatewayBond[]> => {
|
||||
const res = await fetch(GATEWAYS_API);
|
||||
const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
|
||||
return gatewaysAnnotated.map(({ gateway_bond, performance }) => ({
|
||||
return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
|
||||
...gateway_bond,
|
||||
performance: toPercentIntegerString(performance),
|
||||
node_performance,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -148,4 +150,10 @@ export class Api {
|
||||
|
||||
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
|
||||
|
||||
static fetchServiceProviders = async (): Promise<DirectoryService[]> => {
|
||||
const res = await fetch(SERVICE_PROVIDERS);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
|
||||
@@ -8,14 +7,10 @@ export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltip
|
||||
headingTitle,
|
||||
tooltipInfo,
|
||||
}) => {
|
||||
const [filter, toggleFilter] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleClick = () => {
|
||||
toggleFilter(!filter);
|
||||
};
|
||||
return (
|
||||
<Box alignItems="center" display="flex" onClick={handleClick}>
|
||||
<Box alignItems="center" display="flex">
|
||||
{tooltipInfo && (
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
@@ -27,19 +22,9 @@ export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltip
|
||||
arrow
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
padding: 0,
|
||||
// border: '1px solid red',
|
||||
// minWidth: 300,
|
||||
}}
|
||||
data-testid={headingTitle}
|
||||
>
|
||||
{headingTitle}
|
||||
<Typography variant="body2" fontWeight={600} data-testid={headingTitle}>
|
||||
{headingTitle}
|
||||
</Typography>
|
||||
{filter ? <ExpandMore /> : <ExpandLess />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
|
||||
import {
|
||||
Link,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCellProps,
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
@@ -8,13 +18,13 @@ import { cellStyles } from './Universal-DataGrid';
|
||||
import { unymToNym } from '../utils/currency';
|
||||
import { GatewayEnrichedRowType } from './Gateways';
|
||||
import { MixnodeRowType } from './MixNodes';
|
||||
import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar';
|
||||
|
||||
export type ColumnsType = {
|
||||
field: string;
|
||||
title: string;
|
||||
headerAlign: string;
|
||||
flex?: number;
|
||||
width?: number;
|
||||
headerAlign?: TableCellProps['align'];
|
||||
width?: string | number;
|
||||
tooltipInfo?: string;
|
||||
};
|
||||
|
||||
@@ -50,6 +60,10 @@ function formatCellValues(val: string | number, field: string) {
|
||||
);
|
||||
}
|
||||
|
||||
if (field === 'stake_saturation') {
|
||||
return <StakeSaturationProgressBar value={Number(val)} threshold={100} />;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -61,11 +75,11 @@ export const DetailTable: FCWithChildren<{
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<Table sx={{ minWidth: 1080 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
{columnsData?.map(({ field, title, width, tooltipInfo }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{tooltipInfo && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GatewayResponse, GatewayBond, GatewayReportResponse } from '../typeDefs/explorer-api';
|
||||
import { toPercentIntegerString } from '../utils';
|
||||
|
||||
export type GatewayRowType = {
|
||||
id: string;
|
||||
@@ -8,7 +9,7 @@ export type GatewayRowType = {
|
||||
host: string;
|
||||
location: string;
|
||||
version: string;
|
||||
performance: string;
|
||||
node_performance: string;
|
||||
};
|
||||
|
||||
export type GatewayEnrichedRowType = GatewayRowType & {
|
||||
@@ -29,7 +30,7 @@ export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowTy
|
||||
bond: gw.pledge_amount.amount || 0,
|
||||
host: gw.gateway.host || '',
|
||||
version: gw.gateway.version || '',
|
||||
performance: gw.performance,
|
||||
node_performance: toPercentIntegerString(gw.node_performance.last_24h),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -46,6 +47,6 @@ export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayRe
|
||||
mixPort: gateway.gateway.mix_port || 0,
|
||||
routingScore: `${report.most_recent}%`,
|
||||
avgUptime: `${report.last_day || report.last_hour}%`,
|
||||
performance: gateway.performance,
|
||||
node_performance: toPercentIntegerString(gateway.node_performance.most_recent),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNo
|
||||
const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]);
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Box display="flex" flexDirection="row" width="100%">
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box display="flex" flexDirection={isMobile ? 'column' : 'row'} width="100%">
|
||||
<Box
|
||||
width={72}
|
||||
height={72}
|
||||
@@ -36,14 +36,14 @@ export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNo
|
||||
>
|
||||
<Identicon size={43} string={mixNodeRow.identity_key} palette={palette} />
|
||||
</Box>
|
||||
<Box ml={2}>
|
||||
<Box ml={isMobile ? 0 : 2} mt={isMobile ? 2 : 0}>
|
||||
<Typography fontSize={21}>{mixnodeDescription.name}</Typography>
|
||||
<Typography>{(mixnodeDescription.description || '').slice(0, 1000)}</Typography>
|
||||
<Button
|
||||
component="a"
|
||||
variant="text"
|
||||
sx={{
|
||||
mt: 4,
|
||||
mt: isMobile ? 2 : 4,
|
||||
borderRadius: '30px',
|
||||
fontWeight: 600,
|
||||
padding: 0,
|
||||
@@ -64,15 +64,27 @@ export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNo
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} display="flex" justifyContent="end" mt={isMobile ? 5 : undefined}>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
md={6}
|
||||
display="flex"
|
||||
justifyContent={isMobile ? 'start' : 'end'}
|
||||
mt={isMobile ? 3 : undefined}
|
||||
>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography fontWeight="600" alignSelf="self-end">
|
||||
<Typography fontWeight="600" alignSelf={isMobile ? 'start' : 'self-end'}>
|
||||
Node status:
|
||||
</Typography>
|
||||
<Box mt={2} alignSelf="self-end">
|
||||
<Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}>
|
||||
<MixNodeStatus status={mixNodeRow.status} />
|
||||
</Box>
|
||||
<Typography mt={1} alignSelf="self-end" color={theme.palette.text.secondary} fontSize="smaller">
|
||||
<Typography
|
||||
mt={1}
|
||||
alignSelf={isMobile ? 'start' : 'self-end'}
|
||||
color={theme.palette.text.secondary}
|
||||
fontSize="smaller"
|
||||
>
|
||||
This node is {statusText} in this epoch
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -4,57 +4,48 @@ export const EconomicsInfoColumns: ColumnsType[] = [
|
||||
{
|
||||
field: 'estimatedTotalReward',
|
||||
title: 'Estimated Total Reward',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
width: '15%',
|
||||
tooltipInfo:
|
||||
'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.',
|
||||
},
|
||||
{
|
||||
field: 'estimatedOperatorReward',
|
||||
title: 'Estimated Operator Reward',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
width: '15%',
|
||||
tooltipInfo:
|
||||
"Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.",
|
||||
},
|
||||
{
|
||||
field: 'selectionChance',
|
||||
title: 'Active Set Probability',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
width: '12.5%',
|
||||
tooltipInfo:
|
||||
'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.',
|
||||
},
|
||||
{
|
||||
field: 'stakeSaturation',
|
||||
title: 'Stake Saturation',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 730k NYM, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.',
|
||||
},
|
||||
{
|
||||
field: 'profitMargin',
|
||||
title: 'Profit Margin',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
width: '12.5%',
|
||||
tooltipInfo:
|
||||
'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.',
|
||||
},
|
||||
{
|
||||
field: 'operatingCost',
|
||||
title: 'Operating Cost',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
width: '10%',
|
||||
tooltipInfo:
|
||||
'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.',
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
width: '10%',
|
||||
tooltipInfo: "Mixnode's average routing score in the last 24 hour",
|
||||
},
|
||||
{
|
||||
field: 'nodePerformance',
|
||||
title: 'Routing Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Node’s routing score is relative to that of the network. Each time a node is tested, the test packets have to go through the full path of the network (a gateway + 3 nodes). If a node in the path drop packets it will affect the score of other nodes in the test.',
|
||||
"Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -9,14 +9,14 @@ const parseToNumber = (value: number | undefined | string) =>
|
||||
export const EconomicsProgress: FCWithChildren<
|
||||
LinearProgressProps & {
|
||||
threshold?: number;
|
||||
color: string;
|
||||
}
|
||||
> = ({ threshold, ...props }) => {
|
||||
> = ({ threshold, color, ...props }) => {
|
||||
const theme = useTheme();
|
||||
const { value } = props;
|
||||
|
||||
const valueNumber: number = parseToNumber(value);
|
||||
const thresholdNumber: number = parseToNumber(threshold);
|
||||
const percentageColor = valueNumber > (threshold || 100) ? 'warning' : 'inherit';
|
||||
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
|
||||
|
||||
return (
|
||||
@@ -29,9 +29,9 @@ export const EconomicsProgress: FCWithChildren<
|
||||
<LinearProgress
|
||||
{...props}
|
||||
variant="determinate"
|
||||
color={percentageColor}
|
||||
color={color}
|
||||
value={percentageToDisplay}
|
||||
sx={{ width: '100%', borderRadius: '5px', backgroundColor: theme.palette.common.white }}
|
||||
sx={{ width: '100%', borderRadius: '5px' }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -14,9 +14,7 @@ const row: EconomicsInfoRowWithIndex = {
|
||||
selectionChance: {
|
||||
value: 'High',
|
||||
},
|
||||
avgUptime: {
|
||||
value: '65 %',
|
||||
},
|
||||
|
||||
estimatedOperatorReward: {
|
||||
value: '80000.123456 NYM',
|
||||
},
|
||||
@@ -29,9 +27,11 @@ const row: EconomicsInfoRowWithIndex = {
|
||||
operatingCost: {
|
||||
value: '11121 NYM',
|
||||
},
|
||||
stakeSaturation: {
|
||||
value: '80 %',
|
||||
progressBarValue: 80,
|
||||
avgUptime: {
|
||||
value: '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: '-',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,9 +55,7 @@ const emptyRow: EconomicsInfoRowWithIndex = {
|
||||
value: '-',
|
||||
progressBarValue: 0,
|
||||
},
|
||||
avgUptime: {
|
||||
value: '-',
|
||||
},
|
||||
|
||||
estimatedOperatorReward: {
|
||||
value: '-',
|
||||
},
|
||||
@@ -70,9 +68,11 @@ const emptyRow: EconomicsInfoRowWithIndex = {
|
||||
operatingCost: {
|
||||
value: '-',
|
||||
},
|
||||
stakeSaturation: {
|
||||
avgUptime: {
|
||||
value: '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: '-',
|
||||
progressBarValue: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -14,11 +14,15 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
||||
currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-';
|
||||
const estimatedOperatorRewards =
|
||||
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
|
||||
const stakeSaturation = economicDynamicsStats?.data?.uncapped_saturation || '-';
|
||||
const profitMargin = mixNode?.data?.profit_margin_percent
|
||||
? toPercentIntegerString(mixNode?.data?.profit_margin_percent)
|
||||
: '-';
|
||||
const avgUptime = economicDynamicsStats?.data?.current_interval_uptime;
|
||||
const avgUptime = mixNode?.data?.node_performance
|
||||
? toPercentIntegerString(mixNode?.data?.node_performance.last_24h)
|
||||
: '-';
|
||||
const nodePerformance = mixNode?.data?.node_performance
|
||||
? toPercentIntegerString(mixNode?.data?.node_performance.most_recent)
|
||||
: '-';
|
||||
|
||||
const opCost = mixNode?.data?.operating_cost;
|
||||
|
||||
@@ -33,10 +37,6 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
||||
selectionChance: {
|
||||
value: selectionChance(economicDynamicsStats),
|
||||
},
|
||||
stakeSaturation: {
|
||||
progressBarValue: typeof stakeSaturation === 'number' ? stakeSaturation * 100 : 0,
|
||||
value: typeof stakeSaturation === 'number' ? `${(stakeSaturation * 100).toFixed(2)} %` : '-',
|
||||
},
|
||||
profitMargin: {
|
||||
value: profitMargin ? `${profitMargin} %` : '-',
|
||||
},
|
||||
@@ -46,5 +46,8 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
||||
avgUptime: {
|
||||
value: avgUptime ? `${avgUptime} %` : '-',
|
||||
},
|
||||
nodePerformance: {
|
||||
value: nodePerformance,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { useIsMobile } from '../../../hooks/useIsMobile';
|
||||
import { EconomicsProgress } from './EconomicsProgress';
|
||||
|
||||
export const StakeSaturationProgressBar = ({ value, threshold }: { value: number; threshold: number }) => {
|
||||
const isTablet = useIsMobile('lg');
|
||||
const percentageColor = value > (threshold || 100) ? 'warning' : 'inherit';
|
||||
const textColor = percentageColor === 'warning' ? 'warning.main' : 'nym.wallet.fee';
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }}
|
||||
id="field"
|
||||
color={percentageColor}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
mr: isTablet ? 0 : 1,
|
||||
mb: isTablet ? 1 : 0,
|
||||
fontWeight: '600',
|
||||
fontSize: '12px',
|
||||
color: textColor,
|
||||
}}
|
||||
id="stake-saturation-progress-bar"
|
||||
>
|
||||
{value}%
|
||||
</Typography>
|
||||
<EconomicsProgress value={value} threshold={threshold} color={percentageColor} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,70 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import { useTheme, Theme } from '@mui/material/styles';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types';
|
||||
import { EconomicsProgress } from './EconomicsProgress';
|
||||
import { cellStyles } from '../../Universal-DataGrid';
|
||||
import { UniversalTableProps } from '../../DetailTable';
|
||||
import { useIsMobile } from '../../../hooks/useIsMobile';
|
||||
import { textColour } from '../../../utils';
|
||||
|
||||
const threshold = 100;
|
||||
|
||||
const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => {
|
||||
const progressBarValue = value?.progressBarValue || 0;
|
||||
const fieldValue = value.value;
|
||||
|
||||
if (progressBarValue > 100) {
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
if (field === 'selectionChance') {
|
||||
// TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow
|
||||
switch (fieldValue) {
|
||||
case 'High':
|
||||
case 'VeryHigh':
|
||||
return theme.palette.nym.networkExplorer.selectionChance.overModerate;
|
||||
case 'Good':
|
||||
case 'Moderate':
|
||||
return theme.palette.nym.networkExplorer.selectionChance.moderate;
|
||||
case 'Low':
|
||||
case 'VeryLow':
|
||||
return theme.palette.nym.networkExplorer.selectionChance.underModerate;
|
||||
default:
|
||||
return theme.palette.nym.wallet.fee;
|
||||
}
|
||||
}
|
||||
return theme.palette.nym.wallet.fee;
|
||||
};
|
||||
|
||||
const formatCellValues = (value: EconomicsRowsType, field: string) => {
|
||||
const isTablet = useIsMobile('lg');
|
||||
if (value.progressBarValue) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }} id="field">
|
||||
<Typography
|
||||
sx={{
|
||||
mr: isTablet ? 0 : 1,
|
||||
mb: isTablet ? 1 : 0,
|
||||
fontWeight: '600',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
id={field}
|
||||
>
|
||||
{value.value}
|
||||
</Typography>
|
||||
<EconomicsProgress threshold={threshold} value={value.progressBarValue} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
|
||||
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
|
||||
{value.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
const formatCellValues = (value: EconomicsRowsType, field: string) => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
|
||||
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
|
||||
{value.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({
|
||||
tableName,
|
||||
@@ -78,21 +27,19 @@ export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsIn
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
{columnsData?.map(({ field, title, tooltipInfo, width }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{tooltipInfo && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={field}
|
||||
placement="top-start"
|
||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
</Box>
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={field}
|
||||
placement="top-start"
|
||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
)}
|
||||
{title}
|
||||
</Box>
|
||||
@@ -106,19 +53,11 @@ export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsIn
|
||||
{columnsData?.map((_, index: number) => {
|
||||
const { field } = columnsData[index];
|
||||
const value: EconomicsRowsType = (eachRow as any)[field];
|
||||
|
||||
console.log(value);
|
||||
return (
|
||||
<TableCell
|
||||
key={_.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
...cellStyles,
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: textColour(value, field, theme),
|
||||
}}
|
||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||
|
||||
@@ -3,14 +3,18 @@ export type EconomicsRowsType = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface EconomicsInfoRow {
|
||||
estimatedTotalReward: EconomicsRowsType;
|
||||
estimatedOperatorReward: EconomicsRowsType;
|
||||
selectionChance: EconomicsRowsType;
|
||||
stakeSaturation: EconomicsRowsType;
|
||||
profitMargin: EconomicsRowsType;
|
||||
avgUptime: EconomicsRowsType;
|
||||
operatingCost: EconomicsRowsType;
|
||||
}
|
||||
type TEconomicsInfoProperties =
|
||||
| 'estimatedTotalReward'
|
||||
| 'estimatedOperatorReward'
|
||||
| 'estimatedOperatorReward'
|
||||
| 'selectionChance'
|
||||
| 'profitMargin'
|
||||
| 'avgUptime'
|
||||
| 'nodePerformance'
|
||||
| 'operatingCost';
|
||||
|
||||
export type EconomicsInfoRow = {
|
||||
[k in TEconomicsInfoProperties]: EconomicsRowsType;
|
||||
};
|
||||
|
||||
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
|
||||
|
||||
+6
-4
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api';
|
||||
import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus, NodePerformance } from '../../typeDefs/explorer-api';
|
||||
import { toPercentIntegerString } from '../../utils';
|
||||
import { unymToNym } from '../../utils/currency';
|
||||
|
||||
@@ -17,8 +17,9 @@ export type MixnodeRowType = {
|
||||
layer: string;
|
||||
profit_percentage: string;
|
||||
avg_uptime: string;
|
||||
stake_saturation: number;
|
||||
stake_saturation: React.ReactNode;
|
||||
operating_cost: string;
|
||||
node_performance: NodePerformance['most_recent'];
|
||||
};
|
||||
|
||||
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
|
||||
@@ -46,8 +47,9 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
|
||||
host: item?.mix_node?.host || '',
|
||||
layer: item?.layer || '',
|
||||
profit_percentage: `${profitPercentage}%`,
|
||||
avg_uptime: `${item.avg_uptime}%` || '-',
|
||||
stake_saturation: uncappedSaturation,
|
||||
avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`,
|
||||
stake_saturation: uncappedSaturation.toFixed(2),
|
||||
operating_cost: `${unymToNym(item.operating_cost?.amount, 6)} NYM`,
|
||||
node_performance: `${toPercentIntegerString(item.node_performance.most_recent)}%`,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box, TextField, MenuItem } from '@mui/material';
|
||||
import { Box, TextField, MenuItem, FormControl } from '@mui/material';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import { Filters } from './Filters/Filters';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
@@ -7,10 +7,10 @@ import { useIsMobile } from '../hooks/useIsMobile';
|
||||
const fieldsHeight = '42.25px';
|
||||
|
||||
type TableToolBarProps = {
|
||||
onChangeSearch: (arg: string) => void;
|
||||
onChangeSearch?: (arg: string) => void;
|
||||
onChangePageSize: (event: SelectChangeEvent<string>) => void;
|
||||
pageSize: string;
|
||||
searchTerm: string;
|
||||
searchTerm?: string;
|
||||
withFilters?: boolean;
|
||||
childrenBefore?: React.ReactNode;
|
||||
childrenAfter?: React.ReactNode;
|
||||
@@ -39,41 +39,43 @@ export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
|
||||
<Box sx={{ display: 'flex', flexDirection: isMobile ? 'column-reverse' : 'row', alignItems: 'middle' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', height: fieldsHeight }}>
|
||||
{childrenBefore}
|
||||
<Select
|
||||
value={pageSize}
|
||||
onChange={onChangePageSize}
|
||||
sx={{
|
||||
width: isMobile ? '50%' : 200,
|
||||
marginRight: isMobile ? 0 : 2,
|
||||
}}
|
||||
>
|
||||
<MenuItem value={10} data-testid="ten">
|
||||
10
|
||||
</MenuItem>
|
||||
<MenuItem value={30} data-testid="thirty">
|
||||
30
|
||||
</MenuItem>
|
||||
<MenuItem value={50} data-testid="fifty">
|
||||
50
|
||||
</MenuItem>
|
||||
<MenuItem value={100} data-testid="hundred">
|
||||
100
|
||||
</MenuItem>
|
||||
</Select>
|
||||
<FormControl size="small">
|
||||
<Select
|
||||
value={pageSize}
|
||||
onChange={onChangePageSize}
|
||||
sx={{
|
||||
width: isMobile ? '100%' : 200,
|
||||
marginRight: isMobile ? 0 : 2,
|
||||
}}
|
||||
>
|
||||
<MenuItem value={10} data-testid="ten">
|
||||
10
|
||||
</MenuItem>
|
||||
<MenuItem value={30} data-testid="thirty">
|
||||
30
|
||||
</MenuItem>
|
||||
<MenuItem value={50} data-testid="fifty">
|
||||
50
|
||||
</MenuItem>
|
||||
<MenuItem value={100} data-testid="hundred">
|
||||
100
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<TextField
|
||||
sx={{
|
||||
width: isMobile ? '100%' : 200,
|
||||
marginBottom: isMobile ? 2 : 0,
|
||||
'& > :not(style)': {
|
||||
height: fieldsHeight,
|
||||
},
|
||||
}}
|
||||
value={searchTerm}
|
||||
data-testid="search-box"
|
||||
placeholder="search"
|
||||
onChange={(event) => onChangeSearch(event.target.value)}
|
||||
/>
|
||||
{!!onChangeSearch && (
|
||||
<TextField
|
||||
sx={{
|
||||
width: isMobile ? '100%' : 200,
|
||||
marginBottom: isMobile ? 2 : 0,
|
||||
}}
|
||||
size="small"
|
||||
value={searchTerm}
|
||||
data-testid="search-box"
|
||||
placeholder="search"
|
||||
onChange={(event) => onChangeSearch(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -38,7 +38,7 @@ const CustomPagination = () => {
|
||||
color="primary"
|
||||
count={state.pagination.pageCount}
|
||||
page={state.pagination.page + 1}
|
||||
onChange={(event, value) => apiRef.current.setPage(value - 1)}
|
||||
onChange={(_, value) => apiRef.current.setPage(value - 1)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -62,7 +62,6 @@ export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({ rows, columns
|
||||
}}
|
||||
columns={columns}
|
||||
pageSize={Number(pageSize)}
|
||||
rowsPerPageOptions={[5]}
|
||||
disableSelectionOnClick
|
||||
autoHeight
|
||||
hideFooter={!pagination}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ApiState,
|
||||
BlockResponse,
|
||||
CountryDataResponse,
|
||||
DirectoryService,
|
||||
GatewayResponse,
|
||||
MixNodeResponse,
|
||||
MixnodeStatus,
|
||||
@@ -24,6 +25,7 @@ interface StateData {
|
||||
mode: PaletteMode;
|
||||
navState: NavOptionType[];
|
||||
validators?: ApiState<ValidatorsResponse>;
|
||||
serviceProviders?: ApiState<DirectoryService>;
|
||||
}
|
||||
|
||||
interface StateApi {
|
||||
@@ -63,6 +65,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
|
||||
const [validators, setValidators] = React.useState<ApiState<ValidatorsResponse>>();
|
||||
const [block, setBlock] = React.useState<ApiState<BlockResponse>>();
|
||||
const [countryData, setCountryData] = React.useState<ApiState<CountryDataResponse>>();
|
||||
const [serviceProviders, setServiceProviders] = React.useState<ApiState<DirectoryService>>();
|
||||
|
||||
const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark'));
|
||||
|
||||
@@ -156,6 +159,20 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchServiceProviders = async () => {
|
||||
setServiceProviders({ data: undefined, isLoading: true });
|
||||
try {
|
||||
const [res] = await Api.fetchServiceProviders();
|
||||
setServiceProviders({ data: res, isLoading: false });
|
||||
} catch (error) {
|
||||
setServiceProviders({
|
||||
error: error instanceof Error ? error : new Error('Service provider api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateNavState = (id: number) => {
|
||||
const updated = navState.map((option) => ({
|
||||
...option,
|
||||
@@ -165,7 +182,14 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
Promise.all([fetchOverviewSummary(), fetchGateways(), fetchValidators(), fetchBlock(), fetchCountryData()]);
|
||||
Promise.all([
|
||||
fetchOverviewSummary(),
|
||||
fetchGateways(),
|
||||
fetchValidators(),
|
||||
fetchBlock(),
|
||||
fetchCountryData(),
|
||||
fetchServiceProviders(),
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const state = React.useMemo<State>(
|
||||
@@ -183,8 +207,20 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
|
||||
toggleMode,
|
||||
updateNavState,
|
||||
validators,
|
||||
serviceProviders,
|
||||
}),
|
||||
[block, countryData, gateways, globalError, mixnodes, mode, navState, summaryOverview, validators],
|
||||
[
|
||||
block,
|
||||
countryData,
|
||||
gateways,
|
||||
globalError,
|
||||
mixnodes,
|
||||
mode,
|
||||
navState,
|
||||
summaryOverview,
|
||||
validators,
|
||||
serviceProviders,
|
||||
],
|
||||
);
|
||||
|
||||
return <MainContext.Provider value={state}>{children}</MainContext.Provider>;
|
||||
|
||||
@@ -44,6 +44,11 @@ export const originalNavOptions: NavOptionType[] = [
|
||||
url: `${BIG_DIPPER}/validators`,
|
||||
title: 'Validators',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
url: 'network-components/service-providers',
|
||||
title: 'Service Providers',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,23 +22,20 @@ const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Bond',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
},
|
||||
{
|
||||
field: 'routingScore',
|
||||
field: 'node_performance',
|
||||
title: 'Routing Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.',
|
||||
"Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test",
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo: 'Is the average routing score in the last 24 hours',
|
||||
tooltipInfo: "Gateway's average routing score in the last 24 hours",
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
@@ -50,19 +47,16 @@ const columns: ColumnsType[] = [
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
title: 'Version',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -130,13 +124,13 @@ const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: Gat
|
||||
* Guard component to handle loading and not found states
|
||||
*/
|
||||
const PageGatewayDetailGuard: FCWithChildren = () => {
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond | undefined>();
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>();
|
||||
const { gateways } = useMainContext();
|
||||
const { id } = useParams<{ id: string | undefined }>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (gateways?.data) {
|
||||
setSelectedGateway(gateways.data.find((gateway) => gateway.gateway.identity_key === id));
|
||||
setSelectedGateway(gateways.data.find((g) => g.gateway.identity_key === id));
|
||||
}
|
||||
}, [gateways, id]);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { Link as RRDLink } from 'react-router-dom';
|
||||
import { Box, Button, Card, Grid, Link as MuiLink } from '@mui/material';
|
||||
import { Box, Card, Grid, Link as MuiLink } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import { diff, rcompare } from 'semver';
|
||||
import { diff, gte, rcompare } from 'semver';
|
||||
import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
import { useMainContext } from '../../context/main';
|
||||
import { gatewayToGridRow } from '../../components/Gateways';
|
||||
import { GatewayResponse } from '../../typeDefs/explorer-api';
|
||||
@@ -25,6 +27,8 @@ export const PageGateways: FCWithChildren = () => {
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
const [versionFilter, setVersionFilter] = React.useState<VersionSelectOptions>(VersionSelectOptions.latestVersion);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handleSearch = (str: string) => {
|
||||
setSearchTerm(str.toLowerCase());
|
||||
};
|
||||
@@ -86,10 +90,10 @@ export const PageGateways: FCWithChildren = () => {
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'identity_key',
|
||||
headerName: 'Identity Key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 380,
|
||||
disableColumnMenu: true,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<>
|
||||
@@ -109,35 +113,30 @@ export const PageGateways: FCWithChildren = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
width: 150,
|
||||
type: 'number',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/gateway/${params.row.identityKey}`}
|
||||
data-testid="pledge-amount"
|
||||
>
|
||||
{unymToNym(params.value, 6)}
|
||||
</MuiLink>
|
||||
field: 'node_performance',
|
||||
renderHeader: () => (
|
||||
<>
|
||||
<InfoTooltip
|
||||
id="gateways-list-routing-score"
|
||||
title="Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test"
|
||||
placement="top-start"
|
||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
<CustomColumnHeading headingTitle="Routing Score" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'performance',
|
||||
headerName: 'Routing Score',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Routing Score" />,
|
||||
width: 150,
|
||||
width: 175,
|
||||
disableColumnMenu: true,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/gateway/${params.row.identityKey}`}
|
||||
to={`/network-components/gateway/${params.row.identity_key}`}
|
||||
data-testid="pledge-amount"
|
||||
>
|
||||
{`${params.value}%`}
|
||||
@@ -145,32 +144,38 @@ export const PageGateways: FCWithChildren = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
||||
width: 180,
|
||||
field: 'version',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/gateway/${params.row.identityKey}`}
|
||||
data-testid="host"
|
||||
href={`${NYM_BIG_DIPPER}/account/${params.value}`}
|
||||
target="_blank"
|
||||
data-testid="owner"
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
sortComparator: (a, b) => {
|
||||
if (gte(a, b)) return 1;
|
||||
return -1;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
width: 180,
|
||||
disableColumnMenu: true,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Button
|
||||
<Box
|
||||
onClick={() => handleSearch(params.value as string)}
|
||||
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
|
||||
sx={{ ...cellStyles, justifyContent: 'flex-start', cursor: 'pointer' }}
|
||||
data-testid="location-button"
|
||||
>
|
||||
<Tooltip text={params.value} id="gateway-location-text">
|
||||
@@ -184,7 +189,25 @@ export const PageGateways: FCWithChildren = () => {
|
||||
{params.value}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
||||
width: 180,
|
||||
disableColumnMenu: true,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/gateway/${params.row.identity_key}`}
|
||||
data-testid="host"
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -192,6 +215,7 @@ export const PageGateways: FCWithChildren = () => {
|
||||
headerName: 'Owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 180,
|
||||
disableColumnMenu: true,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
@@ -206,20 +230,21 @@ export const PageGateways: FCWithChildren = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
headerName: 'Version',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
|
||||
field: 'bond',
|
||||
width: 150,
|
||||
headerAlign: 'left',
|
||||
disableColumnMenu: true,
|
||||
type: 'number',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles }}
|
||||
href={`${NYM_BIG_DIPPER}/account/${params.value}`}
|
||||
target="_blank"
|
||||
data-testid="owner"
|
||||
component={RRDLink}
|
||||
to={`/network-components/gateway/${params.row.identity_key}`}
|
||||
data-testid="pledge-amount"
|
||||
>
|
||||
{params.value}
|
||||
{unymToNym(params.value, 6)}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -17,45 +17,45 @@ const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
headerAlign: 'left',
|
||||
width: 230,
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
title: 'Identity Key',
|
||||
headerAlign: 'left',
|
||||
width: 230,
|
||||
width: '15%',
|
||||
},
|
||||
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Stake',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
width: '12.5%',
|
||||
},
|
||||
{
|
||||
field: 'stake_saturation',
|
||||
title: 'Stake Saturation',
|
||||
width: '12.5%',
|
||||
tooltipInfo:
|
||||
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 730k NYM, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.',
|
||||
},
|
||||
{
|
||||
field: 'self_percentage',
|
||||
width: '10%',
|
||||
title: 'Bond %',
|
||||
headerAlign: 'left',
|
||||
width: 99,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'host',
|
||||
width: '10%',
|
||||
title: 'Host',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'layer',
|
||||
title: 'Layer',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -64,11 +64,10 @@ const columns: ColumnsType[] = [
|
||||
*/
|
||||
const PageMixnodeDetailWithState: FCWithChildren = () => {
|
||||
const { mixNode, mixNodeRow, description, stats, status, uptimeStory, uniqDelegations } = useMixnodeContext();
|
||||
|
||||
console.log(mixNodeRow);
|
||||
return (
|
||||
<Box component="main">
|
||||
<Title text="Mixnode Detail" />
|
||||
|
||||
<Grid container spacing={2} mt={1} mb={6}>
|
||||
<Grid item xs={12}>
|
||||
{mixNodeRow && description?.data && (
|
||||
@@ -76,13 +75,11 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<DetailTable columnsData={columns} tableName="Mixnode detail table" rows={mixNodeRow ? [mixNodeRow] : []} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12}>
|
||||
<DelegatorsInfoTable
|
||||
@@ -92,7 +89,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12}>
|
||||
<ContentCard title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`}>
|
||||
@@ -100,7 +96,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ContentCard title="Mixnode Stats">
|
||||
@@ -144,7 +139,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12} md={4}>
|
||||
{status && (
|
||||
|
||||
@@ -85,6 +85,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
{
|
||||
field: 'mix_id',
|
||||
headerName: 'Mix ID',
|
||||
disableColumnMenu: true,
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 100,
|
||||
@@ -103,6 +104,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
{
|
||||
field: 'identity_key',
|
||||
headerName: 'Identity Key',
|
||||
disableColumnMenu: true,
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 170,
|
||||
@@ -128,6 +130,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
{
|
||||
field: 'bond',
|
||||
headerName: 'Stake',
|
||||
disableColumnMenu: true,
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Stake" />,
|
||||
type: 'number',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
@@ -146,6 +149,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
{
|
||||
field: 'stake_saturation',
|
||||
headerName: 'Stake Saturation',
|
||||
disableColumnMenu: true,
|
||||
renderHeader: () => (
|
||||
<CustomColumnHeading
|
||||
headingTitle="Stake Saturation"
|
||||
@@ -164,13 +168,14 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
>
|
||||
{`${params.value.toFixed(2)} %`}
|
||||
{`${params.value} %`}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'pledge_amount',
|
||||
headerName: 'Bond',
|
||||
disableColumnMenu: true,
|
||||
width: 175,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />,
|
||||
@@ -189,6 +194,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
{
|
||||
field: 'profit_percentage',
|
||||
headerName: 'Profit Margin',
|
||||
disableColumnMenu: true,
|
||||
renderHeader: () => (
|
||||
<CustomColumnHeading
|
||||
headingTitle="Profit Margin"
|
||||
@@ -220,6 +226,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 170,
|
||||
headerAlign: 'left',
|
||||
disableColumnMenu: true,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
||||
@@ -231,12 +238,13 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'avg_uptime',
|
||||
field: 'node_performance',
|
||||
headerName: 'Routing Score',
|
||||
disableColumnMenu: true,
|
||||
renderHeader: () => (
|
||||
<CustomColumnHeading
|
||||
headingTitle="Routing Score"
|
||||
tooltipInfo="Node’s routing score is relative to that of the network. Each time a node is tested, the test packets have to go through the full path of the network (a gateway + 3 nodes). If a node in the path drop packets it will affect the score of other nodes in the test."
|
||||
tooltipInfo="Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test."
|
||||
/>
|
||||
),
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
@@ -255,6 +263,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
{
|
||||
field: 'owner',
|
||||
headerName: 'Owner',
|
||||
disableColumnMenu: true,
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 120,
|
||||
headerAlign: 'left',
|
||||
@@ -274,6 +283,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
field: 'location',
|
||||
headerName: 'Location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
disableColumnMenu: true,
|
||||
width: 120,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
@@ -303,6 +313,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
field: 'host',
|
||||
headerName: 'Host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
|
||||
disableColumnMenu: true,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 130,
|
||||
headerAlign: 'left',
|
||||
@@ -321,7 +332,6 @@ export const PageMixnodes: FCWithChildren = () => {
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
setPageSize(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title text="Mixnodes" />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Grid, Link, Typography } from '@mui/material';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PeopleAlt } from '@mui/icons-material';
|
||||
import { WorldMap } from '../../components/WorldMap';
|
||||
import { useMainContext } from '../../context/main';
|
||||
import { formatNumber } from '../../utils';
|
||||
@@ -18,7 +19,7 @@ import { Icons } from '../../components/Icons';
|
||||
export const PageOverview: FCWithChildren = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { summaryOverview, gateways, validators, block, countryData } = useMainContext();
|
||||
const { summaryOverview, gateways, validators, block, countryData, serviceProviders } = useMainContext();
|
||||
return (
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<Grid>
|
||||
@@ -61,7 +62,7 @@ export const PageOverview: FCWithChildren = () => {
|
||||
</>
|
||||
)}
|
||||
{gateways && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => navigate('/network-components/gateways')}
|
||||
title="Gateways"
|
||||
@@ -71,9 +72,19 @@ export const PageOverview: FCWithChildren = () => {
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{serviceProviders && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => navigate('/network-components/service-providers')}
|
||||
title="Service providers"
|
||||
icon={<PeopleAlt />}
|
||||
count={serviceProviders.data?.items.length}
|
||||
errorMsg={summaryOverview?.error}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{validators && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
|
||||
title="Validators"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, FormControl, Grid, MenuItem, Select, SelectChangeEvent } from '@mui/material';
|
||||
import { Api } from '../../api';
|
||||
import { TableToolbar } from '../../components/TableToolbar';
|
||||
import { Title } from '../../components/Title';
|
||||
import { UniversalDataGrid } from '../../components/Universal-DataGrid';
|
||||
import { DirectoryService } from '../../typeDefs/explorer-api';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
headerName: 'Client ID',
|
||||
field: 'address',
|
||||
disableColumnMenu: true,
|
||||
flex: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const SupportedApps = () => {
|
||||
const [selected, setSelected] = useState<string>('');
|
||||
const handleChange = (e: SelectChangeEvent) => setSelected(e.target.value);
|
||||
return (
|
||||
<FormControl size="small">
|
||||
<Select value={selected} onChange={handleChange} displayEmpty sx={{ mr: 2 }}>
|
||||
<MenuItem value="">Supported Apps</MenuItem>
|
||||
<MenuItem sx={{ opacity: 1 }}>Keybase</MenuItem>
|
||||
<MenuItem>Telegram</MenuItem>
|
||||
<MenuItem>Electrum</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export const ServiceProviders = () => {
|
||||
const [serviceProviders, setServiceProviders] = useState<DirectoryService>();
|
||||
const [pageSize, setPageSize] = React.useState('10');
|
||||
|
||||
const getServiceproviders = async () => {
|
||||
const [data] = await Api.fetchServiceProviders();
|
||||
setServiceProviders(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getServiceproviders();
|
||||
}, []);
|
||||
|
||||
const handleOnPageSizeChange = (event: SelectChangeEvent<string>) => {
|
||||
setPageSize(event.target.value);
|
||||
};
|
||||
|
||||
if (!serviceProviders) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title text="Service Providers" />
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Card
|
||||
sx={{
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<TableToolbar
|
||||
onChangePageSize={handleOnPageSizeChange}
|
||||
pageSize={pageSize}
|
||||
childrenBefore={<SupportedApps />}
|
||||
/>
|
||||
<UniversalDataGrid pagination rows={serviceProviders.items} columns={columns} pageSize={pageSize} />
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { PageGateways } from '../pages/Gateways';
|
||||
import { PageGatewayDetail } from '../pages/GatewayDetail';
|
||||
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
|
||||
import { PageMixnodes } from '../pages/Mixnodes';
|
||||
import { ServiceProviders } from '../pages/ServiceProviders';
|
||||
|
||||
const ValidatorRoute: FCWithChildren = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -21,6 +22,6 @@ export const NetworkComponentsRoutes: FCWithChildren = () => (
|
||||
<Route path="gateways" element={<PageGateways />} />
|
||||
<Route path="gateway/:id" element={<PageGatewayDetail />} />
|
||||
<Route path="validators" element={<ValidatorRoute />} />
|
||||
<Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} />
|
||||
<Route path="service-providers" element={<ServiceProviders />} />
|
||||
</ReactRouterRoutes>
|
||||
);
|
||||
|
||||
+8
-12
@@ -4,31 +4,27 @@
|
||||
the theme declaration in index.tsx or the style prop
|
||||
in <DataGrid /> */
|
||||
|
||||
.MuiDataGrid-sortIcon, .MuiDataGrid-menuIcon, .MuiDataGrid-iconButtonContainer{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-columnSeparator {
|
||||
visibility: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* TODO - this should be managed somehow in MUI DataGrid
|
||||
but documentation doesnt offer a way to do it. Possibly only
|
||||
included in Data Grid Pro package */
|
||||
.MuiDataGrid-header-override {
|
||||
height: 55px;
|
||||
min-height: 55px;
|
||||
height: 55px;
|
||||
min-height: 55px;
|
||||
}
|
||||
|
||||
/* Again, no offered way to add sx to this specific div to kill the padding
|
||||
which puts it out of sync with other (sx styled) cells */
|
||||
div div.MuiDataGrid-root .MuiDataGrid-columnHeaderTitleContainer {
|
||||
padding-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.MuiDrawer-paperAnchorLeft {
|
||||
min-width: 100vw;
|
||||
margin-top: 58px;
|
||||
}
|
||||
.MuiDrawer-paperAnchorLeft {
|
||||
min-width: 100vw;
|
||||
margin-top: 58px;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user