Compare commits
41 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 | |||
| f0d9703587 | |||
| c08efef8ed | |||
| 6d29774744 | |||
| af6bab7703 | |||
| 7033f92d82 | |||
| 128dfa6d81 | |||
| 0b0ec075bb | |||
| cca4d21e7c |
@@ -79,6 +79,9 @@ jobs:
|
|||||||
override: true
|
override: true
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Install wasm-opt
|
||||||
|
run: cargo install wasm-opt
|
||||||
|
|
||||||
- name: Build release contracts
|
- name: Build release contracts
|
||||||
run: make wasm
|
run: make wasm
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
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]
|
runs-on: [self-hosted, custom-runner-linux]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -19,6 +19,9 @@ jobs:
|
|||||||
override: true
|
override: true
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Install wasm-opt
|
||||||
|
run: cargo install wasm-opt
|
||||||
|
|
||||||
- name: Build release contracts
|
- name: Build release contracts
|
||||||
run: make wasm
|
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
|
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [custom-runner-mac-m1]
|
platform: [macos-latest]
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
@@ -24,15 +24,10 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
|
|
||||||
- name: Setup yarn
|
|
||||||
run: npm install -g yarn
|
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|
||||||
- name: Install the Apple developer certificate for code signing
|
- name: Install the Apple developer certificate for code signing
|
||||||
env:
|
env:
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
@@ -44,7 +39,7 @@ jobs:
|
|||||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
# import certificate and provisioning profile from secrets
|
# import certificate and provisioning profile from secrets
|
||||||
echo -n "$APPLE_CERTIFICATE" | base64 --decode --output=$CERTIFICATE_PATH
|
echo -n "$APPLE_CERTIFICATE" | base64 --decode --output $CERTIFICATE_PATH
|
||||||
|
|
||||||
# create temporary keychain
|
# create temporary keychain
|
||||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defaults:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-tauri:
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-nym:
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defaults:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-tauri:
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ defaults:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-tauri:
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -4,6 +4,40 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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)
|
## [v1.1.10] (2023-02-21)
|
||||||
|
|
||||||
- Verloc listener causing mixnode unexpected shutdown ([#3038])
|
- Verloc listener causing mixnode unexpected shutdown ([#3038])
|
||||||
|
|||||||
Generated
+33
-66
@@ -677,7 +677,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "client-core"
|
name = "client-core"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"dashmap 5.4.0",
|
"dashmap 5.4.0",
|
||||||
@@ -705,6 +705,8 @@ dependencies = [
|
|||||||
"time 0.3.17",
|
"time 0.3.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"tokio-tungstenite 0.14.0",
|
||||||
|
"tungstenite 0.13.0",
|
||||||
"url",
|
"url",
|
||||||
"validator-client",
|
"validator-client",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -1711,31 +1713,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-iterator"
|
name = "enum-iterator"
|
||||||
version = "0.8.1"
|
version = "1.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9"
|
checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"enum-iterator-derive 0.8.1",
|
"enum-iterator-derive",
|
||||||
]
|
|
||||||
|
|
||||||
[[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",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1800,7 +1782,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "explorer-api"
|
name = "explorer-api"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.1.4",
|
"clap 4.1.4",
|
||||||
@@ -2205,9 +2187,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git2"
|
name = "git2"
|
||||||
version = "0.16.1"
|
version = "0.14.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
|
checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2915,9 +2897,9 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libgit2-sys"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3279,7 +3261,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-api"
|
name = "nym-api"
|
||||||
version = "1.1.11"
|
version = "1.1.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3360,7 +3342,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-bin-common"
|
name = "nym-bin-common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 4.1.4",
|
"clap 4.1.4",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
@@ -3369,7 +3351,7 @@ dependencies = [
|
|||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"semver 0.11.0",
|
"semver 0.11.0",
|
||||||
"serde",
|
"serde",
|
||||||
"vergen 7.5.1",
|
"vergen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3389,7 +3371,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-cli"
|
name = "nym-cli"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
@@ -3447,7 +3429,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-client"
|
name = "nym-client"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 4.1.4",
|
"clap 4.1.4",
|
||||||
"client-core",
|
"client-core",
|
||||||
@@ -3496,7 +3478,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-contracts-common"
|
name = "nym-contracts-common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bs58",
|
"bs58",
|
||||||
"cosmwasm-std",
|
"cosmwasm-std",
|
||||||
@@ -3508,7 +3490,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-crypto"
|
name = "nym-crypto"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.8.2",
|
"aes 0.8.2",
|
||||||
"blake3",
|
"blake3",
|
||||||
@@ -3541,7 +3523,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-gateway"
|
name = "nym-gateway"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -3599,7 +3581,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-mixnet-contract-common"
|
name = "nym-mixnet-contract-common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bs58",
|
"bs58",
|
||||||
"cosmwasm-std",
|
"cosmwasm-std",
|
||||||
@@ -3618,7 +3600,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-mixnode"
|
name = "nym-mixnode"
|
||||||
version = "1.1.11"
|
version = "1.1.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"atty",
|
"atty",
|
||||||
@@ -3673,7 +3655,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-network-requester"
|
name = "nym-network-requester"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"clap 4.1.4",
|
"clap 4.1.4",
|
||||||
@@ -3711,7 +3693,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-network-statistics"
|
name = "nym-network-statistics"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"log",
|
"log",
|
||||||
@@ -3755,7 +3737,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-pemstore"
|
name = "nym-pemstore"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pem",
|
"pem",
|
||||||
]
|
]
|
||||||
@@ -3774,6 +3756,7 @@ dependencies = [
|
|||||||
"nym-network-defaults",
|
"nym-network-defaults",
|
||||||
"nym-sphinx",
|
"nym-sphinx",
|
||||||
"nym-task",
|
"nym-task",
|
||||||
|
"nym-topology",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
"tap",
|
"tap",
|
||||||
@@ -3786,7 +3769,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-socks5-client"
|
name = "nym-socks5-client"
|
||||||
version = "1.1.10"
|
version = "1.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 4.1.4",
|
"clap 4.1.4",
|
||||||
"client-core",
|
"client-core",
|
||||||
@@ -3946,7 +3929,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-sphinx-types"
|
name = "nym-sphinx-types"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sphinx-packet",
|
"sphinx-packet",
|
||||||
]
|
]
|
||||||
@@ -3981,6 +3964,7 @@ dependencies = [
|
|||||||
name = "nym-topology"
|
name = "nym-topology"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"bs58",
|
"bs58",
|
||||||
"log",
|
"log",
|
||||||
"nym-bin-common",
|
"nym-bin-common",
|
||||||
@@ -4030,12 +4014,12 @@ dependencies = [
|
|||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"vergen 5.1.17",
|
"vergen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-vesting-contract-common"
|
name = "nym-vesting-contract-common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cosmwasm-std",
|
"cosmwasm-std",
|
||||||
"nym-contracts-common",
|
"nym-contracts-common",
|
||||||
@@ -6914,30 +6898,13 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vergen"
|
name = "vergen"
|
||||||
version = "5.1.17"
|
version = "7.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e"
|
checksum = "447f9238a4553957277b3ee09d80babeae0811f1b3baefb093de1c0448437a37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"chrono",
|
"enum-iterator",
|
||||||
"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",
|
|
||||||
"getset",
|
"getset",
|
||||||
"git2",
|
"git2",
|
||||||
"rustc_version 0.4.0",
|
"rustc_version 0.4.0",
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ edition = "2021"
|
|||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
async-trait = "0.1.63"
|
async-trait = "0.1.64"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ fmt-wasm-client:
|
|||||||
|
|
||||||
wasm:
|
wasm:
|
||||||
RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown
|
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
|
mixnet-opt: wasm
|
||||||
cd contracts/mixnet && make opt
|
cd contracts/mixnet && make opt
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "client-core"
|
name = "client-core"
|
||||||
version = "1.1.10"
|
version = "1.1.12"
|
||||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.66"
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1.58" }
|
async-trait = { workspace = true }
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
dashmap = "5.4.0"
|
dashmap = "5.4.0"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
@@ -20,6 +20,7 @@ serde_json = "1.0.89"
|
|||||||
tap = "1.0.1"
|
tap = "1.0.1"
|
||||||
thiserror = "1.0.34"
|
thiserror = "1.0.34"
|
||||||
url = { version ="2.2", features = ["serde"] }
|
url = { version ="2.2", features = ["serde"] }
|
||||||
|
tungstenite = { version = "0.13.0", default-features = false }
|
||||||
tokio = { version = "1.24.1", features = ["macros"]}
|
tokio = { version = "1.24.1", features = ["macros"]}
|
||||||
time = "0.3.17"
|
time = "0.3.17"
|
||||||
|
|
||||||
@@ -44,6 +45,9 @@ features = ["time"]
|
|||||||
version = "1.24.1"
|
version = "1.24.1"
|
||||||
features = ["time"]
|
features = ["time"]
|
||||||
|
|
||||||
|
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
|
||||||
|
version = "0.14"
|
||||||
|
|
||||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
||||||
@@ -65,6 +69,7 @@ features = ["futures"]
|
|||||||
|
|
||||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils]
|
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils]
|
||||||
path = "../../common/wasm-utils"
|
path = "../../common/wasm-utils"
|
||||||
|
features = ["websocket"]
|
||||||
|
|
||||||
[target."cfg(target_arch = \"wasm32\")".dependencies.time]
|
[target."cfg(target_arch = \"wasm32\")".dependencies.time]
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyCon
|
|||||||
use crate::client::replies::reply_storage::{
|
use crate::client::replies::reply_storage::{
|
||||||
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
|
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
|
||||||
};
|
};
|
||||||
|
use crate::client::topology_control::nym_api_provider::NymApiTopologyProvider;
|
||||||
use crate::client::topology_control::{
|
use crate::client::topology_control::{
|
||||||
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
|
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
|
||||||
};
|
};
|
||||||
@@ -37,10 +38,12 @@ use nym_sphinx::addressing::nodes::NodeIdentity;
|
|||||||
use nym_sphinx::receiver::ReconstructedMessage;
|
use nym_sphinx::receiver::ReconstructedMessage;
|
||||||
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
|
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
|
||||||
use nym_task::{TaskClient, TaskManager};
|
use nym_task::{TaskClient, TaskManager};
|
||||||
|
use nym_topology::provider_trait::TopologyProvider;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tap::TapFallible;
|
use tap::TapFallible;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use validator_client::nyxd::CosmWasmClient;
|
use validator_client::nyxd::CosmWasmClient;
|
||||||
|
|
||||||
@@ -90,6 +93,7 @@ impl ClientOutput {
|
|||||||
pub struct ClientState {
|
pub struct ClientState {
|
||||||
pub shared_lane_queue_lengths: LaneQueueLengths,
|
pub shared_lane_queue_lengths: LaneQueueLengths,
|
||||||
pub reply_controller_sender: ReplyControllerSender,
|
pub reply_controller_sender: ReplyControllerSender,
|
||||||
|
pub topology_accessor: TopologyAccessor,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ClientInputStatus {
|
pub enum ClientInputStatus {
|
||||||
@@ -154,6 +158,7 @@ pub struct BaseClientBuilder<'a, B, C: Clone> {
|
|||||||
nym_api_endpoints: Vec<Url>,
|
nym_api_endpoints: Vec<Url>,
|
||||||
reply_storage_backend: B,
|
reply_storage_backend: B,
|
||||||
|
|
||||||
|
custom_topology_provider: Option<Box<dyn TopologyProvider>>,
|
||||||
bandwidth_controller: Option<BandwidthController<C>>,
|
bandwidth_controller: Option<BandwidthController<C>>,
|
||||||
key_manager: KeyManager,
|
key_manager: KeyManager,
|
||||||
}
|
}
|
||||||
@@ -177,6 +182,7 @@ where
|
|||||||
bandwidth_controller,
|
bandwidth_controller,
|
||||||
reply_storage_backend,
|
reply_storage_backend,
|
||||||
key_manager,
|
key_manager,
|
||||||
|
custom_topology_provider: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,11 +201,17 @@ where
|
|||||||
disabled_credentials: credentials_toggle.is_disabled(),
|
disabled_credentials: credentials_toggle.is_disabled(),
|
||||||
nym_api_endpoints,
|
nym_api_endpoints,
|
||||||
reply_storage_backend,
|
reply_storage_backend,
|
||||||
|
custom_topology_provider: None,
|
||||||
bandwidth_controller,
|
bandwidth_controller,
|
||||||
key_manager,
|
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 {
|
pub fn as_mix_recipient(&self) -> Recipient {
|
||||||
Recipient::new(
|
Recipient::new(
|
||||||
*self.key_manager.identity_keypair().public_key(),
|
*self.key_manager.identity_keypair().public_key(),
|
||||||
@@ -304,7 +316,7 @@ where
|
|||||||
}
|
}
|
||||||
let gateway_address = self.gateway_config.gateway_listener.clone();
|
let gateway_address = self.gateway_config.gateway_listener.clone();
|
||||||
if gateway_address.is_empty() {
|
if gateway_address.is_empty() {
|
||||||
return Err(ClientCoreError::GatwayAddressUnknown);
|
return Err(ClientCoreError::GatewayAddressUnknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
let gateway_identity = identity::PublicKey::from_base58_string(gateway_id)
|
let gateway_identity = identity::PublicKey::from_base58_string(gateway_id)
|
||||||
@@ -341,25 +353,38 @@ where
|
|||||||
Ok(gateway_client)
|
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
|
// future responsible for periodically polling directory server and updating
|
||||||
// the current global view of topology
|
// the current global view of topology
|
||||||
async fn start_topology_refresher(
|
async fn start_topology_refresher(
|
||||||
nym_api_urls: Vec<Url>,
|
topology_provider: Box<dyn TopologyProvider>,
|
||||||
refresh_rate: Duration,
|
refresh_rate: Duration,
|
||||||
topology_accessor: TopologyAccessor,
|
topology_accessor: TopologyAccessor,
|
||||||
shutdown: TaskClient,
|
shutdown: TaskClient,
|
||||||
) -> Result<(), ClientCoreError> {
|
) -> Result<(), ClientCoreError> {
|
||||||
let topology_refresher_config = TopologyRefresherConfig::new(
|
let topology_refresher_config = TopologyRefresherConfig::new(refresh_rate);
|
||||||
nym_api_urls,
|
|
||||||
refresh_rate,
|
let mut topology_refresher = TopologyRefresher::new(
|
||||||
env!("CARGO_PKG_VERSION").to_string(),
|
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
|
// before returning, block entire runtime to refresh the current network view so that any
|
||||||
// components depending on topology would see a non-empty view
|
// components depending on topology would see a non-empty view
|
||||||
info!("Obtaining initial network topology");
|
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 {
|
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
|
||||||
log::error!(
|
log::error!(
|
||||||
@@ -468,8 +493,12 @@ where
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let topology_provider = Self::setup_topology_provider(
|
||||||
|
self.custom_topology_provider.take(),
|
||||||
|
self.nym_api_endpoints,
|
||||||
|
);
|
||||||
Self::start_topology_refresher(
|
Self::start_topology_refresher(
|
||||||
self.nym_api_endpoints.clone(),
|
topology_provider,
|
||||||
self.debug_config.topology_refresh_rate,
|
self.debug_config.topology_refresh_rate,
|
||||||
shared_topology_accessor.clone(),
|
shared_topology_accessor.clone(),
|
||||||
task_manager.subscribe(),
|
task_manager.subscribe(),
|
||||||
@@ -530,7 +559,7 @@ where
|
|||||||
self.debug_config,
|
self.debug_config,
|
||||||
self.key_manager.ack_key(),
|
self.key_manager.ack_key(),
|
||||||
self_address,
|
self_address,
|
||||||
shared_topology_accessor,
|
shared_topology_accessor.clone(),
|
||||||
sphinx_message_sender,
|
sphinx_message_sender,
|
||||||
task_manager.subscribe(),
|
task_manager.subscribe(),
|
||||||
);
|
);
|
||||||
@@ -554,6 +583,7 @@ where
|
|||||||
client_state: ClientState {
|
client_state: ClientState {
|
||||||
shared_lane_queue_lengths,
|
shared_lane_queue_lengths,
|
||||||
reply_controller_sender,
|
reply_controller_sender,
|
||||||
|
topology_accessor: shared_topology_accessor,
|
||||||
},
|
},
|
||||||
task_manager,
|
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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use nym_config::defaults::NymNetworkDetails;
|
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 nym_sphinx::params::PacketSize;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
@@ -579,7 +579,7 @@ impl<T: NymConfig> Client<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_database_path(id: &str) -> PathBuf {
|
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 gateway_client::error::GatewayClientError;
|
||||||
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
|
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
|
||||||
|
use nym_topology::gateway::GatewayConversionError;
|
||||||
use nym_topology::NymTopologyError;
|
use nym_topology::NymTopologyError;
|
||||||
use validator_client::ValidatorClientError;
|
use validator_client::ValidatorClientError;
|
||||||
|
|
||||||
@@ -53,7 +54,32 @@ pub enum ClientCoreError {
|
|||||||
GatewayOwnerUnknown,
|
GatewayOwnerUnknown,
|
||||||
|
|
||||||
#[error("The address of the gateway is unknown - did you run init?")]
|
#[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")]
|
#[error("failed to register receiver for reconstructed mixnet messages")]
|
||||||
FailedToRegisterReceiver,
|
FailedToRegisterReceiver,
|
||||||
|
|||||||
@@ -6,52 +6,223 @@ use crate::{
|
|||||||
config::{persistence::key_pathfinder::ClientKeyPathfinder, Config},
|
config::{persistence::key_pathfinder::ClientKeyPathfinder, Config},
|
||||||
error::ClientCoreError,
|
error::ClientCoreError,
|
||||||
};
|
};
|
||||||
#[cfg(target_arch = "wasm32")]
|
use futures::{SinkExt, StreamExt};
|
||||||
use gateway_client::wasm_mockups::SigningNyxdClient;
|
|
||||||
use gateway_client::GatewayClient;
|
use gateway_client::GatewayClient;
|
||||||
use gateway_requests::registration::handshake::SharedKeys;
|
use gateway_requests::registration::handshake::SharedKeys;
|
||||||
|
use log::{debug, info, trace, warn};
|
||||||
use nym_config::NymConfig;
|
use nym_config::NymConfig;
|
||||||
use nym_crypto::asymmetric::identity;
|
use nym_crypto::asymmetric::identity;
|
||||||
use nym_topology::{filter::VersionFilterable, gateway};
|
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 std::{sync::Arc, time::Duration};
|
||||||
use tap::TapFallible;
|
use tap::TapFallible;
|
||||||
|
use tungstenite::Message;
|
||||||
use url::Url;
|
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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use validator_client::nyxd::SigningNyxdClient;
|
use validator_client::nyxd::SigningNyxdClient;
|
||||||
|
|
||||||
pub(super) async fn query_gateway_details(
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
validator_servers: Vec<Url>,
|
type WsConn = WebSocketStream<MaybeTlsStream<TcpStream>>;
|
||||||
chosen_gateway_id: Option<identity::PublicKey>,
|
|
||||||
) -> Result<gateway::Node, ClientCoreError> {
|
#[cfg(target_arch = "wasm32")]
|
||||||
let nym_api = validator_servers
|
use gateway_client::wasm_mockups::SigningNyxdClient;
|
||||||
.choose(&mut thread_rng())
|
#[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)?;
|
.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);
|
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
|
let valid_gateways = gateways
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|gateway| gateway.try_into().ok())
|
.filter_map(|gateway| gateway.try_into().ok())
|
||||||
.collect::<Vec<gateway::Node>>();
|
.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"));
|
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.
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
// (remember that in active topology all gateways have at least 100 reputation so should
|
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
|
||||||
// be working correctly)
|
match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await {
|
||||||
if let Some(gateway_id) = chosen_gateway_id {
|
Err(_elapsed) => Err(ClientCoreError::GatewayConnectionTimeout),
|
||||||
filtered_gateways
|
Ok(Err(conn_failure)) => Err(conn_failure.into()),
|
||||||
.iter()
|
Ok(Ok((stream, _))) => Ok(stream),
|
||||||
.find(|gateway| gateway.identity_key == gateway_id)
|
}
|
||||||
.ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_id.to_string()))
|
}
|
||||||
.cloned()
|
|
||||||
|
#[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 {
|
} else {
|
||||||
filtered_gateways
|
uniformly_random_gateway(&mut rng, gateways)
|
||||||
.choose(&mut rand::thread_rng())
|
|
||||||
.ok_or(ClientCoreError::NoGatewaysOnNetwork)
|
|
||||||
.cloned()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ pub async fn register_with_gateway(
|
|||||||
key_manager: &mut KeyManager,
|
key_manager: &mut KeyManager,
|
||||||
nym_api_endpoints: Vec<Url>,
|
nym_api_endpoints: Vec<Url>,
|
||||||
chosen_gateway_id: Option<identity::PublicKey>,
|
chosen_gateway_id: Option<identity::PublicKey>,
|
||||||
|
by_latency: bool,
|
||||||
) -> Result<GatewayEndpointConfig, ClientCoreError> {
|
) -> Result<GatewayEndpointConfig, ClientCoreError> {
|
||||||
// Get the gateway details of the gateway we will use
|
// 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);
|
log::debug!("Querying gateway gives: {}", gateway);
|
||||||
|
|
||||||
let our_identity = key_manager.identity_keypair();
|
let our_identity = key_manager.identity_keypair();
|
||||||
@@ -102,6 +104,7 @@ pub async fn setup_gateway_from_config<C, T>(
|
|||||||
register_gateway: bool,
|
register_gateway: bool,
|
||||||
user_chosen_gateway_id: Option<identity::PublicKey>,
|
user_chosen_gateway_id: Option<identity::PublicKey>,
|
||||||
config: &Config<T>,
|
config: &Config<T>,
|
||||||
|
by_latency: bool,
|
||||||
) -> Result<GatewayEndpointConfig, ClientCoreError>
|
) -> Result<GatewayEndpointConfig, ClientCoreError>
|
||||||
where
|
where
|
||||||
C: NymConfig + ClientCoreConfigTrait,
|
C: NymConfig + ClientCoreConfigTrait,
|
||||||
@@ -117,9 +120,12 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Else, we preceed by querying the nym-api
|
// Else, we preceed by querying the nym-api
|
||||||
let gateway =
|
let gateway = helpers::query_gateway_details(
|
||||||
helpers::query_gateway_details(config.get_nym_api_endpoints(), user_chosen_gateway_id)
|
config.get_nym_api_endpoints(),
|
||||||
.await?;
|
user_chosen_gateway_id,
|
||||||
|
by_latency,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
log::debug!("Querying gateway gives: {}", gateway);
|
log::debug!("Querying gateway gives: {}", gateway);
|
||||||
|
|
||||||
// If we are not registering, just return this and assume the caller has the keys already and
|
// 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 error::Result;
|
||||||
use log::*;
|
use log::*;
|
||||||
use nym_bin_common::completions::fig_generate;
|
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 nym_network_defaults::{setup_env, NymNetworkDetails};
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
@@ -51,8 +51,11 @@ async fn block_until_coconut_is_available<C: Clone + CosmWasmClient + Send + Syn
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
// Use 20 additional seconds to avoid the exact moment of going into the final epoch state
|
// 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() + 20 - current_timestamp_secs;
|
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);
|
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));
|
std::thread::sleep(Duration::from_secs(secs_until_final));
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,10 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Command::Run(r) => {
|
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 shared_storage = credential_storage::initialise_storage(db_path).await;
|
||||||
let recovery_storage = recovery_storage::RecoveryStorage::new(r.recovery_dir)?;
|
let recovery_storage = recovery_storage::RecoveryStorage::new(r.recovery_dir)?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-client"
|
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>"]
|
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||||
description = "Implementation of the Nym Client"
|
description = "Implementation of the Nym Client"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ impl SocketClient {
|
|||||||
let ClientState {
|
let ClientState {
|
||||||
shared_lane_queue_lengths,
|
shared_lane_queue_lengths,
|
||||||
reply_controller_sender,
|
reply_controller_sender,
|
||||||
|
..
|
||||||
} = client_state;
|
} = client_state;
|
||||||
|
|
||||||
let websocket_handler = websocket::HandlerBuilder::new(
|
let websocket_handler = websocket::HandlerBuilder::new(
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ pub(crate) struct Init {
|
|||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
gateway: Option<identity::PublicKey>,
|
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,
|
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
|
||||||
/// potentially causing loss of access.
|
/// potentially causing loss of access.
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
@@ -143,6 +148,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), ClientError> {
|
|||||||
register_gateway,
|
register_gateway,
|
||||||
user_chosen_gateway_id,
|
user_chosen_gateway_id,
|
||||||
config.get_base(),
|
config.get_base(),
|
||||||
|
args.latency_based_selection,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
|
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-socks5-client"
|
name = "nym-socks5-client"
|
||||||
version = "1.1.10"
|
version = "1.1.12"
|
||||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
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"
|
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ impl NymClient {
|
|||||||
|
|
||||||
let ClientState {
|
let ClientState {
|
||||||
shared_lane_queue_lengths,
|
shared_lane_queue_lengths,
|
||||||
reply_controller_sender: _,
|
..
|
||||||
} = client_status;
|
} = client_status;
|
||||||
|
|
||||||
let authenticator = Authenticator::new(auth_methods, allowed_users);
|
let authenticator = Authenticator::new(auth_methods, allowed_users);
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ pub(crate) struct Init {
|
|||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
gateway: Option<identity::PublicKey>,
|
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,
|
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
|
||||||
/// potentially causing loss of access.
|
/// potentially causing loss of access.
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
@@ -149,6 +154,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), Socks5ClientError> {
|
|||||||
register_gateway,
|
register_gateway,
|
||||||
user_chosen_gateway_id,
|
user_chosen_gateway_id,
|
||||||
config.get_base(),
|
config.get_base(),
|
||||||
|
args.latency_based_selection,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
|
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-bin-common"
|
name = "nym-bin-common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Common code for nym binaries"
|
description = "Common code for nym binaries"
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ log = { workspace = true }
|
|||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
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"] }
|
tokio = { version = "1.24.1", features = ["macros"] }
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
|
|||||||
# required for nyxd-client
|
# required for nyxd-client
|
||||||
# at some point it might be possible to make it wasm-compatible
|
# 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)
|
# 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 }
|
bip39 = { version = "1", features = ["rand"], optional = true }
|
||||||
nym-config = { path = "../../config", 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}
|
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,
|
GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
|
||||||
RewardEstimationResponse, StakeSaturationResponse,
|
RewardEstimationResponse, StakeSaturationResponse,
|
||||||
};
|
};
|
||||||
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
pub use nym_mixnet_contract_common::{mixnode::MixNodeDetails, GatewayBond, IdentityKeyRef, MixId};
|
||||||
use nym_mixnet_contract_common::MixId;
|
|
||||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef};
|
|
||||||
|
|
||||||
#[cfg(feature = "nyxd-client")]
|
#[cfg(feature = "nyxd-client")]
|
||||||
use crate::nyxd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient};
|
use crate::nyxd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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 cosmrs::Coin as CosmosCoin;
|
||||||
pub use cosmwasm_std::Coin as CosmWasmCoin;
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||||
pub struct MismatchedDenoms;
|
pub struct MismatchedDenoms;
|
||||||
@@ -19,6 +22,40 @@ pub struct Coin {
|
|||||||
pub denom: String,
|
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 {
|
impl Coin {
|
||||||
pub fn new<S: Into<String>>(amount: u128, denom: S) -> Self {
|
pub fn new<S: Into<String>>(amount: u128, denom: S) -> Self {
|
||||||
Coin {
|
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>
|
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::nyxd::Coin;
|
|
||||||
use crate::nyxd::Gas;
|
use crate::nyxd::Gas;
|
||||||
|
use crate::nyxd::{Coin, GasPrice};
|
||||||
use cosmrs::{tx, AccountId};
|
use cosmrs::{tx, AccountId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
@@ -64,6 +64,12 @@ impl Display for Fee {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub fn new_payer_granter_auto(
|
||||||
gas_adjustment: Option<GasAdjustment>,
|
gas_adjustment: Option<GasAdjustment>,
|
||||||
payer: Option<AccountId>,
|
payer: Option<AccountId>,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub mod defaults;
|
|||||||
|
|
||||||
pub const CONFIG_DIR: &str = "config";
|
pub const CONFIG_DIR: &str = "config";
|
||||||
pub const DATA_DIR: &str = "data";
|
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 {
|
pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||||
fn template() -> &'static str;
|
fn template() -> &'static str;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-contracts-common"
|
name = "nym-contracts-common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Common library for Nym cosmwasm contracts"
|
description = "Common library for Nym cosmwasm contracts"
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-mixnet-contract-common"
|
name = "nym-mixnet-contract-common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Common library for the Nym mixnet contract"
|
description = "Common library for the Nym mixnet contract"
|
||||||
rust-version = "1.62"
|
rust-version = "1.62"
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
@@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
schemars = "0.8"
|
schemars = "0.8"
|
||||||
thiserror = "1.0"
|
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"
|
serde_json = "1.0.0"
|
||||||
humantime-serde = "1.1.1"
|
humantime-serde = "1.1.1"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-vesting-contract-common"
|
name = "nym-vesting-contract-common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Common library for the Nym vesting contract"
|
description = "Common library for the Nym vesting contract"
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
@@ -9,8 +9,8 @@ repository = { workspace = true }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cosmwasm-std = "1.0.0"
|
cosmwasm-std = "1.0.0"
|
||||||
mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-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.1.0" }
|
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.2.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
schemars = "0.8"
|
schemars = "0.8"
|
||||||
ts-rs = {version = "6.1.2", optional = true}
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1.51" }
|
async-trait = { workspace = true }
|
||||||
|
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
|
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-crypto"
|
name = "nym-crypto"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Crypto library for the nym mixnet"
|
description = "Crypto library for the nym mixnet"
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
@@ -26,8 +26,8 @@ subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
|
|||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.1.0" }
|
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0" }
|
||||||
nym-pemstore = { path = "../../common/pemstore", version = "0.1.0" }
|
nym-pemstore = { path = "../../common/pemstore", version = "0.2.0" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand_chacha = "0.2"
|
rand_chacha = "0.2"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[cfg(feature = "rand")]
|
#[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")]
|
#[cfg(feature = "serde")]
|
||||||
impl Serialize for PublicKey {
|
impl Serialize for PublicKey {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1.51" }
|
async-trait = { workspace = true }
|
||||||
thiserror = "1.0"
|
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
|
# those dependencies are due to intriducing preparer and receiver. Perpaphs that indicates they should be moved
|
||||||
# to separate crate?
|
# to separate crate?
|
||||||
nym-crypto = { path = "../crypto", version = "0.1.0" }
|
nym-crypto = { path = "../crypto", version = "0.2.0" }
|
||||||
nym-topology = { path = "../topology" }
|
nym-topology = { path = "../topology" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-sphinx-types"
|
name = "nym-sphinx-types"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Re-export sphinx packet types"
|
description = "Re-export sphinx packet types"
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-pemstore"
|
name = "nym-pemstore"
|
||||||
description = "Store private-public keypairs in PEM format"
|
description = "Store private-public keypairs in PEM format"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
license = { 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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1.51" }
|
async-trait = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::error::StatsError;
|
|||||||
use crate::StatsMessage;
|
use crate::StatsMessage;
|
||||||
|
|
||||||
pub const DEFAULT_STATISTICS_SERVICE_ADDRESS: &str = "127.0.0.1";
|
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_VERSION: &str = "/v1";
|
||||||
pub const STATISTICS_SERVICE_API_STATISTICS: &str = "statistic";
|
pub const STATISTICS_SERVICE_API_STATISTICS: &str = "statistic";
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ bs58 = "0.4"
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
|
async-trait = { workspace = true, optional = true }
|
||||||
|
|
||||||
## internal
|
## internal
|
||||||
nym-crypto = { path = "../crypto" }
|
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-addressing = { path = "../nymsphinx/addressing" }
|
||||||
nym-sphinx-types = { path = "../nymsphinx/types" }
|
nym-sphinx-types = { path = "../nymsphinx/types" }
|
||||||
nym-bin-common = { path = "../bin-common" }
|
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 gateway;
|
||||||
pub mod mix;
|
pub mod mix;
|
||||||
|
|
||||||
|
#[cfg(feature = "provider-trait")]
|
||||||
|
pub mod provider_trait;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)]
|
#[derive(Debug, Clone, Error)]
|
||||||
pub enum NymTopologyError {
|
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")]
|
#[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}")]
|
#[error("No mixnodes available on layer {layer}")]
|
||||||
EmptyMixLayer { layer: MixLayer },
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -97,7 +110,7 @@ impl NymTopology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn num_mixnodes(&self) -> usize {
|
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> {
|
pub fn mixes_as_vec(&self) -> Vec<mix::Node> {
|
||||||
@@ -238,6 +251,46 @@ impl NymTopology {
|
|||||||
Ok(())
|
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]
|
#[must_use]
|
||||||
pub fn filter_system_version(&self, expected_version: &str) -> Self {
|
pub fn filter_system_version(&self, expected_version: &str) -> Self {
|
||||||
self.filter_node_versions(expected_version)
|
self.filter_node_versions(expected_version)
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
use crate::{filter, NetworkAddress};
|
use crate::{filter, NetworkAddress};
|
||||||
use nym_crypto::asymmetric::{encryption, identity};
|
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_addressing::nodes::NymNodeRoutingAddress;
|
||||||
use nym_sphinx_types::Node as SphinxNode;
|
use nym_sphinx_types::Node as SphinxNode;
|
||||||
use std::convert::{TryFrom, TryInto};
|
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::epoch_state::transactions::advance_epoch_state;
|
||||||
use crate::support::tests::fixtures::dealer_details_fixture;
|
use crate::support::tests::fixtures::dealer_details_fixture;
|
||||||
use crate::support::tests::helpers;
|
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 coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
||||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||||
use cw4::Member;
|
use cw4::Member;
|
||||||
@@ -147,6 +147,8 @@ pub(crate) mod tests {
|
|||||||
.block
|
.block
|
||||||
.time
|
.time
|
||||||
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
|
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
|
||||||
|
|
||||||
|
add_fixture_dealer(deps.as_mut());
|
||||||
advance_epoch_state(deps.as_mut(), env).unwrap();
|
advance_epoch_state(deps.as_mut(), env).unwrap();
|
||||||
|
|
||||||
let ret = try_add_dealer(
|
let ret = try_add_dealer(
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ pub(crate) mod tests {
|
|||||||
use crate::epoch_state::transactions::advance_epoch_state;
|
use crate::epoch_state::transactions::advance_epoch_state;
|
||||||
use crate::support::tests::fixtures::{dealer_details_fixture, dealing_bytes_fixture};
|
use crate::support::tests::fixtures::{dealer_details_fixture, dealing_bytes_fixture};
|
||||||
use crate::support::tests::helpers;
|
use crate::support::tests::helpers;
|
||||||
|
use crate::support::tests::helpers::add_fixture_dealer;
|
||||||
use coconut_dkg_common::dealer::DealerDetails;
|
use coconut_dkg_common::dealer::DealerDetails;
|
||||||
use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
||||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||||
@@ -80,6 +81,7 @@ pub(crate) mod tests {
|
|||||||
.block
|
.block
|
||||||
.time
|
.time
|
||||||
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
|
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
|
||||||
|
add_fixture_dealer(deps.as_mut());
|
||||||
advance_epoch_state(deps.as_mut(), env).unwrap();
|
advance_epoch_state(deps.as_mut(), env).unwrap();
|
||||||
|
|
||||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false)
|
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 current_epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||||
let next_epoch = if let Some(state) = current_epoch.state.next() {
|
let next_epoch = if let Some(state) = current_epoch.state.next() {
|
||||||
// We are during DKG process
|
// We are during DKG process
|
||||||
|
let mut new_state = state;
|
||||||
if let EpochState::DealingExchange { resharing } = state {
|
if let EpochState::DealingExchange { resharing } = state {
|
||||||
let current_dealers = current_dealers()
|
let current_dealers = current_dealers()
|
||||||
.keys(deps.storage, None, None, Order::Ascending)
|
.keys(deps.storage, None, None, Order::Ascending)
|
||||||
.collect::<Result<Vec<Addr>, _>>()?;
|
.collect::<Result<Vec<Addr>, _>>()?;
|
||||||
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
|
if current_dealers.is_empty() {
|
||||||
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
|
// If no dealer registered yet, we just stay in the same state until there's at least one
|
||||||
THRESHOLD.save(deps.storage, &threshold)?;
|
new_state = current_epoch.state;
|
||||||
if !resharing {
|
} else {
|
||||||
let replacement_data = InitialReplacementData {
|
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
|
||||||
initial_dealers: current_dealers,
|
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
|
||||||
initial_height: None,
|
THRESHOLD.save(deps.storage, &threshold)?;
|
||||||
};
|
if !resharing {
|
||||||
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
|
let replacement_data = InitialReplacementData {
|
||||||
|
initial_dealers: current_dealers,
|
||||||
|
initial_height: None,
|
||||||
|
};
|
||||||
|
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
Epoch::new(
|
Epoch::new(
|
||||||
state,
|
new_state,
|
||||||
current_epoch.epoch_id,
|
current_epoch.epoch_id,
|
||||||
current_epoch.time_configuration,
|
current_epoch.time_configuration,
|
||||||
env.block.time,
|
env.block.time,
|
||||||
@@ -392,6 +398,14 @@ pub(crate) mod tests {
|
|||||||
EarlyEpochStateAdvancement(1)
|
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
|
// setup dealer details
|
||||||
let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1));
|
let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1));
|
||||||
for details in all_details.iter() {
|
for details in all_details.iter() {
|
||||||
@@ -404,7 +418,7 @@ pub(crate) mod tests {
|
|||||||
.may_load(&deps.storage)
|
.may_load(&deps.storage)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_none());
|
.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();
|
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::contract::instantiate;
|
use crate::contract::instantiate;
|
||||||
|
use crate::dealers::storage::current_dealers;
|
||||||
use coconut_dkg_common::msg::InstantiateMsg;
|
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::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
|
||||||
use cosmwasm_std::{
|
use cosmwasm_std::{
|
||||||
from_binary, to_binary, ContractResult, Empty, MemoryStorage, OwnedDeps, QuerierResult,
|
from_binary, to_binary, Addr, ContractResult, DepsMut, Empty, MemoryStorage, OwnedDeps,
|
||||||
SystemResult, WasmQuery,
|
QuerierResult, SystemResult, WasmQuery,
|
||||||
};
|
};
|
||||||
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
|
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@@ -22,6 +24,22 @@ lazy_static! {
|
|||||||
pub static ref GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(vec![]);
|
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 {
|
fn querier_handler(query: &WasmQuery) -> QuerierResult {
|
||||||
let bin = match query {
|
let bin = match query {
|
||||||
WasmQuery::Smart { contract_addr, msg } => {
|
WasmQuery::Smart { contract_addr, msg } => {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::epoch_state::transactions::advance_epoch_state;
|
use crate::epoch_state::transactions::advance_epoch_state;
|
||||||
use crate::support::tests::helpers;
|
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::dealer::DealerDetails;
|
||||||
use coconut_dkg_common::types::{EpochState, TimeConfiguration};
|
use coconut_dkg_common::types::{EpochState, TimeConfiguration};
|
||||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||||
@@ -104,6 +104,7 @@ mod tests {
|
|||||||
let info = mock_info("requester", &[]);
|
let info = mock_info("requester", &[]);
|
||||||
let share = "share".to_string();
|
let share = "share".to_string();
|
||||||
|
|
||||||
|
add_fixture_dealer(deps.as_mut());
|
||||||
env.block.time = env
|
env.block.time = env
|
||||||
.block
|
.block
|
||||||
.time
|
.time
|
||||||
@@ -171,6 +172,7 @@ mod tests {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
add_fixture_dealer(deps.as_mut());
|
||||||
env.block.time = env
|
env.block.time = env
|
||||||
.block
|
.block
|
||||||
.time
|
.time
|
||||||
@@ -247,6 +249,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_fixture_dealer(deps.as_mut());
|
||||||
env.block.time = env
|
env.block.time = env
|
||||||
.block
|
.block
|
||||||
.time
|
.time
|
||||||
@@ -292,6 +295,7 @@ mod tests {
|
|||||||
let share = "share".to_string();
|
let share = "share".to_string();
|
||||||
let multisig_info = mock_info(MULTISIG_CONTRACT, &[]);
|
let multisig_info = mock_info(MULTISIG_CONTRACT, &[]);
|
||||||
|
|
||||||
|
add_fixture_dealer(deps.as_mut());
|
||||||
env.block.time = env
|
env.block.time = env
|
||||||
.block
|
.block
|
||||||
.time
|
.time
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ name = "mixnet_contract"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-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.1.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"}
|
#nym-config = { path = "../../common/config"}
|
||||||
|
|
||||||
cosmwasm-std = "1.0.0"
|
cosmwasm-std = "1.0.0"
|
||||||
@@ -43,7 +43,7 @@ rand_chacha = "0.2"
|
|||||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[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]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
@@ -4,5 +4,10 @@
|
|||||||
use vergen::{vergen, Config};
|
use vergen::{vergen, Config};
|
||||||
|
|
||||||
fn main() {
|
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 {
|
ContractBuildInformation {
|
||||||
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||||
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
||||||
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
|
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
|
||||||
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
|
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
|
||||||
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
|
.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(),
|
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ name = "vesting_contract"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-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.1.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.1.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 "}
|
cosmwasm-std = { version = "1.0.0 "}
|
||||||
cw-storage-plus = { version = "0.13.4", features = ["iterator"] }
|
cw-storage-plus = { version = "0.13.4", features = ["iterator"] }
|
||||||
@@ -38,4 +38,4 @@ hex = "0.4.3"
|
|||||||
serde_json = "1.0.66"
|
serde_json = "1.0.66"
|
||||||
|
|
||||||
[build-dependencies]
|
[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};
|
use vergen::{vergen, Config};
|
||||||
|
|
||||||
fn main() {
|
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")
|
vergen(Config::default()).expect("failed to extract build metadata")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -743,9 +743,13 @@ pub fn get_contract_version() -> ContractBuildInformation {
|
|||||||
ContractBuildInformation {
|
ContractBuildInformation {
|
||||||
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||||
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
||||||
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
|
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
|
||||||
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
|
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
|
||||||
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
|
.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(),
|
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "explorer-api"
|
name = "explorer-api"
|
||||||
version = "1.1.10"
|
version = "1.1.12"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# 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::mix_nodes::http::mix_nodes_make_default_routes;
|
||||||
use crate::overview::http::overview_make_default_routes;
|
use crate::overview::http::overview_make_default_routes;
|
||||||
use crate::ping::http::ping_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::state::ExplorerApiStateContext;
|
||||||
use crate::validators::http::validators_make_default_routes;
|
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),
|
"/overview" => overview_make_default_routes(&openapi_settings),
|
||||||
"/ping" => ping_make_default_routes(&openapi_settings),
|
"/ping" => ping_make_default_routes(&openapi_settings),
|
||||||
"/validators" => validators_make_default_routes(&openapi_settings),
|
"/validators" => validators_make_default_routes(&openapi_settings),
|
||||||
|
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
|
||||||
};
|
};
|
||||||
|
|
||||||
building_rocket
|
building_rocket
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ mod mix_node;
|
|||||||
pub(crate) mod mix_nodes;
|
pub(crate) mod mix_nodes;
|
||||||
mod overview;
|
mod overview;
|
||||||
mod ping;
|
mod ping;
|
||||||
|
pub(crate) mod service_providers;
|
||||||
mod state;
|
mod state;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
mod validators;
|
mod validators;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use serde::Serialize;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use validator_client::models::SelectionChance;
|
use validator_client::models::{NodePerformance, SelectionChance};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)]
|
#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@@ -36,6 +36,7 @@ pub(crate) struct PrettyDetailedMixNodeBond {
|
|||||||
pub stake_saturation: f32,
|
pub stake_saturation: f32,
|
||||||
pub uncapped_saturation: f32,
|
pub uncapped_saturation: f32,
|
||||||
pub avg_uptime: u8,
|
pub avg_uptime: u8,
|
||||||
|
pub node_performance: NodePerformance,
|
||||||
pub estimated_operator_apy: f64,
|
pub estimated_operator_apy: f64,
|
||||||
pub estimated_delegators_apy: f64,
|
pub estimated_delegators_apy: f64,
|
||||||
pub operating_cost: Coin,
|
pub operating_cost: Coin,
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ impl ThreadsafeMixNodesCache {
|
|||||||
layer: node.mixnode_details.bond_information.layer,
|
layer: node.mixnode_details.bond_information.layer,
|
||||||
mix_node: node.mixnode_details.bond_information.mix_node.clone(),
|
mix_node: node.mixnode_details.bond_information.mix_node.clone(),
|
||||||
avg_uptime: node.performance.round_to_integer(),
|
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,
|
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)
|
uncapped_saturation: best_effort_small_dec_to_f64(node.uncapped_stake_saturation)
|
||||||
as f32,
|
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",
|
"name": "@nym/network-explorer",
|
||||||
"version": "1.0.5",
|
"version": "1.0.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -121,4 +121,4 @@
|
|||||||
"last 1 safari version"
|
"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 BLOCK_API = `${NYM_API_BASE_URL}/block`;
|
||||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
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 = `${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
|
// errors
|
||||||
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
|
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,
|
OVERVIEW_API,
|
||||||
UPTIME_STORY_API,
|
UPTIME_STORY_API,
|
||||||
VALIDATORS_API,
|
VALIDATORS_API,
|
||||||
|
SERVICE_PROVIDERS,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -28,8 +29,8 @@ import {
|
|||||||
ValidatorsResponse,
|
ValidatorsResponse,
|
||||||
GatewayBondAnnotated,
|
GatewayBondAnnotated,
|
||||||
GatewayBond,
|
GatewayBond,
|
||||||
|
DirectoryService,
|
||||||
} from '../typeDefs/explorer-api';
|
} from '../typeDefs/explorer-api';
|
||||||
import { toPercentIntegerString } from '../utils';
|
|
||||||
|
|
||||||
function getFromCache(key: string) {
|
function getFromCache(key: string) {
|
||||||
const ts = Number(localStorage.getItem('ts'));
|
const ts = Number(localStorage.getItem('ts'));
|
||||||
@@ -63,6 +64,7 @@ export class Api {
|
|||||||
if (cachedMixnodes) {
|
if (cachedMixnodes) {
|
||||||
return cachedMixnodes;
|
return cachedMixnodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(MIXNODES_API);
|
const res = await fetch(MIXNODES_API);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
storeInCache('mixnodes', JSON.stringify(json));
|
storeInCache('mixnodes', JSON.stringify(json));
|
||||||
@@ -94,9 +96,9 @@ export class Api {
|
|||||||
static fetchGateways = async (): Promise<GatewayBond[]> => {
|
static fetchGateways = async (): Promise<GatewayBond[]> => {
|
||||||
const res = await fetch(GATEWAYS_API);
|
const res = await fetch(GATEWAYS_API);
|
||||||
const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
|
const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
|
||||||
return gatewaysAnnotated.map(({ gateway_bond, performance }) => ({
|
return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
|
||||||
...gateway_bond,
|
...gateway_bond,
|
||||||
performance: toPercentIntegerString(performance),
|
node_performance,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,4 +150,10 @@ export class Api {
|
|||||||
|
|
||||||
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||||
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
|
(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 * as React from 'react';
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||||
|
|
||||||
@@ -8,14 +7,10 @@ export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltip
|
|||||||
headingTitle,
|
headingTitle,
|
||||||
tooltipInfo,
|
tooltipInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const [filter, toggleFilter] = React.useState<boolean>(false);
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
toggleFilter(!filter);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Box alignItems="center" display="flex" onClick={handleClick}>
|
<Box alignItems="center" display="flex">
|
||||||
{tooltipInfo && (
|
{tooltipInfo && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={tooltipInfo}
|
title={tooltipInfo}
|
||||||
@@ -27,19 +22,9 @@ export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltip
|
|||||||
arrow
|
arrow
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Typography
|
<Typography variant="body2" fontWeight={600} data-testid={headingTitle}>
|
||||||
sx={{
|
{headingTitle}
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: 14,
|
|
||||||
padding: 0,
|
|
||||||
// border: '1px solid red',
|
|
||||||
// minWidth: 300,
|
|
||||||
}}
|
|
||||||
data-testid={headingTitle}
|
|
||||||
>
|
|
||||||
{headingTitle}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
{filter ? <ExpandMore /> : <ExpandLess />}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import * as React from 'react';
|
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 { useTheme } from '@mui/material/styles';
|
||||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||||
@@ -8,13 +18,13 @@ import { cellStyles } from './Universal-DataGrid';
|
|||||||
import { unymToNym } from '../utils/currency';
|
import { unymToNym } from '../utils/currency';
|
||||||
import { GatewayEnrichedRowType } from './Gateways';
|
import { GatewayEnrichedRowType } from './Gateways';
|
||||||
import { MixnodeRowType } from './MixNodes';
|
import { MixnodeRowType } from './MixNodes';
|
||||||
|
import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar';
|
||||||
|
|
||||||
export type ColumnsType = {
|
export type ColumnsType = {
|
||||||
field: string;
|
field: string;
|
||||||
title: string;
|
title: string;
|
||||||
headerAlign: string;
|
headerAlign?: TableCellProps['align'];
|
||||||
flex?: number;
|
width?: string | number;
|
||||||
width?: number;
|
|
||||||
tooltipInfo?: string;
|
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;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,11 +75,11 @@ export const DetailTable: FCWithChildren<{
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
<Table sx={{ minWidth: 1080 }} aria-label={tableName}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
{columnsData?.map(({ field, title, width, tooltipInfo }) => (
|
||||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
{tooltipInfo && (
|
{tooltipInfo && (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { GatewayResponse, GatewayBond, GatewayReportResponse } from '../typeDefs/explorer-api';
|
import { GatewayResponse, GatewayBond, GatewayReportResponse } from '../typeDefs/explorer-api';
|
||||||
|
import { toPercentIntegerString } from '../utils';
|
||||||
|
|
||||||
export type GatewayRowType = {
|
export type GatewayRowType = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -8,7 +9,7 @@ export type GatewayRowType = {
|
|||||||
host: string;
|
host: string;
|
||||||
location: string;
|
location: string;
|
||||||
version: string;
|
version: string;
|
||||||
performance: string;
|
node_performance: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GatewayEnrichedRowType = GatewayRowType & {
|
export type GatewayEnrichedRowType = GatewayRowType & {
|
||||||
@@ -29,7 +30,7 @@ export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowTy
|
|||||||
bond: gw.pledge_amount.amount || 0,
|
bond: gw.pledge_amount.amount || 0,
|
||||||
host: gw.gateway.host || '',
|
host: gw.gateway.host || '',
|
||||||
version: gw.gateway.version || '',
|
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,
|
mixPort: gateway.gateway.mix_port || 0,
|
||||||
routingScore: `${report.most_recent}%`,
|
routingScore: `${report.most_recent}%`,
|
||||||
avgUptime: `${report.last_day || report.last_hour}%`,
|
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]);
|
const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]);
|
||||||
return (
|
return (
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Box display="flex" flexDirection="row" width="100%">
|
<Box display="flex" flexDirection={isMobile ? 'column' : 'row'} width="100%">
|
||||||
<Box
|
<Box
|
||||||
width={72}
|
width={72}
|
||||||
height={72}
|
height={72}
|
||||||
@@ -36,14 +36,14 @@ export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNo
|
|||||||
>
|
>
|
||||||
<Identicon size={43} string={mixNodeRow.identity_key} palette={palette} />
|
<Identicon size={43} string={mixNodeRow.identity_key} palette={palette} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box ml={2}>
|
<Box ml={isMobile ? 0 : 2} mt={isMobile ? 2 : 0}>
|
||||||
<Typography fontSize={21}>{mixnodeDescription.name}</Typography>
|
<Typography fontSize={21}>{mixnodeDescription.name}</Typography>
|
||||||
<Typography>{(mixnodeDescription.description || '').slice(0, 1000)}</Typography>
|
<Typography>{(mixnodeDescription.description || '').slice(0, 1000)}</Typography>
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
variant="text"
|
variant="text"
|
||||||
sx={{
|
sx={{
|
||||||
mt: 4,
|
mt: isMobile ? 2 : 4,
|
||||||
borderRadius: '30px',
|
borderRadius: '30px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@@ -64,15 +64,27 @@ export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNo
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</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">
|
<Box display="flex" flexDirection="column">
|
||||||
<Typography fontWeight="600" alignSelf="self-end">
|
<Typography fontWeight="600" alignSelf={isMobile ? 'start' : 'self-end'}>
|
||||||
Node status:
|
Node status:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box mt={2} alignSelf="self-end">
|
<Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}>
|
||||||
<MixNodeStatus status={mixNodeRow.status} />
|
<MixNodeStatus status={mixNodeRow.status} />
|
||||||
</Box>
|
</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
|
This node is {statusText} in this epoch
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,57 +4,48 @@ export const EconomicsInfoColumns: ColumnsType[] = [
|
|||||||
{
|
{
|
||||||
field: 'estimatedTotalReward',
|
field: 'estimatedTotalReward',
|
||||||
title: 'Estimated Total Reward',
|
title: 'Estimated Total Reward',
|
||||||
flex: 1,
|
width: '15%',
|
||||||
headerAlign: 'left',
|
|
||||||
tooltipInfo:
|
tooltipInfo:
|
||||||
'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.',
|
'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'estimatedOperatorReward',
|
field: 'estimatedOperatorReward',
|
||||||
title: 'Estimated Operator Reward',
|
title: 'Estimated Operator Reward',
|
||||||
flex: 1,
|
width: '15%',
|
||||||
headerAlign: 'left',
|
|
||||||
tooltipInfo:
|
tooltipInfo:
|
||||||
"Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.",
|
"Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'selectionChance',
|
field: 'selectionChance',
|
||||||
title: 'Active Set Probability',
|
title: 'Active Set Probability',
|
||||||
flex: 1,
|
width: '12.5%',
|
||||||
headerAlign: 'left',
|
|
||||||
tooltipInfo:
|
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.',
|
'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',
|
field: 'profitMargin',
|
||||||
title: 'Profit Margin',
|
title: 'Profit Margin',
|
||||||
flex: 1,
|
width: '12.5%',
|
||||||
headerAlign: 'left',
|
|
||||||
tooltipInfo:
|
tooltipInfo:
|
||||||
'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.',
|
'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'operatingCost',
|
field: 'operatingCost',
|
||||||
title: 'Operating Cost',
|
title: 'Operating Cost',
|
||||||
flex: 1,
|
width: '10%',
|
||||||
headerAlign: 'left',
|
|
||||||
tooltipInfo:
|
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.',
|
'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',
|
field: 'avgUptime',
|
||||||
|
title: 'Avg. Score',
|
||||||
|
width: '10%',
|
||||||
|
tooltipInfo: "Mixnode's average routing score in the last 24 hour",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nodePerformance',
|
||||||
title: 'Routing Score',
|
title: 'Routing Score',
|
||||||
flex: 1,
|
|
||||||
headerAlign: 'left',
|
|
||||||
tooltipInfo:
|
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<
|
export const EconomicsProgress: FCWithChildren<
|
||||||
LinearProgressProps & {
|
LinearProgressProps & {
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
|
color: string;
|
||||||
}
|
}
|
||||||
> = ({ threshold, ...props }) => {
|
> = ({ threshold, color, ...props }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { value } = props;
|
const { value } = props;
|
||||||
|
|
||||||
const valueNumber: number = parseToNumber(value);
|
const valueNumber: number = parseToNumber(value);
|
||||||
const thresholdNumber: number = parseToNumber(threshold);
|
const thresholdNumber: number = parseToNumber(threshold);
|
||||||
const percentageColor = valueNumber > (threshold || 100) ? 'warning' : 'inherit';
|
|
||||||
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
|
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,9 +29,9 @@ export const EconomicsProgress: FCWithChildren<
|
|||||||
<LinearProgress
|
<LinearProgress
|
||||||
{...props}
|
{...props}
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
color={percentageColor}
|
color={color}
|
||||||
value={percentageToDisplay}
|
value={percentageToDisplay}
|
||||||
sx={{ width: '100%', borderRadius: '5px', backgroundColor: theme.palette.common.white }}
|
sx={{ width: '100%', borderRadius: '5px' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ const row: EconomicsInfoRowWithIndex = {
|
|||||||
selectionChance: {
|
selectionChance: {
|
||||||
value: 'High',
|
value: 'High',
|
||||||
},
|
},
|
||||||
avgUptime: {
|
|
||||||
value: '65 %',
|
|
||||||
},
|
|
||||||
estimatedOperatorReward: {
|
estimatedOperatorReward: {
|
||||||
value: '80000.123456 NYM',
|
value: '80000.123456 NYM',
|
||||||
},
|
},
|
||||||
@@ -29,9 +27,11 @@ const row: EconomicsInfoRowWithIndex = {
|
|||||||
operatingCost: {
|
operatingCost: {
|
||||||
value: '11121 NYM',
|
value: '11121 NYM',
|
||||||
},
|
},
|
||||||
stakeSaturation: {
|
avgUptime: {
|
||||||
value: '80 %',
|
value: '-',
|
||||||
progressBarValue: 80,
|
},
|
||||||
|
nodePerformance: {
|
||||||
|
value: '-',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,9 +55,7 @@ const emptyRow: EconomicsInfoRowWithIndex = {
|
|||||||
value: '-',
|
value: '-',
|
||||||
progressBarValue: 0,
|
progressBarValue: 0,
|
||||||
},
|
},
|
||||||
avgUptime: {
|
|
||||||
value: '-',
|
|
||||||
},
|
|
||||||
estimatedOperatorReward: {
|
estimatedOperatorReward: {
|
||||||
value: '-',
|
value: '-',
|
||||||
},
|
},
|
||||||
@@ -70,9 +68,11 @@ const emptyRow: EconomicsInfoRowWithIndex = {
|
|||||||
operatingCost: {
|
operatingCost: {
|
||||||
value: '-',
|
value: '-',
|
||||||
},
|
},
|
||||||
stakeSaturation: {
|
avgUptime: {
|
||||||
|
value: '-',
|
||||||
|
},
|
||||||
|
nodePerformance: {
|
||||||
value: '-',
|
value: '-',
|
||||||
progressBarValue: 0,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
|||||||
currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-';
|
currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-';
|
||||||
const estimatedOperatorRewards =
|
const estimatedOperatorRewards =
|
||||||
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
|
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
|
||||||
const stakeSaturation = economicDynamicsStats?.data?.uncapped_saturation || '-';
|
|
||||||
const profitMargin = mixNode?.data?.profit_margin_percent
|
const profitMargin = mixNode?.data?.profit_margin_percent
|
||||||
? toPercentIntegerString(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;
|
const opCost = mixNode?.data?.operating_cost;
|
||||||
|
|
||||||
@@ -33,10 +37,6 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
|||||||
selectionChance: {
|
selectionChance: {
|
||||||
value: selectionChance(economicDynamicsStats),
|
value: selectionChance(economicDynamicsStats),
|
||||||
},
|
},
|
||||||
stakeSaturation: {
|
|
||||||
progressBarValue: typeof stakeSaturation === 'number' ? stakeSaturation * 100 : 0,
|
|
||||||
value: typeof stakeSaturation === 'number' ? `${(stakeSaturation * 100).toFixed(2)} %` : '-',
|
|
||||||
},
|
|
||||||
profitMargin: {
|
profitMargin: {
|
||||||
value: profitMargin ? `${profitMargin} %` : '-',
|
value: profitMargin ? `${profitMargin} %` : '-',
|
||||||
},
|
},
|
||||||
@@ -46,5 +46,8 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
|||||||
avgUptime: {
|
avgUptime: {
|
||||||
value: avgUptime ? `${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 * as React from 'react';
|
||||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
|
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
|
||||||
import { Box } from '@mui/system';
|
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 { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||||
import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types';
|
import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types';
|
||||||
import { EconomicsProgress } from './EconomicsProgress';
|
|
||||||
import { cellStyles } from '../../Universal-DataGrid';
|
|
||||||
import { UniversalTableProps } from '../../DetailTable';
|
import { UniversalTableProps } from '../../DetailTable';
|
||||||
import { useIsMobile } from '../../../hooks/useIsMobile';
|
import { textColour } from '../../../utils';
|
||||||
|
|
||||||
const threshold = 100;
|
const formatCellValues = (value: EconomicsRowsType, field: string) => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
|
||||||
const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => {
|
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
|
||||||
const progressBarValue = value?.progressBarValue || 0;
|
{value.value}
|
||||||
const fieldValue = value.value;
|
</Typography>
|
||||||
|
</Box>
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({
|
export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({
|
||||||
tableName,
|
tableName,
|
||||||
@@ -78,21 +27,19 @@ export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsIn
|
|||||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
{columnsData?.map(({ field, title, tooltipInfo, width }) => (
|
||||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
{tooltipInfo && (
|
{tooltipInfo && (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
<Tooltip
|
||||||
<Tooltip
|
title={tooltipInfo}
|
||||||
title={tooltipInfo}
|
id={field}
|
||||||
id={field}
|
placement="top-start"
|
||||||
placement="top-start"
|
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
maxWidth={230}
|
||||||
maxWidth={230}
|
arrow
|
||||||
arrow
|
/>
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
{title}
|
{title}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -106,19 +53,11 @@ export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsIn
|
|||||||
{columnsData?.map((_, index: number) => {
|
{columnsData?.map((_, index: number) => {
|
||||||
const { field } = columnsData[index];
|
const { field } = columnsData[index];
|
||||||
const value: EconomicsRowsType = (eachRow as any)[field];
|
const value: EconomicsRowsType = (eachRow as any)[field];
|
||||||
|
console.log(value);
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={_.title}
|
key={_.title}
|
||||||
component="th"
|
|
||||||
scope="row"
|
|
||||||
variant="body"
|
|
||||||
sx={{
|
sx={{
|
||||||
...cellStyles,
|
|
||||||
padding: 2,
|
|
||||||
width: 200,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: textColour(value, field, theme),
|
color: textColour(value, field, theme),
|
||||||
}}
|
}}
|
||||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ export type EconomicsRowsType = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface EconomicsInfoRow {
|
type TEconomicsInfoProperties =
|
||||||
estimatedTotalReward: EconomicsRowsType;
|
| 'estimatedTotalReward'
|
||||||
estimatedOperatorReward: EconomicsRowsType;
|
| 'estimatedOperatorReward'
|
||||||
selectionChance: EconomicsRowsType;
|
| 'estimatedOperatorReward'
|
||||||
stakeSaturation: EconomicsRowsType;
|
| 'selectionChance'
|
||||||
profitMargin: EconomicsRowsType;
|
| 'profitMargin'
|
||||||
avgUptime: EconomicsRowsType;
|
| 'avgUptime'
|
||||||
operatingCost: EconomicsRowsType;
|
| 'nodePerformance'
|
||||||
}
|
| 'operatingCost';
|
||||||
|
|
||||||
|
export type EconomicsInfoRow = {
|
||||||
|
[k in TEconomicsInfoProperties]: EconomicsRowsType;
|
||||||
|
};
|
||||||
|
|
||||||
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
|
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
|
||||||
|
|||||||
+6
-4
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable camelcase */
|
/* 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 { toPercentIntegerString } from '../../utils';
|
||||||
import { unymToNym } from '../../utils/currency';
|
import { unymToNym } from '../../utils/currency';
|
||||||
|
|
||||||
@@ -17,8 +17,9 @@ export type MixnodeRowType = {
|
|||||||
layer: string;
|
layer: string;
|
||||||
profit_percentage: string;
|
profit_percentage: string;
|
||||||
avg_uptime: string;
|
avg_uptime: string;
|
||||||
stake_saturation: number;
|
stake_saturation: React.ReactNode;
|
||||||
operating_cost: string;
|
operating_cost: string;
|
||||||
|
node_performance: NodePerformance['most_recent'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
|
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
|
||||||
@@ -46,8 +47,9 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
|
|||||||
host: item?.mix_node?.host || '',
|
host: item?.mix_node?.host || '',
|
||||||
layer: item?.layer || '',
|
layer: item?.layer || '',
|
||||||
profit_percentage: `${profitPercentage}%`,
|
profit_percentage: `${profitPercentage}%`,
|
||||||
avg_uptime: `${item.avg_uptime}%` || '-',
|
avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`,
|
||||||
stake_saturation: uncappedSaturation,
|
stake_saturation: uncappedSaturation.toFixed(2),
|
||||||
operating_cost: `${unymToNym(item.operating_cost?.amount, 6)} NYM`,
|
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 * 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 Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||||
import { Filters } from './Filters/Filters';
|
import { Filters } from './Filters/Filters';
|
||||||
import { useIsMobile } from '../hooks/useIsMobile';
|
import { useIsMobile } from '../hooks/useIsMobile';
|
||||||
@@ -7,10 +7,10 @@ import { useIsMobile } from '../hooks/useIsMobile';
|
|||||||
const fieldsHeight = '42.25px';
|
const fieldsHeight = '42.25px';
|
||||||
|
|
||||||
type TableToolBarProps = {
|
type TableToolBarProps = {
|
||||||
onChangeSearch: (arg: string) => void;
|
onChangeSearch?: (arg: string) => void;
|
||||||
onChangePageSize: (event: SelectChangeEvent<string>) => void;
|
onChangePageSize: (event: SelectChangeEvent<string>) => void;
|
||||||
pageSize: string;
|
pageSize: string;
|
||||||
searchTerm: string;
|
searchTerm?: string;
|
||||||
withFilters?: boolean;
|
withFilters?: boolean;
|
||||||
childrenBefore?: React.ReactNode;
|
childrenBefore?: React.ReactNode;
|
||||||
childrenAfter?: 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', flexDirection: isMobile ? 'column-reverse' : 'row', alignItems: 'middle' }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', height: fieldsHeight }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', height: fieldsHeight }}>
|
||||||
{childrenBefore}
|
{childrenBefore}
|
||||||
<Select
|
<FormControl size="small">
|
||||||
value={pageSize}
|
<Select
|
||||||
onChange={onChangePageSize}
|
value={pageSize}
|
||||||
sx={{
|
onChange={onChangePageSize}
|
||||||
width: isMobile ? '50%' : 200,
|
sx={{
|
||||||
marginRight: isMobile ? 0 : 2,
|
width: isMobile ? '100%' : 200,
|
||||||
}}
|
marginRight: isMobile ? 0 : 2,
|
||||||
>
|
}}
|
||||||
<MenuItem value={10} data-testid="ten">
|
>
|
||||||
10
|
<MenuItem value={10} data-testid="ten">
|
||||||
</MenuItem>
|
10
|
||||||
<MenuItem value={30} data-testid="thirty">
|
</MenuItem>
|
||||||
30
|
<MenuItem value={30} data-testid="thirty">
|
||||||
</MenuItem>
|
30
|
||||||
<MenuItem value={50} data-testid="fifty">
|
</MenuItem>
|
||||||
50
|
<MenuItem value={50} data-testid="fifty">
|
||||||
</MenuItem>
|
50
|
||||||
<MenuItem value={100} data-testid="hundred">
|
</MenuItem>
|
||||||
100
|
<MenuItem value={100} data-testid="hundred">
|
||||||
</MenuItem>
|
100
|
||||||
</Select>
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
<TextField
|
{!!onChangeSearch && (
|
||||||
sx={{
|
<TextField
|
||||||
width: isMobile ? '100%' : 200,
|
sx={{
|
||||||
marginBottom: isMobile ? 2 : 0,
|
width: isMobile ? '100%' : 200,
|
||||||
'& > :not(style)': {
|
marginBottom: isMobile ? 2 : 0,
|
||||||
height: fieldsHeight,
|
}}
|
||||||
},
|
size="small"
|
||||||
}}
|
value={searchTerm}
|
||||||
value={searchTerm}
|
data-testid="search-box"
|
||||||
data-testid="search-box"
|
placeholder="search"
|
||||||
placeholder="search"
|
onChange={(event) => onChangeSearch(event.target.value)}
|
||||||
onChange={(event) => onChangeSearch(event.target.value)}
|
/>
|
||||||
/>
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const CustomPagination = () => {
|
|||||||
color="primary"
|
color="primary"
|
||||||
count={state.pagination.pageCount}
|
count={state.pagination.pageCount}
|
||||||
page={state.pagination.page + 1}
|
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}
|
columns={columns}
|
||||||
pageSize={Number(pageSize)}
|
pageSize={Number(pageSize)}
|
||||||
rowsPerPageOptions={[5]}
|
|
||||||
disableSelectionOnClick
|
disableSelectionOnClick
|
||||||
autoHeight
|
autoHeight
|
||||||
hideFooter={!pagination}
|
hideFooter={!pagination}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ApiState,
|
ApiState,
|
||||||
BlockResponse,
|
BlockResponse,
|
||||||
CountryDataResponse,
|
CountryDataResponse,
|
||||||
|
DirectoryService,
|
||||||
GatewayResponse,
|
GatewayResponse,
|
||||||
MixNodeResponse,
|
MixNodeResponse,
|
||||||
MixnodeStatus,
|
MixnodeStatus,
|
||||||
@@ -24,6 +25,7 @@ interface StateData {
|
|||||||
mode: PaletteMode;
|
mode: PaletteMode;
|
||||||
navState: NavOptionType[];
|
navState: NavOptionType[];
|
||||||
validators?: ApiState<ValidatorsResponse>;
|
validators?: ApiState<ValidatorsResponse>;
|
||||||
|
serviceProviders?: ApiState<DirectoryService>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StateApi {
|
interface StateApi {
|
||||||
@@ -63,6 +65,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
|
|||||||
const [validators, setValidators] = React.useState<ApiState<ValidatorsResponse>>();
|
const [validators, setValidators] = React.useState<ApiState<ValidatorsResponse>>();
|
||||||
const [block, setBlock] = React.useState<ApiState<BlockResponse>>();
|
const [block, setBlock] = React.useState<ApiState<BlockResponse>>();
|
||||||
const [countryData, setCountryData] = React.useState<ApiState<CountryDataResponse>>();
|
const [countryData, setCountryData] = React.useState<ApiState<CountryDataResponse>>();
|
||||||
|
const [serviceProviders, setServiceProviders] = React.useState<ApiState<DirectoryService>>();
|
||||||
|
|
||||||
const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark'));
|
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 updateNavState = (id: number) => {
|
||||||
const updated = navState.map((option) => ({
|
const updated = navState.map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
@@ -165,7 +182,14 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
Promise.all([fetchOverviewSummary(), fetchGateways(), fetchValidators(), fetchBlock(), fetchCountryData()]);
|
Promise.all([
|
||||||
|
fetchOverviewSummary(),
|
||||||
|
fetchGateways(),
|
||||||
|
fetchValidators(),
|
||||||
|
fetchBlock(),
|
||||||
|
fetchCountryData(),
|
||||||
|
fetchServiceProviders(),
|
||||||
|
]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const state = React.useMemo<State>(
|
const state = React.useMemo<State>(
|
||||||
@@ -183,8 +207,20 @@ export const MainContextProvider: FCWithChildren = ({ children }) => {
|
|||||||
toggleMode,
|
toggleMode,
|
||||||
updateNavState,
|
updateNavState,
|
||||||
validators,
|
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>;
|
return <MainContext.Provider value={state}>{children}</MainContext.Provider>;
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ export const originalNavOptions: NavOptionType[] = [
|
|||||||
url: `${BIG_DIPPER}/validators`,
|
url: `${BIG_DIPPER}/validators`,
|
||||||
title: 'Validators',
|
title: 'Validators',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
url: 'network-components/service-providers',
|
||||||
|
title: 'Service Providers',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,23 +22,20 @@ const columns: ColumnsType[] = [
|
|||||||
{
|
{
|
||||||
field: 'bond',
|
field: 'bond',
|
||||||
title: 'Bond',
|
title: 'Bond',
|
||||||
flex: 1,
|
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'routingScore',
|
field: 'node_performance',
|
||||||
title: 'Routing Score',
|
title: 'Routing Score',
|
||||||
flex: 1,
|
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
tooltipInfo:
|
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',
|
field: 'avgUptime',
|
||||||
title: 'Avg. Score',
|
title: 'Avg. Score',
|
||||||
flex: 1,
|
|
||||||
headerAlign: 'left',
|
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',
|
field: 'host',
|
||||||
@@ -50,19 +47,16 @@ const columns: ColumnsType[] = [
|
|||||||
field: 'location',
|
field: 'location',
|
||||||
title: 'Location',
|
title: 'Location',
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'owner',
|
field: 'owner',
|
||||||
title: 'Owner',
|
title: 'Owner',
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'version',
|
field: 'version',
|
||||||
title: 'Version',
|
title: 'Version',
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -130,13 +124,13 @@ const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: Gat
|
|||||||
* Guard component to handle loading and not found states
|
* Guard component to handle loading and not found states
|
||||||
*/
|
*/
|
||||||
const PageGatewayDetailGuard: FCWithChildren = () => {
|
const PageGatewayDetailGuard: FCWithChildren = () => {
|
||||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond | undefined>();
|
const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>();
|
||||||
const { gateways } = useMainContext();
|
const { gateways } = useMainContext();
|
||||||
const { id } = useParams<{ id: string | undefined }>();
|
const { id } = useParams<{ id: string | undefined }>();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (gateways?.data) {
|
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]);
|
}, [gateways, id]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link as RRDLink } from 'react-router-dom';
|
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 { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||||
import { SelectChangeEvent } from '@mui/material/Select';
|
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 { useMainContext } from '../../context/main';
|
||||||
import { gatewayToGridRow } from '../../components/Gateways';
|
import { gatewayToGridRow } from '../../components/Gateways';
|
||||||
import { GatewayResponse } from '../../typeDefs/explorer-api';
|
import { GatewayResponse } from '../../typeDefs/explorer-api';
|
||||||
@@ -25,6 +27,8 @@ export const PageGateways: FCWithChildren = () => {
|
|||||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||||
const [versionFilter, setVersionFilter] = React.useState<VersionSelectOptions>(VersionSelectOptions.latestVersion);
|
const [versionFilter, setVersionFilter] = React.useState<VersionSelectOptions>(VersionSelectOptions.latestVersion);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const handleSearch = (str: string) => {
|
const handleSearch = (str: string) => {
|
||||||
setSearchTerm(str.toLowerCase());
|
setSearchTerm(str.toLowerCase());
|
||||||
};
|
};
|
||||||
@@ -86,10 +90,10 @@ export const PageGateways: FCWithChildren = () => {
|
|||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
field: 'identity_key',
|
field: 'identity_key',
|
||||||
headerName: 'Identity Key',
|
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
width: 380,
|
width: 380,
|
||||||
|
disableColumnMenu: true,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<>
|
<>
|
||||||
@@ -109,35 +113,30 @@ export const PageGateways: FCWithChildren = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'bond',
|
field: 'node_performance',
|
||||||
width: 150,
|
renderHeader: () => (
|
||||||
type: 'number',
|
<>
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
<InfoTooltip
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
id="gateways-list-routing-score"
|
||||||
headerAlign: 'left',
|
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"
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
placement="top-start"
|
||||||
<MuiLink
|
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||||
sx={{ ...cellStyles }}
|
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||||
component={RRDLink}
|
maxWidth={230}
|
||||||
to={`/network-components/gateway/${params.row.identityKey}`}
|
arrow
|
||||||
data-testid="pledge-amount"
|
/>
|
||||||
>
|
<CustomColumnHeading headingTitle="Routing Score" />
|
||||||
{unymToNym(params.value, 6)}
|
</>
|
||||||
</MuiLink>
|
|
||||||
),
|
),
|
||||||
},
|
width: 175,
|
||||||
{
|
disableColumnMenu: true,
|
||||||
field: 'performance',
|
|
||||||
headerName: 'Routing Score',
|
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Routing Score" />,
|
|
||||||
width: 150,
|
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<MuiLink
|
<MuiLink
|
||||||
sx={{ ...cellStyles }}
|
sx={{ ...cellStyles }}
|
||||||
component={RRDLink}
|
component={RRDLink}
|
||||||
to={`/network-components/gateway/${params.row.identityKey}`}
|
to={`/network-components/gateway/${params.row.identity_key}`}
|
||||||
data-testid="pledge-amount"
|
data-testid="pledge-amount"
|
||||||
>
|
>
|
||||||
{`${params.value}%`}
|
{`${params.value}%`}
|
||||||
@@ -145,32 +144,38 @@ export const PageGateways: FCWithChildren = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'host',
|
field: 'version',
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
|
||||||
width: 180,
|
width: 150,
|
||||||
|
disableColumnMenu: true,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<MuiLink
|
<MuiLink
|
||||||
sx={{ ...cellStyles }}
|
sx={{ ...cellStyles }}
|
||||||
component={RRDLink}
|
href={`${NYM_BIG_DIPPER}/account/${params.value}`}
|
||||||
to={`/network-components/gateway/${params.row.identityKey}`}
|
target="_blank"
|
||||||
data-testid="host"
|
data-testid="owner"
|
||||||
>
|
>
|
||||||
{params.value}
|
{params.value}
|
||||||
</MuiLink>
|
</MuiLink>
|
||||||
),
|
),
|
||||||
|
sortComparator: (a, b) => {
|
||||||
|
if (gte(a, b)) return 1;
|
||||||
|
return -1;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'location',
|
field: 'location',
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||||
width: 180,
|
width: 180,
|
||||||
|
disableColumnMenu: true,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<Button
|
<Box
|
||||||
onClick={() => handleSearch(params.value as string)}
|
onClick={() => handleSearch(params.value as string)}
|
||||||
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
|
sx={{ ...cellStyles, justifyContent: 'flex-start', cursor: 'pointer' }}
|
||||||
data-testid="location-button"
|
data-testid="location-button"
|
||||||
>
|
>
|
||||||
<Tooltip text={params.value} id="gateway-location-text">
|
<Tooltip text={params.value} id="gateway-location-text">
|
||||||
@@ -184,7 +189,25 @@ export const PageGateways: FCWithChildren = () => {
|
|||||||
{params.value}
|
{params.value}
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</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',
|
headerName: 'Owner',
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||||
width: 180,
|
width: 180,
|
||||||
|
disableColumnMenu: true,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
@@ -206,20 +230,21 @@ export const PageGateways: FCWithChildren = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'version',
|
field: 'bond',
|
||||||
headerName: 'Version',
|
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
|
|
||||||
width: 150,
|
width: 150,
|
||||||
headerAlign: 'left',
|
disableColumnMenu: true,
|
||||||
|
type: 'number',
|
||||||
|
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
|
headerAlign: 'left',
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<MuiLink
|
<MuiLink
|
||||||
sx={{ ...cellStyles }}
|
sx={{ ...cellStyles }}
|
||||||
href={`${NYM_BIG_DIPPER}/account/${params.value}`}
|
component={RRDLink}
|
||||||
target="_blank"
|
to={`/network-components/gateway/${params.row.identity_key}`}
|
||||||
data-testid="owner"
|
data-testid="pledge-amount"
|
||||||
>
|
>
|
||||||
{params.value}
|
{unymToNym(params.value, 6)}
|
||||||
</MuiLink>
|
</MuiLink>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,45 +17,45 @@ const columns: ColumnsType[] = [
|
|||||||
{
|
{
|
||||||
field: 'owner',
|
field: 'owner',
|
||||||
title: 'Owner',
|
title: 'Owner',
|
||||||
headerAlign: 'left',
|
width: '15%',
|
||||||
width: 230,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'identity_key',
|
field: 'identity_key',
|
||||||
title: 'Identity Key',
|
title: 'Identity Key',
|
||||||
headerAlign: 'left',
|
width: '15%',
|
||||||
width: 230,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'bond',
|
field: 'bond',
|
||||||
title: 'Stake',
|
title: 'Stake',
|
||||||
flex: 1,
|
width: '12.5%',
|
||||||
headerAlign: 'left',
|
},
|
||||||
|
{
|
||||||
|
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',
|
field: 'self_percentage',
|
||||||
|
width: '10%',
|
||||||
title: 'Bond %',
|
title: 'Bond %',
|
||||||
headerAlign: 'left',
|
|
||||||
width: 99,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'host',
|
field: 'host',
|
||||||
|
width: '10%',
|
||||||
title: 'Host',
|
title: 'Host',
|
||||||
headerAlign: 'left',
|
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'location',
|
field: 'location',
|
||||||
title: 'Location',
|
title: 'Location',
|
||||||
headerAlign: 'left',
|
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'layer',
|
field: 'layer',
|
||||||
title: 'Layer',
|
title: 'Layer',
|
||||||
headerAlign: 'left',
|
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -64,11 +64,10 @@ const columns: ColumnsType[] = [
|
|||||||
*/
|
*/
|
||||||
const PageMixnodeDetailWithState: FCWithChildren = () => {
|
const PageMixnodeDetailWithState: FCWithChildren = () => {
|
||||||
const { mixNode, mixNodeRow, description, stats, status, uptimeStory, uniqDelegations } = useMixnodeContext();
|
const { mixNode, mixNodeRow, description, stats, status, uptimeStory, uniqDelegations } = useMixnodeContext();
|
||||||
|
console.log(mixNodeRow);
|
||||||
return (
|
return (
|
||||||
<Box component="main">
|
<Box component="main">
|
||||||
<Title text="Mixnode Detail" />
|
<Title text="Mixnode Detail" />
|
||||||
|
|
||||||
<Grid container spacing={2} mt={1} mb={6}>
|
<Grid container spacing={2} mt={1} mb={6}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
{mixNodeRow && description?.data && (
|
{mixNodeRow && description?.data && (
|
||||||
@@ -76,13 +75,11 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
|
|||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<DetailTable columnsData={columns} tableName="Mixnode detail table" rows={mixNodeRow ? [mixNodeRow] : []} />
|
<DetailTable columnsData={columns} tableName="Mixnode detail table" rows={mixNodeRow ? [mixNodeRow] : []} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={2} mt={0}>
|
<Grid container spacing={2} mt={0}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<DelegatorsInfoTable
|
<DelegatorsInfoTable
|
||||||
@@ -92,7 +89,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={2} mt={0}>
|
<Grid container spacing={2} mt={0}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<ContentCard title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`}>
|
<ContentCard title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`}>
|
||||||
@@ -100,7 +96,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
|
|||||||
</ContentCard>
|
</ContentCard>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={2} mt={0}>
|
<Grid container spacing={2} mt={0}>
|
||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<ContentCard title="Mixnode Stats">
|
<ContentCard title="Mixnode Stats">
|
||||||
@@ -144,7 +139,6 @@ const PageMixnodeDetailWithState: FCWithChildren = () => {
|
|||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container spacing={2} mt={0}>
|
<Grid container spacing={2} mt={0}>
|
||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
{status && (
|
{status && (
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
{
|
{
|
||||||
field: 'mix_id',
|
field: 'mix_id',
|
||||||
headerName: 'Mix ID',
|
headerName: 'Mix ID',
|
||||||
|
disableColumnMenu: true,
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />,
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
width: 100,
|
width: 100,
|
||||||
@@ -103,6 +104,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
{
|
{
|
||||||
field: 'identity_key',
|
field: 'identity_key',
|
||||||
headerName: 'Identity Key',
|
headerName: 'Identity Key',
|
||||||
|
disableColumnMenu: true,
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
width: 170,
|
width: 170,
|
||||||
@@ -128,6 +130,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
{
|
{
|
||||||
field: 'bond',
|
field: 'bond',
|
||||||
headerName: 'Stake',
|
headerName: 'Stake',
|
||||||
|
disableColumnMenu: true,
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Stake" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Stake" />,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
@@ -146,6 +149,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
{
|
{
|
||||||
field: 'stake_saturation',
|
field: 'stake_saturation',
|
||||||
headerName: 'Stake Saturation',
|
headerName: 'Stake Saturation',
|
||||||
|
disableColumnMenu: true,
|
||||||
renderHeader: () => (
|
renderHeader: () => (
|
||||||
<CustomColumnHeading
|
<CustomColumnHeading
|
||||||
headingTitle="Stake Saturation"
|
headingTitle="Stake Saturation"
|
||||||
@@ -164,13 +168,14 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
component={RRDLink}
|
component={RRDLink}
|
||||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||||
>
|
>
|
||||||
{`${params.value.toFixed(2)} %`}
|
{`${params.value} %`}
|
||||||
</MuiLink>
|
</MuiLink>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'pledge_amount',
|
field: 'pledge_amount',
|
||||||
headerName: 'Bond',
|
headerName: 'Bond',
|
||||||
|
disableColumnMenu: true,
|
||||||
width: 175,
|
width: 175,
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />,
|
||||||
@@ -189,6 +194,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
{
|
{
|
||||||
field: 'profit_percentage',
|
field: 'profit_percentage',
|
||||||
headerName: 'Profit Margin',
|
headerName: 'Profit Margin',
|
||||||
|
disableColumnMenu: true,
|
||||||
renderHeader: () => (
|
renderHeader: () => (
|
||||||
<CustomColumnHeading
|
<CustomColumnHeading
|
||||||
headingTitle="Profit Margin"
|
headingTitle="Profit Margin"
|
||||||
@@ -220,6 +226,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
width: 170,
|
width: 170,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
|
disableColumnMenu: true,
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<MuiLink
|
<MuiLink
|
||||||
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
||||||
@@ -231,12 +238,13 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'avg_uptime',
|
field: 'node_performance',
|
||||||
headerName: 'Routing Score',
|
headerName: 'Routing Score',
|
||||||
|
disableColumnMenu: true,
|
||||||
renderHeader: () => (
|
renderHeader: () => (
|
||||||
<CustomColumnHeading
|
<CustomColumnHeading
|
||||||
headingTitle="Routing Score"
|
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',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
@@ -255,6 +263,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
{
|
{
|
||||||
field: 'owner',
|
field: 'owner',
|
||||||
headerName: 'Owner',
|
headerName: 'Owner',
|
||||||
|
disableColumnMenu: true,
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||||
width: 120,
|
width: 120,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
@@ -274,6 +283,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
field: 'location',
|
field: 'location',
|
||||||
headerName: 'Location',
|
headerName: 'Location',
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||||
|
disableColumnMenu: true,
|
||||||
width: 120,
|
width: 120,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
@@ -303,6 +313,7 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
field: 'host',
|
field: 'host',
|
||||||
headerName: 'Host',
|
headerName: 'Host',
|
||||||
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
|
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
|
||||||
|
disableColumnMenu: true,
|
||||||
headerClassName: 'MuiDataGrid-header-override',
|
headerClassName: 'MuiDataGrid-header-override',
|
||||||
width: 130,
|
width: 130,
|
||||||
headerAlign: 'left',
|
headerAlign: 'left',
|
||||||
@@ -321,7 +332,6 @@ export const PageMixnodes: FCWithChildren = () => {
|
|||||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||||
setPageSize(event.target.value);
|
setPageSize(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title text="Mixnodes" />
|
<Title text="Mixnodes" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Box, Grid, Link, Typography } from '@mui/material';
|
|||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { PeopleAlt } from '@mui/icons-material';
|
||||||
import { WorldMap } from '../../components/WorldMap';
|
import { WorldMap } from '../../components/WorldMap';
|
||||||
import { useMainContext } from '../../context/main';
|
import { useMainContext } from '../../context/main';
|
||||||
import { formatNumber } from '../../utils';
|
import { formatNumber } from '../../utils';
|
||||||
@@ -18,7 +19,7 @@ import { Icons } from '../../components/Icons';
|
|||||||
export const PageOverview: FCWithChildren = () => {
|
export const PageOverview: FCWithChildren = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { summaryOverview, gateways, validators, block, countryData } = useMainContext();
|
const { summaryOverview, gateways, validators, block, countryData, serviceProviders } = useMainContext();
|
||||||
return (
|
return (
|
||||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -61,7 +62,7 @@ export const PageOverview: FCWithChildren = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{gateways && (
|
{gateways && (
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={4}>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
onClick={() => navigate('/network-components/gateways')}
|
onClick={() => navigate('/network-components/gateways')}
|
||||||
title="Gateways"
|
title="Gateways"
|
||||||
@@ -71,9 +72,19 @@ export const PageOverview: FCWithChildren = () => {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</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 && (
|
{validators && (
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={4}>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
|
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
|
||||||
title="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 { PageGatewayDetail } from '../pages/GatewayDetail';
|
||||||
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
|
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
|
||||||
import { PageMixnodes } from '../pages/Mixnodes';
|
import { PageMixnodes } from '../pages/Mixnodes';
|
||||||
|
import { ServiceProviders } from '../pages/ServiceProviders';
|
||||||
|
|
||||||
const ValidatorRoute: FCWithChildren = () => {
|
const ValidatorRoute: FCWithChildren = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -21,6 +22,6 @@ export const NetworkComponentsRoutes: FCWithChildren = () => (
|
|||||||
<Route path="gateways" element={<PageGateways />} />
|
<Route path="gateways" element={<PageGateways />} />
|
||||||
<Route path="gateway/:id" element={<PageGatewayDetail />} />
|
<Route path="gateway/:id" element={<PageGatewayDetail />} />
|
||||||
<Route path="validators" element={<ValidatorRoute />} />
|
<Route path="validators" element={<ValidatorRoute />} />
|
||||||
<Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} />
|
<Route path="service-providers" element={<ServiceProviders />} />
|
||||||
</ReactRouterRoutes>
|
</ReactRouterRoutes>
|
||||||
);
|
);
|
||||||
|
|||||||
+8
-12
@@ -4,31 +4,27 @@
|
|||||||
the theme declaration in index.tsx or the style prop
|
the theme declaration in index.tsx or the style prop
|
||||||
in <DataGrid /> */
|
in <DataGrid /> */
|
||||||
|
|
||||||
.MuiDataGrid-sortIcon, .MuiDataGrid-menuIcon, .MuiDataGrid-iconButtonContainer{
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiDataGrid-columnSeparator {
|
.MuiDataGrid-columnSeparator {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO - this should be managed somehow in MUI DataGrid
|
/* TODO - this should be managed somehow in MUI DataGrid
|
||||||
but documentation doesnt offer a way to do it. Possibly only
|
but documentation doesnt offer a way to do it. Possibly only
|
||||||
included in Data Grid Pro package */
|
included in Data Grid Pro package */
|
||||||
.MuiDataGrid-header-override {
|
.MuiDataGrid-header-override {
|
||||||
height: 55px;
|
height: 55px;
|
||||||
min-height: 55px;
|
min-height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Again, no offered way to add sx to this specific div to kill the padding
|
/* 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 */
|
which puts it out of sync with other (sx styled) cells */
|
||||||
div div.MuiDataGrid-root .MuiDataGrid-columnHeaderTitleContainer {
|
div div.MuiDataGrid-root .MuiDataGrid-columnHeaderTitleContainer {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
.MuiDrawer-paperAnchorLeft {
|
.MuiDrawer-paperAnchorLeft {
|
||||||
min-width: 100vw;
|
min-width: 100vw;
|
||||||
margin-top: 58px;
|
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