Compare commits

...

41 Commits

Author SHA1 Message Date
farbanas 03e082725e chore: update changelogs for release 2023-03-07 14:30:38 +01:00
farbanas 3e3307887e chore: bumped versions for release 2023-03-07 14:19:45 +01:00
farbanas d85683aaa8 chore: bumped version of contracts 2023-03-07 13:49:03 +01:00
farbanas e89ed985fc chore: bumped version of common crates 2023-03-07 13:27:03 +01:00
Jon Häggblad 45ebd7c37a Add list of published common crates 2023-03-07 09:58:07 +01:00
Jon Häggblad 3506020e55 Update Cargo.lock (#3152) 2023-03-07 09:57:09 +01:00
Pierre Dommerc eca77d684b feat(wallet): send - add user fees settings and memo field (#3146)
* feat(wallet): send - add user fees settings, memo field

* fix(wallet): send, custom fees validation
2023-03-06 18:26:07 +01:00
Jon Häggblad dc4353c682 Fix docs.rs build (#3145)
* contracts: fix docs.rs build

* Cargo.lock

* rustfmt

* vesting: option_env for git info

* Change to vergen =7.4.3
2023-03-06 16:38:14 +01:00
Fouad e1d8069967 Explorer Service Provider List (#3142)
* create service providers route

* make request for well known service providers

* fetch and display service providers

* service provider overview

handle undefined data

fix linting

fix type

* use full width column
2023-03-06 16:36:55 +01:00
farbanas 060726b7a3 fix: add step to install wasm-opt to our CI for building and uploading binaries 2023-03-06 16:35:31 +01:00
Fouad f72ccb0f0d Update tooltips for routing and average score (#3133)
* update tooltips for routing and average score

* fix up table alignment

fix lint errors

* add node_performance to explorer api response for mixnodes

* use mixnode node_performance for avg and lastest values

* move stake sat to top table

fix lint errors

* update stake saturation text color
2023-03-06 15:58:33 +01:00
Jędrzej Stuczyński 2ff6bfbdd8 feat: ability to inject custom topology into clients (#3055)
* ability to specify custom TopologyProvider in TopologyRefresher

* topology provider builder method for base client

* ability to take manual control over topology

* wasm fixes

* added topology injection to nym-sdk API

* added examples to nym-sdk and exposed additional helper methods
2023-03-06 13:50:06 +00:00
Bogdan-Ștefan Neacșu d4f0b4772b Fix Service stats address 2023-03-06 14:00:14 +02:00
Pierre Dommerc 04e652441e fix(explorer): fix layout responsiveness in mixnode details view (#3130) 2023-03-01 16:29:47 +01:00
Fouad ccf5990bc7 update selected service provider description style (#3128) 2023-03-01 16:22:52 +01:00
Fouad 3d7c9ee2b8 NE - fix gateways' version number sorting on the gateway list page (#3131)
* fix filter input sizes

* reorder gateway columns

* update tooltips

* fix column sorting in explorer lists

fix lint errors
2023-03-01 16:21:33 +01:00
farbanas bc5bb271d8 feat: remove unused workflows, update a couple of if conditions in github workflows and add the command for optimizing contract sizes to Makefile 2023-02-28 17:26:39 +01:00
farbanas 158e3cb073 Merge branch 'master' into develop 2023-02-28 17:09:58 +01:00
farbanas 36fb0eba29 Update versions as part of release/v1.1.11 2023-02-28 13:53:14 +01:00
farbanas 1b37e85418 Update changelogs as part of release/v1.1.11 2023-02-28 13:43:32 +01:00
Bogdan-Ștefan Neacşu 6a93497c8f Extend public key submission in case no dealer registered (#3106) 2023-02-28 12:58:01 +01:00
Tommy Verrall b8ee3465f8 Update build_and_run.sh
remove unnecessary environment variable
2023-02-28 10:33:45 +01:00
Mark Sinclair a7dfb36a84 Merge pull request #3111 from nymtech/feature/sdk-1.1.7
Typescript SDK 1.1.7
2023-02-27 18:07:08 +00:00
Mark Sinclair 14a7b5bdc8 Typescript SDK 1.1.7 2023-02-27 17:55:41 +00:00
Tommy Verrall ffbd76539a Merge pull request #3108 from nymtech/feature/nym-connect-select-sp
Feature/nym connect select sp
2023-02-27 17:48:31 +02:00
fmtabbara bdcc19e86a PR update 2023-02-27 15:33:54 +00:00
fmtabbara 7929bac685 turn off user defined SP address is input it empty 2023-02-27 14:20:16 +00:00
Jon Häggblad f590aad42c nym-api: uptime rework (#3053)
* nym-api: cache updates as node performance

* nym-api: update get mixnode avg_uptime endpoint

* nym-api: mixnode report to use cached data

* nym-api: annotate gateway bond with node performance

* nym-api: gateway report to use cached data

* wip

* Add get_gateway_avg_uptime

* Add comment

* update NR gateways to include node_performance on frontend

* use node_performance values on frontend

* fixup select gateway from list

* fix up lint errors

---------

Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
2023-02-27 12:40:00 +01:00
fmtabbara ec23f3dcb7 disable input when connected
fix lint errors
2023-02-27 11:28:38 +00:00
fmtabbara 9644eb4329 pick service provider 2023-02-27 11:28:38 +00:00
fmtabbara 7a4c6e4ed4 storage type
update types
2023-02-27 11:28:36 +00:00
fmtabbara d5ad504104 reuseable storage functions 2023-02-27 11:28:36 +00:00
Tommy Verrall d684f6d7ae Merge pull request #3099 from nymtech/feature/nym-connect-select-sp
NymConnect Select a service provider
2023-02-27 12:26:00 +02:00
Bogdan-Ștefan Neacşu f0d9703587 Feature/fix db name collision (#3103)
* Fix db naming collision

* Increase gateway timeout to accommodate bandwidth spending time
2023-02-24 16:52:56 +02:00
fmtabbara c08efef8ed add autofocus to IdentityKeyComponent 2023-02-23 22:58:59 +00:00
fmtabbara 6d29774744 style tweaks 2023-02-23 22:57:21 +00:00
fmtabbara af6bab7703 fix lint error 2023-02-23 17:40:39 +00:00
fmtabbara 7033f92d82 add UI for picking service provider 2023-02-23 17:33:03 +00:00
Jędrzej Stuczyński 128dfa6d81 Feature/latency based gateway selection (#3081)
* wip

* new option to select gateways based on latency

* further changes for wasm-compatibility

* post rebase fixes + clippy

I know, I should have probably included them properly during rebasing ¯\_(ツ)_/¯

* android change

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

Some files were not shown because too many files have changed in this diff Show More