diff --git a/.github/workflows/ci-check-ns-api-version.yml b/.github/workflows/ci-check-ns-api-version.yml index 92acb50395..00ef062fc3 100644 --- a/.github/workflows/ci-check-ns-api-version.yml +++ b/.github/workflows/ci-check-ns-api-version.yml @@ -44,8 +44,10 @@ jobs: echo "Tag is empty" exit 1 fi + # first, list all tags for logging purposes curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq - exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' ) + # check if there's a matching tag + exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' ) if [[ $exists = "true" ]]; then echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo" exit 1 @@ -53,5 +55,5 @@ jobs: echo "Version '$TAG' doesn't exist on the remote" else echo "Unknown output '$exists'" - exit 1 + exit 2 fi diff --git a/.github/workflows/ci-check-nym-stats-api-version.yml b/.github/workflows/ci-check-nym-stats-api-version.yml index 2bcb0bc87e..6f3eb75770 100644 --- a/.github/workflows/ci-check-nym-stats-api-version.yml +++ b/.github/workflows/ci-check-nym-stats-api-version.yml @@ -44,8 +44,10 @@ jobs: echo "Tag is empty" exit 1 fi + # first, list all tags for logging purposes curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq - exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' ) + # check if there's a matching tag + exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' ) if [[ $exists = "true" ]]; then echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo" exit 1 @@ -53,5 +55,5 @@ jobs: echo "Version '$TAG' doesn't exist on the remote" else echo "Unknown output '$exists'" - exit 1 + exit 2 fi diff --git a/.github/workflows/ci-contracts-upload-binaries.yml b/.github/workflows/ci-contracts-upload-binaries.yml index c85d9046db..6cd620bcb1 100644 --- a/.github/workflows/ci-contracts-upload-binaries.yml +++ b/.github/workflows/ci-contracts-upload-binaries.yml @@ -57,6 +57,7 @@ jobs: cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR + cp contracts/target/wasm32-unknown-unknown/release/nym_performance_contract.wasm $OUTPUT_DIR - name: Deploy branch to CI www continue-on-error: true diff --git a/.github/workflows/publish-nym-binaries.yml b/.github/workflows/publish-nym-binaries.yml index fef9420435..5ceb1477b2 100644 --- a/.github/workflows/publish-nym-binaries.yml +++ b/.github/workflows/publish-nym-binaries.yml @@ -19,7 +19,11 @@ jobs: if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }} strategy: fail-fast: false - runs-on: arc-ubuntu-22.04 + matrix: + include: + - os: arc-ubuntu-22.04 + target: x86_64-unknown-linux-gnu + runs-on: ${{ matrix.os }} outputs: release_id: ${{ steps.create-release.outputs.id }} diff --git a/Cargo.lock b/Cargo.lock index 88187f611b..3df4e8a6d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,6 +1923,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cw-multi-test" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533b31c94b9e10e77e2468a2b1559aa506505d18c4e52eb64cbfc624ca876ad2" +dependencies = [ + "anyhow", + "bech32", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "itertools 0.14.0", + "prost 0.13.5", + "schemars", + "serde", + "sha2 0.10.9", + "thiserror 2.0.12", +] + [[package]] name = "cw-storage-plus" version = "2.0.0" @@ -5302,6 +5322,7 @@ dependencies = [ "nym-sphinx 0.1.0", "nym-task 0.1.0", "sqlx", + "sqlx-pool-guard", "thiserror 2.0.12", "time", "tokio", @@ -5447,6 +5468,7 @@ dependencies = [ name = "nym-contracts-common" version = "0.5.0" dependencies = [ + "anyhow", "bs58", "cosmwasm-schema", "cosmwasm-std", @@ -5474,6 +5496,19 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-contracts-common-testing" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", +] + [[package]] name = "nym-cpp-ffi" version = "0.1.2" @@ -5570,6 +5605,7 @@ dependencies = [ "nym-ecash-time 0.1.0", "serde", "sqlx", + "sqlx-pool-guard", "thiserror 2.0.12", "tokio", "zeroize", @@ -6284,7 +6320,6 @@ dependencies = [ "schemars", "semver 1.0.26", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -6647,7 +6682,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "3.0.0" +version = "3.1.1" dependencies = [ "ammonia", "anyhow", @@ -6889,6 +6924,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common 0.5.0", + "schemars", + "serde", + "thiserror 2.0.12", +] + [[package]] name = "nym-pool-contract-common" version = "0.1.0" @@ -7436,7 +7484,7 @@ dependencies = [ [[package]] name = "nym-statistics-api" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "axum 0.7.9", @@ -7708,6 +7756,7 @@ dependencies = [ "nym-mixnet-contract-common 0.6.0", "nym-multisig-contract-common 0.1.0", "nym-network-defaults 0.1.0", + "nym-performance-contract-common", "nym-serde-helpers 0.1.0", "nym-vesting-contract-common 0.7.0", "prost 0.13.5", @@ -8689,6 +8738,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc_pidinfo" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af53dad2390f8df98dda1e4188322bdf2f91c86cf6001f51d10d64451edf463a" +dependencies = [ + "libc", +] + [[package]] name = "prometheus" version = "0.14.0" @@ -10276,6 +10334,19 @@ dependencies = [ "whoami", ] +[[package]] +name = "sqlx-pool-guard" +version = "0.1.0" +dependencies = [ + "proc_pidinfo", + "sqlx", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "windows 0.61.1", +] + [[package]] name = "sqlx-postgres" version = "0.8.6" @@ -10732,6 +10803,7 @@ dependencies = [ "nym-mixnet-contract-common 0.6.0", "nym-multisig-contract-common 0.1.0", "nym-pemstore 0.3.0", + "nym-performance-contract-common", "nym-validator-client 0.1.0", "nym-vesting-contract-common 0.7.0", "rand 0.8.5", @@ -12291,6 +12363,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -12325,6 +12419,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -12347,6 +12465,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "windows-interface" version = "0.57.0" @@ -12370,10 +12499,31 @@ dependencies = [ ] [[package]] -name = "windows-link" -version = "0.1.0" +name = "windows-interface" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] [[package]] name = "windows-registry" @@ -12381,7 +12531,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.3.1", + "windows-result 0.3.4", "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -12406,9 +12556,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] @@ -12432,6 +12582,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -12530,6 +12689,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 36d8f575d0..8b3404c545 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,11 +34,12 @@ members = [ "common/config", "common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/contracts-common", + "common/cosmwasm-smart-contracts/contracts-common-testing", "common/cosmwasm-smart-contracts/easy_addr", "common/cosmwasm-smart-contracts/ecash-contract", "common/cosmwasm-smart-contracts/group-contract", "common/cosmwasm-smart-contracts/mixnet-contract", - "common/cosmwasm-smart-contracts/multisig-contract", + "common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract", "common/cosmwasm-smart-contracts/nym-pool-contract", "common/cosmwasm-smart-contracts/vesting-contract", "common/credential-storage", @@ -126,6 +127,7 @@ members = [ "service-providers/common", "service-providers/ip-packet-router", "service-providers/network-requester", + "sqlx-pool-guard", "tools/echo-server", "tools/internal/contract-state-importer/importer-cli", "tools/internal/contract-state-importer/importer-contract", @@ -135,7 +137,6 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", - "tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check", "tools/nym-cli", "tools/nym-id-cli", @@ -287,6 +288,7 @@ petgraph = "0.6.5" pin-project = "1.1" pin-project-lite = "0.2.16" publicsuffix = "2.3.0" +proc_pidinfo = "0.1.3" quote = "1" rand = "0.8.5" rand_chacha = "0.3" @@ -368,9 +370,6 @@ subtle = "2.5.0" # cosmwasm-related cosmwasm-schema = "=2.2.2" cosmwasm-std = "=2.2.2" -# use 1.0.1 as that's the version used by cosmwasm-std 2.2.1 -# (and ideally we don't want to pull the same dependency twice) -serde-json-wasm = "=1.0.1" # same version as used by cosmwasm cw-utils = "=2.0.0" cw-storage-plus = "=2.0.0" @@ -378,6 +377,7 @@ cw2 = { version = "=2.0.0" } cw3 = { version = "=2.0.0" } cw4 = { version = "=2.0.0" } cw-controllers = { version = "=2.0.0" } +cw-multi-test = "=2.3.2" # cosmrs-related bip32 = { version = "0.5.3", default-features = false } diff --git a/Makefile b/Makefile index 709126a6bb..0bd2234edb 100644 --- a/Makefile +++ b/Makefile @@ -133,7 +133,7 @@ clippy: sdk-wasm-lint # Build contracts ready for deploy # ----------------------------------------------------------------------------- -CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract +CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract nym_performance_contract CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS)) CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 914c5feea0..b41eca332a 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -416,6 +416,9 @@ pub struct Traffic { /// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops + /// requires use of the updated SURB packet format. pub disable_mix_hops: bool, } diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/error.rs b/common/client-core/gateways-storage/src/backend/fs_backend/error.rs index bdcaa0fdb5..59b7bef89d 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/error.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/error.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::BadGateway; -use std::io; -use std::path::PathBuf; +use std::{io, path::PathBuf}; use thiserror::Error; #[derive(Debug, Error)] @@ -19,7 +18,6 @@ pub enum StorageError { #[error("failed to perform sqlx migration: {source}")] MigrationError { - #[source] #[from] source: sqlx::migrate::MigrateError, }, @@ -32,7 +30,6 @@ pub enum StorageError { #[error("failed to run the SQL query: {source}")] QueryError { - #[source] #[from] source: sqlx::error::Error, }, diff --git a/common/client-core/src/client/base_client/non_wasm_helpers.rs b/common/client-core/src/client/base_client/non_wasm_helpers.rs index 15ad0792c8..87e7fb62a2 100644 --- a/common/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/common/client-core/src/client/base_client/non_wasm_helpers.rs @@ -1,19 +1,17 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::{ - fs_backend, CombinedReplyStorage, ReplyStorageBackend, +use crate::{ + client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend}, + config, + config::Config, + error::ClientCoreError, }; -use crate::config; -use crate::config::Config; -use crate::error::ClientCoreError; use nym_bandwidth_controller::BandwidthController; use nym_client_core_gateways_storage::OnDiskGatewaysDetails; use nym_credential_storage::storage::Storage as CredentialStorage; -use nym_validator_client::nyxd; -use nym_validator_client::QueryHttpRpcNyxdClient; -use std::path::Path; -use std::{fs, io}; +use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient}; +use std::{io, path::Path}; use time::OffsetDateTime; use tracing::{error, info, trace}; use url::Url; @@ -22,11 +20,11 @@ async fn setup_fresh_backend>( db_path: P, surb_config: &config::ReplySurbs, ) -> Result { - info!("creating fresh surb database"); + info!("Creating fresh surb database"); let mut storage_backend = match fs_backend::Backend::init(db_path).await { Ok(backend) => backend, Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}"); + error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}"); return Err(ClientCoreError::SurbStorageError { source: Box::new(err), }); @@ -40,14 +38,15 @@ async fn setup_fresh_backend>( surb_config.minimum_reply_surb_storage_threshold, surb_config.maximum_reply_surb_storage_threshold, ); - storage_backend - .init_fresh(&mem_store) - .await - .map_err(|err| ClientCoreError::SurbStorageError { - source: Box::new(err), - })?; - - Ok(storage_backend) + match storage_backend.init_fresh(&mem_store).await { + Ok(()) => Ok(storage_backend), + Err(err) => { + storage_backend.shutdown().await; + Err(ClientCoreError::SurbStorageError { + source: Box::new(err), + }) + } + } } // fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend { @@ -58,12 +57,11 @@ async fn setup_fresh_backend>( // ) // } -fn archive_corrupted_database>(db_path: P) -> io::Result<()> { +async fn archive_corrupted_database>(db_path: P) -> io::Result<()> { let db_path = db_path.as_ref(); debug_assert!(db_path.exists()); let now = OffsetDateTime::now_utc().unix_timestamp(); - let suffix = format!("_{now}.corrupted"); let new_extension = @@ -72,11 +70,15 @@ fn archive_corrupted_database>(db_path: P) -> io::Result<()> { } else { suffix }; + let renamed = db_path.with_extension(new_extension); - let mut renamed = db_path.to_owned(); - renamed.set_extension(new_extension); - - fs::rename(db_path, renamed) + tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| { + error!( + "Failed to rename corrupt database file: {} to {}", + db_path.display(), + renamed.display() + ); + }) } pub async fn setup_fs_reply_surb_backend>( @@ -87,13 +89,12 @@ pub async fn setup_fs_reply_surb_backend>( // the existing one let db_path = db_path.as_ref(); if db_path.exists() { - info!("loading existing surb database"); + info!("Loading existing surb database"); match fs_backend::Backend::try_load(db_path).await { Ok(backend) => Ok(backend), Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); - - archive_corrupted_database(db_path)?; + error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); + archive_corrupted_database(db_path).await?; setup_fresh_backend(db_path, surb_config).await } } diff --git a/common/client-core/src/client/mix_traffic/mod.rs b/common/client-core/src/client/mix_traffic/mod.rs index 6f58c477df..5b7e5ab7bd 100644 --- a/common/client-core/src/client/mix_traffic/mod.rs +++ b/common/client-core/src/client/mix_traffic/mod.rs @@ -146,7 +146,7 @@ impl MixTrafficController { Some(client_request) => { match self.gateway_transceiver.send_client_request(client_request).await { Ok(_) => (), - Err(e) => error!("Failed to send client request: {}", e), + Err(e) => error!("Failed to send client request: {e}"), }; }, None => { diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index b183fe4c05..7c7000c3b3 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -65,7 +65,7 @@ impl AcknowledgementListener { return; } - trace!("Received {} from the mix network", frag_id); + trace!("Received {frag_id} from the mix network"); self.stats_tx .report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into()); if let Err(err) = self diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index d335196983..ad2fa1a3c2 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -126,7 +126,7 @@ impl ActionController { fn handle_insert(&mut self, pending_acks: Vec) { for pending_ack in pending_acks { let frag_id = pending_ack.message_chunk.fragment_identifier(); - trace!("{} is inserted", frag_id); + trace!("{frag_id} is inserted"); if self .pending_acks_data @@ -161,22 +161,16 @@ impl ActionController { let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout); *queue_key = Some(new_queue_key) } else { - debug!( - "Tried to START TIMER on pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to START TIMER on pending ack that is already gone! - {frag_id}"); } } fn handle_remove(&mut self, frag_id: FragmentIdentifier) { - trace!("{} is getting removed", frag_id); + trace!("{frag_id} is getting removed"); match self.pending_acks_data.remove(&frag_id) { None => { - debug!( - "Tried to REMOVE pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to REMOVE pending ack that is already gone! - {frag_id}"); } Some((_, queue_key)) => { if let Some(queue_key) = queue_key { @@ -188,10 +182,7 @@ impl ActionController { } else { // I'm not 100% sure if having a `None` key is even possible here // (REMOVE would have to be called before START TIMER), - debug!( - "Tried to REMOVE pending ack without TIMER active - {}", - frag_id - ); + debug!("Tried to REMOVE pending ack without TIMER active - {frag_id}"); } } } @@ -200,7 +191,7 @@ impl ActionController { // initiated basically as a first step of retransmission. At first data has its delay updated // (as new sphinx packet was created with new expected delivery time) fn handle_update_pending_ack(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) { - trace!("{} is updating its delay", frag_id); + trace!("{frag_id} is updating its delay"); // TODO: is it possible to solve this without either locking or temporarily removing the value? if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) { // this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets) @@ -213,10 +204,7 @@ impl ActionController { self.pending_acks_data .insert(frag_id, (Arc::new(inner_data), queue_key)); } else { - debug!( - "Tried to UPDATE TIMER on pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to UPDATE TIMER on pending ack that is already gone! - {frag_id}"); } } diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index 0f4d902f54..694f56d762 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -103,6 +103,9 @@ pub(crate) struct Config { /// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops + /// requires use of the updated SURB packet format. disable_mix_hops: bool, /// Average delay a data packet is going to get delay at a single mixnode. @@ -157,8 +160,12 @@ impl Config { } /// Configure whether messages senders using this config should use mix hops or not when sending messages. + /// + /// This overrides the `use_legacy_sphinx_format` setting as disabled mix hops + /// requires use of the updated SURB packet format. pub fn disable_mix_hops(mut self, disable_mix_hops: bool) -> Self { self.disable_mix_hops = disable_mix_hops; + self.use_legacy_sphinx_format = false; self } } diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index ebc6b40380..8c77c1152c 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -202,7 +202,7 @@ where // well technically the message was not sent just yet, but now it's up to internal // queues and client load rather than the required delay. So realistically we can treat // whatever is about to happen as negligible additional delay. - trace!("{} is about to get sent to the mixnet", frag_id); + trace!("{frag_id} is about to get sent to the mixnet"); if let Err(err) = self.sent_notifier.unbounded_send(frag_id) { error!("Failed to notify about sent message: {err}"); } diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs index faf740d4b0..7c147bed8d 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs @@ -164,11 +164,11 @@ impl SendingDelayController { self.current_multiplier() ); if self.current_multiplier() > 0 { - tracing::debug!("{}", status_str); + tracing::debug!("{status_str}"); } else if self.current_multiplier() > 1 { - tracing::info!("{}", status_str); + tracing::info!("{status_str}"); } else if self.current_multiplier() > 2 { - tracing::warn!("{}", status_str); + tracing::warn!("{status_str}"); } self.time_when_logged_about_elevated_multiplier = now; } diff --git a/common/client-core/src/client/received_buffer.rs b/common/client-core/src/client/received_buffer.rs index 044b5ee355..743fc79653 100644 --- a/common/client-core/src/client/received_buffer.rs +++ b/common/client-core/src/client/received_buffer.rs @@ -221,10 +221,7 @@ impl ReceivedMessagesBuffer { let stored_messages = std::mem::take(&mut guard.messages); if !stored_messages.is_empty() { if let Err(err) = sender.unbounded_send(stored_messages) { - error!( - "The sender channel we just received is already invalidated - {:?}", - err - ); + error!("The sender channel we just received is already invalidated - {err:?}"); // put the values back to the buffer // the returned error has two fields: err: SendError and val: T, // where val is the value that was failed to get sent; diff --git a/common/client-core/src/client/statistics_control.rs b/common/client-core/src/client/statistics_control.rs index 1bf28c9f5b..98897ae6b9 100644 --- a/common/client-core/src/client/statistics_control.rs +++ b/common/client-core/src/client/statistics_control.rs @@ -93,7 +93,7 @@ impl StatisticsControl { None, ); if let Err(err) = self.report_tx.send(report_message).await { - tracing::error!("Failed to report client stats: {:?}", err); + tracing::error!("Failed to report client stats: {err:?}"); } else { self.stats.reset(); } diff --git a/common/client-core/src/client/transmission_buffer.rs b/common/client-core/src/client/transmission_buffer.rs index b0f8f8092e..ee15210d14 100644 --- a/common/client-core/src/client/transmission_buffer.rs +++ b/common/client-core/src/client/transmission_buffer.rs @@ -218,7 +218,7 @@ impl TransmissionBuffer { }; let msg = self.pop_front_from_lane(&lane)?; - tracing::trace!("picking to send from lane: {:?}", lane); + tracing::trace!("picking to send from lane: {lane:?}"); Some((lane, msg)) } diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 0d4a9fa742..d099431ded 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -113,7 +113,7 @@ pub async fn gateways_for_init( .nodes; info!("nym api reports {} gateways", gateways.len()); - tracing::trace!("Gateways: {:#?}", gateways); + tracing::trace!("Gateways: {gateways:#?}"); // filter out gateways below minimum performance and ones that could operate as a mixnode // (we don't want instability) @@ -124,7 +124,7 @@ pub async fn gateways_for_init( .filter_map(|gateway| gateway.try_into().ok()) .collect::>(); tracing::debug!("After checking validity: {}", valid_gateways.len()); - tracing::trace!("Valid gateways: {:#?}", valid_gateways); + tracing::trace!("Valid gateways: {valid_gateways:#?}"); tracing::info!( "and {} after validity and performance filtering", @@ -289,7 +289,7 @@ pub(super) fn get_specified_gateway( gateways: &[RoutingNode], must_use_tls: bool, ) -> Result { - tracing::debug!("Requesting specified gateway: {}", gateway_identity); + tracing::debug!("Requesting specified gateway: {gateway_identity}"); let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity) .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; diff --git a/common/client-core/surb-storage/Cargo.toml b/common/client-core/surb-storage/Cargo.toml index 45f0532a85..551bfb74ec 100644 --- a/common/client-core/surb-storage/Cargo.toml +++ b/common/client-core/surb-storage/Cargo.toml @@ -17,15 +17,26 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false nym-sphinx = { path = "../../nymsphinx" } nym-task = { path = "../../task" } +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +workspace = true +features = ["fs"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] optional = true +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard] +path = "../../../sqlx-pool-guard" + [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } [features] fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"] diff --git a/common/client-core/surb-storage/src/backend/fs_backend/error.rs b/common/client-core/surb-storage/src/backend/fs_backend/error.rs index 02742758ae..e1eb9036da 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/error.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/error.rs @@ -1,8 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::io; -use std::path::PathBuf; +use std::{io, path::PathBuf}; use thiserror::Error; #[derive(Debug, Error)] @@ -30,7 +29,6 @@ pub enum StorageError { #[error("failed to perform sqlx migration: {source}")] MigrationError { - #[source] #[from] source: sqlx::migrate::MigrateError, }, @@ -43,7 +41,6 @@ pub enum StorageError { #[error("failed to run the SQL query: {source}")] QueryError { - #[source] #[from] source: sqlx::error::Error, }, diff --git a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs index 87ff19b78d..4a39c37b87 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs @@ -13,9 +13,11 @@ use std::path::Path; use time::OffsetDateTime; use tracing::{error, info}; +use sqlx_pool_guard::SqlitePoolGuard; + #[derive(Debug, Clone)] pub struct StorageManager { - pub connection_pool: sqlx::SqlitePool, + connection_pool: SqlitePoolGuard, } // all SQL goes here @@ -35,7 +37,7 @@ impl StorageManager { .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) .auto_vacuum(SqliteAutoVacuum::Incremental) - .filename(database_path) + .filename(&database_path) .create_if_missing(fresh) .disable_statement_logging(); @@ -47,11 +49,15 @@ impl StorageManager { } }; + let connection_pool = + SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool); + if let Err(err) = sqlx::migrate!("./fs_surbs_migrations") - .run(&connection_pool) + .run(&*connection_pool) .await { error!("Failed to initialize SQLx database: {err}"); + connection_pool.close().await; return Err(err.into()); } @@ -59,10 +65,15 @@ impl StorageManager { Ok(StorageManager { connection_pool }) } + /// Close connection pool waiting for all connections to be closed. + pub async fn close_pool(&self) { + self.connection_pool.close().await; + } + #[allow(dead_code)] pub async fn status_table_exists(&self) -> Result { sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'") - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await .map(|r| r.is_some()) } @@ -71,28 +82,28 @@ impl StorageManager { sqlx::query!( "INSERT INTO status(flush_in_progress, previous_flush, client_in_use) VALUES (0, 0, 1)" ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_flush_status(&self) -> Result { sqlx::query!("SELECT flush_in_progress FROM status;") - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await .map(|r| r.flush_in_progress > 0) } pub async fn set_previous_flush(&self, timestamp: OffsetDateTime) -> Result<(), sqlx::Error> { sqlx::query!("UPDATE status SET previous_flush = ?", timestamp) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_previous_flush_time(&self) -> Result { sqlx::query!(r#"SELECT previous_flush AS "previous_flush: OffsetDateTime" FROM status"#) - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await .map(|r| r.previous_flush) } @@ -100,14 +111,14 @@ impl StorageManager { pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> { let in_progress_int = i64::from(in_progress); sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_client_in_use_status(&self) -> Result { sqlx::query!("SELECT client_in_use FROM status;") - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await .map(|r| r.client_in_use > 0) } @@ -115,21 +126,21 @@ impl StorageManager { pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> { let in_use_int = i64::from(in_use); sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM reply_key;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_reply_keys(&self) -> Result, sqlx::Error> { sqlx::query_as("SELECT * FROM reply_key;") - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -145,14 +156,14 @@ impl StorageManager { stored_reply_key.reply_key, stored_reply_key.sent_at ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_surb_senders(&self) -> Result, sqlx::Error> { sqlx::query_as("SELECT * FROM reply_surb_sender;") - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -167,7 +178,7 @@ impl StorageManager { stored_surb_sender.tag, stored_surb_sender.last_sent ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await? .last_insert_rowid(); Ok(id) @@ -185,17 +196,17 @@ impl StorageManager { "#, sender_id ) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM reply_surb;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; sqlx::query!("DELETE FROM reply_surb_sender;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) @@ -213,7 +224,7 @@ impl StorageManager { stored_reply_surb.reply_surb, stored_reply_surb.encoded_key_rotation ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -227,7 +238,7 @@ impl StorageManager { SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata; "#, ) - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await } @@ -241,7 +252,7 @@ impl StorageManager { "#, metadata.min_reply_surb_threshold, metadata.max_reply_surb_threshold, - ).execute(&self.connection_pool).await?; + ).execute(&*self.connection_pool).await?; Ok(()) } } diff --git a/common/client-core/surb-storage/src/backend/fs_backend/mod.rs b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs index 9a2621f728..625913e174 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/mod.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs @@ -1,15 +1,16 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::backend::fs_backend::manager::StorageManager; -use crate::backend::fs_backend::models::{ - ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender, +use crate::{ + backend::fs_backend::{ + manager::StorageManager, + models::{ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender}, + }, + surb_storage::ReceivedReplySurbs, + CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, }; -use crate::surb_storage::ReceivedReplySurbs; -use crate::{CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys}; use async_trait::async_trait; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; -use std::fs; use std::path::{Path, PathBuf}; use time::OffsetDateTime; use tracing::{error, info, warn}; @@ -39,15 +40,17 @@ impl Backend { } let manager = StorageManager::init(database_path, true).await?; - manager.create_status_table().await?; - - let backend = Backend { - temporary_old_path: None, - database_path: owned_path, - manager, - }; - - Ok(backend) + match manager.create_status_table().await { + Ok(()) => Ok(Backend { + temporary_old_path: None, + database_path: owned_path, + manager, + }), + Err(err) => { + manager.close_pool().await; + Err(err.into()) + } + } } pub async fn try_load>(database_path: P) -> Result { @@ -59,7 +62,25 @@ impl Backend { } let manager = StorageManager::init(database_path, false).await?; + match Self::try_load_inner(&manager).await { + Ok(()) => Ok(Backend { + temporary_old_path: None, + database_path: owned_path, + manager, + }), + Err(e) => { + manager.close_pool().await; + Err(e) + } + } + } + /// Gracefully close sqlite connection pool and drop backend. + pub async fn shutdown(self) { + self.manager.close_pool().await + } + + async fn try_load_inner(manager: &StorageManager) -> Result<(), StorageError> { // the database flush wasn't fully finished and thus the data is in inconsistent state // (we don't really know what's properly saved or what's not) if manager.get_flush_status().await? { @@ -104,20 +125,11 @@ impl Backend { manager.delete_all_reply_keys().await?; } - Ok(Backend { - temporary_old_path: None, - database_path: owned_path, - // manager: StorageManagerState::Storage(manager), - manager, - }) - } - - async fn close_pool(&mut self) { - self.manager.connection_pool.close().await; + Ok(()) } async fn rotate(&mut self) -> Result<(), StorageError> { - self.close_pool().await; + self.manager.close_pool().await; let new_extension = if let Some(existing_extension) = self.database_path.extension().and_then(|ext| ext.to_str()) @@ -130,7 +142,8 @@ impl Backend { let mut temp_old = self.database_path.clone(); temp_old.set_extension(new_extension); - fs::rename(&self.database_path, &temp_old) + tokio::fs::rename(&self.database_path, &temp_old) + .await .map_err(|err| StorageError::DatabaseRenameError { source: err })?; self.manager = StorageManager::init(&self.database_path, true).await?; self.manager.create_status_table().await?; @@ -139,9 +152,10 @@ impl Backend { Ok(()) } - fn remove_old(&mut self) -> Result<(), StorageError> { + async fn remove_old(&mut self) -> Result<(), StorageError> { if let Some(old_path) = self.temporary_old_path.take() { - fs::remove_file(old_path) + tokio::fs::remove_file(old_path) + .await .map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err }) } else { warn!("the old database file doesn't seem to exist!"); @@ -302,7 +316,7 @@ impl ReplyStorageBackend for Backend { self.dump_reply_surb_storage_metadata(surbs_ref).await?; self.dump_reply_surbs(surbs_ref).await?; - self.remove_old()?; + self.remove_old().await?; self.end_storage_flush().await } diff --git a/common/client-core/surb-storage/src/lib.rs b/common/client-core/surb-storage/src/lib.rs index 3f49f9da9d..b7ac924706 100644 --- a/common/client-core/surb-storage/src/lib.rs +++ b/common/client-core/surb-storage/src/lib.rs @@ -37,7 +37,6 @@ where self.backend.load_surb_storage(surb_freshness_cutoff).await } - // this will have to get enabled after merging develop pub async fn flush_on_shutdown( mut self, mem_state: CombinedReplyStorage, @@ -54,7 +53,6 @@ where shutdown.recv().await; info!("PersistentReplyStorage is flushing all reply-related data to underlying storage"); - info!("you MUST NOT forcefully shutdown now or you risk data corruption!"); if let Err(err) = self.backend.flush_surb_storage(&mem_state).await { error!("failed to flush our reply-related data to the persistent storage: {err}") } else { diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index eeadec5ea1..612349315f 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -272,7 +272,7 @@ impl GatewayClient { ) -> Result<(), GatewayClientError> { if let Some(shared_key) = self.shared_key() { let encrypted = message.encrypt(&*shared_key)?; - Box::pin(self.send_websocket_message(encrypted)).await?; + Box::pin(self.send_websocket_message_without_response(encrypted)).await?; Ok(()) } else { Err(GatewayClientError::ConnectionInInvalidState) @@ -330,9 +330,80 @@ impl GatewayClient { } } + /// Attempt to send a websocket message to the gateway without waiting for any response + async fn send_websocket_message_without_response( + &mut self, + msg: impl Into, + ) -> Result<(), GatewayClientError> { + match self.connection { + SocketState::Available(ref mut conn) => Ok(conn.send(msg.into()).await?), + SocketState::PartiallyDelegated(ref mut partially_delegated) => { + if let Err(err) = partially_delegated.send_without_response(msg.into()).await { + error!("failed to send message without response - {err}..."); + // we must ensure we do not leave the task still active + if let Err(err) = self.recover_socket_connection().await { + error!("... and the delegated stream has also errored out - {err}") + } + Err(err) + } else { + Ok(()) + } + } + SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished), + _ => Err(GatewayClientError::ConnectionInInvalidState), + } + } + + // A very nasty hack due to lack of id tags on messages - send a non-sphinx packet websocket + // message and wait until first non 'Send' response within timeout + pub async fn send_websocket_message_with_non_send_response( + &mut self, + msg: impl Into, + ) -> Result { + let should_restart_mixnet_listener = if self.connection.is_partially_delegated() { + self.recover_socket_connection().await?; + true + } else { + false + }; + + let conn = match self.connection { + SocketState::Available(ref mut conn) => conn, + SocketState::NotConnected => return Err(GatewayClientError::ConnectionNotEstablished), + _ => return Err(GatewayClientError::ConnectionInInvalidState), + }; + conn.send(msg.into()).await?; + + let timeout = sleep(self.cfg.connection.response_timeout_duration); + tokio::pin!(timeout); + + let response = loop { + tokio::select! { + _ = &mut timeout => { + break Err(GatewayClientError::Timeout); + } + // note: the below will also listen for shutdown signals + msg = self.read_control_response() => { + match msg { + Ok(res) => if !res.is_send() { + break Ok(res); + }, + Err(err) => break Err(err), + } + } + } + }; + + if should_restart_mixnet_listener { + self.start_listening_for_mixnet_messages()?; + } + response + } + + /// Attempt to send a websocket message to the gateway and wait until we receive a response. // If we want to send a message (with response), we need to have a full control over the socket, // as we need to be able to write the request and read the subsequent response - pub async fn send_websocket_message( + pub async fn send_websocket_message_with_response( &mut self, msg: impl Into, ) -> Result { @@ -387,29 +458,6 @@ impl GatewayClient { } } - async fn send_websocket_message_without_response( - &mut self, - msg: Message, - ) -> Result<(), GatewayClientError> { - match self.connection { - SocketState::Available(ref mut conn) => Ok(conn.send(msg).await?), - SocketState::PartiallyDelegated(ref mut partially_delegated) => { - if let Err(err) = partially_delegated.send_without_response(msg).await { - error!("failed to send message without response - {err}..."); - // we must ensure we do not leave the task still active - if let Err(err) = self.recover_socket_connection().await { - error!("... and the delegated stream has also errored out - {err}") - } - Err(err) - } else { - Ok(()) - } - } - SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished), - _ => Err(GatewayClientError::ConnectionInInvalidState), - } - } - fn check_gateway_protocol( &self, gateway_protocol: Option, @@ -535,7 +583,10 @@ impl GatewayClient { .encrypt(legacy_key)?; info!("sending upgrade request and awaiting the acknowledgement back"); - let (ciphertext, nonce) = match self.send_websocket_message(upgrade_request).await? { + let (ciphertext, nonce) = match self + .send_websocket_message_with_response(upgrade_request) + .await? + { ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce), ServerResponse::Error { message } => { return Err(GatewayClientError::GatewayError(message)) @@ -567,7 +618,7 @@ impl GatewayClient { &mut self, msg: ClientControlRequest, ) -> Result<(), GatewayClientError> { - match self.send_websocket_message(msg).await? { + match self.send_websocket_message_with_response(msg).await? { ServerResponse::Authenticate { protocol_version, status, @@ -717,13 +768,16 @@ impl GatewayClient { } } + /// Attempt to retrieve the currently supported gateway protocol version of the remote. pub async fn get_gateway_protocol(&mut self) -> Result { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } match self - .send_websocket_message(ClientControlRequest::SupportedProtocol {}) + .send_websocket_message_with_non_send_response( + ClientControlRequest::SupportedProtocol {}, + ) .await? { ServerResponse::SupportedProtocol { version } => Ok(version), @@ -740,7 +794,10 @@ impl GatewayClient { credential, self.shared_key.as_ref().unwrap(), )?; - let bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self + .send_websocket_message_with_non_send_response(msg) + .await? + { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), ServerResponse::TypedError { error } => { @@ -758,7 +815,10 @@ impl GatewayClient { async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth; - let bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self + .send_websocket_message_with_non_send_response(msg) + .await? + { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 7ff310b039..ea4e8beeb4 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -19,6 +19,7 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } +nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" } nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs index 8210c5b476..ad7db0991b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs @@ -13,6 +13,7 @@ pub mod ecash_query_client; pub mod group_query_client; pub mod mixnet_query_client; pub mod multisig_query_client; +pub mod performance_query_client; pub mod vesting_query_client; // signing clients @@ -21,6 +22,7 @@ pub mod ecash_signing_client; pub mod group_signing_client; pub mod mixnet_signing_client; pub mod multisig_signing_client; +pub mod performance_signing_client; pub mod vesting_signing_client; // re-export query traits @@ -29,6 +31,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient}; pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient}; pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient}; pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient}; +pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient}; pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient}; // re-export signing traits @@ -37,6 +40,7 @@ pub use ecash_signing_client::EcashSigningClient; pub use group_signing_client::GroupSigningClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_signing_client::MultisigSigningClient; +pub use performance_signing_client::PerformanceSigningClient; pub use vesting_signing_client::VestingSigningClient; // helper for providing blanket implementation for query clients @@ -44,6 +48,7 @@ pub trait NymContractsProvider { // main fn mixnet_contract_address(&self) -> Option<&AccountId>; fn vesting_contract_address(&self) -> Option<&AccountId>; + fn performance_contract_address(&self) -> Option<&AccountId>; // coconut-related fn ecash_contract_address(&self) -> Option<&AccountId>; @@ -56,6 +61,7 @@ pub trait NymContractsProvider { pub struct TypedNymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, + pub performance_contract_address: Option, pub ecash_contract_address: Option, pub group_contract_address: Option, @@ -76,6 +82,10 @@ impl TryFrom for TypedNymContracts { .vesting_contract_address .map(|addr| addr.parse()) .transpose()?, + performance_contract_address: value + .performance_contract_address + .map(|addr| addr.parse()) + .transpose()?, ecash_contract_address: value .ecash_contract_address .map(|addr| addr.parse()) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs new file mode 100644 index 0000000000..470a412e8d --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs @@ -0,0 +1,271 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::collect_paged; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::error::NyxdError; +use crate::nyxd::CosmWasmClient; +use async_trait::async_trait; +use cosmrs::AccountId; +use serde::Deserialize; + +pub use nym_performance_contract_common::{ + msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse, EpochId, + EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, + NetworkMonitorInformation, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, + NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitor, RetiredNetworkMonitorsPagedResponse, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PerformanceQueryClient { + async fn query_performance_contract( + &self, + query: PerformanceQueryMsg, + ) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn admin(&self) -> Result { + self.query_performance_contract(PerformanceQueryMsg::Admin {}) + .await + } + + async fn get_node_performance( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NodePerformance { epoch_id, node_id }) + .await + } + + async fn get_node_performance_paged( + &self, + node_id: NodeId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + }) + .await + } + + async fn get_node_measurements( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id }) + .await + } + + async fn get_epoch_measurements_paged( + &self, + epoch_id: EpochId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + }) + .await + } + + async fn get_epoch_performance_paged( + &self, + epoch_id: EpochId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + }) + .await + } + + async fn get_full_historical_performance_paged( + &self, + start_after: Option<(EpochId, NodeId)>, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::FullHistoricalPerformancePaged { + start_after, + limit, + }) + .await + } + + async fn get_network_monitor( + &self, + address: &AccountId, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NetworkMonitor { + address: address.to_string(), + }) + .await + } + + async fn get_network_monitors_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NetworkMonitorsPaged { + start_after, + limit, + }) + .await + } + + async fn get_retired_network_monitors_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::RetiredNetworkMonitorsPaged { + start_after, + limit, + }) + .await + } + + async fn get_last_submission(&self) -> Result { + self.query_performance_contract(PerformanceQueryMsg::LastSubmittedMeasurement {}) + .await + } +} + +// extension trait to the query client to deal with the paged queries +// (it didn't feel appropriate to combine it with the existing trait +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PagedPerformanceQueryClient: PerformanceQueryClient { + async fn get_all_node_performance( + &self, + node_id: NodeId, + ) -> Result, NyxdError> { + collect_paged!(self, get_node_performance_paged, performance, node_id) + } + + async fn get_all_epoch_measurements( + &self, + node_id: NodeId, + ) -> Result, NyxdError> { + collect_paged!(self, get_epoch_measurements_paged, measurements, node_id) + } + + async fn get_all_epoch_performance( + &self, + epoch_id: EpochId, + ) -> Result, NyxdError> { + collect_paged!(self, get_epoch_performance_paged, performance, epoch_id) + } + + async fn get_all_full_historical_performance( + &self, + ) -> Result, NyxdError> { + collect_paged!(self, get_full_historical_performance_paged, performance) + } + + async fn get_all_network_monitors(&self) -> Result, NyxdError> { + collect_paged!(self, get_network_monitors_paged, info) + } + + async fn get_all_retired_network_monitors( + &self, + ) -> Result, NyxdError> { + collect_paged!(self, get_retired_network_monitors_paged, info) + } +} + +#[async_trait] +impl PagedPerformanceQueryClient for T where T: PerformanceQueryClient {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl PerformanceQueryClient for C +where + C: CosmWasmClient + NymContractsProvider + Send + Sync, +{ + async fn query_performance_contract( + &self, + query: PerformanceQueryMsg, + ) -> Result + where + for<'a> T: Deserialize<'a>, + { + let performance_contract_address = &self + .performance_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?; + self.query_contract_smart(performance_contract_address, &query) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_performance_contract_common::QueryMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_query_variants_are_covered( + client: C, + msg: PerformanceQueryMsg, + ) { + match msg { + PerformanceQueryMsg::Admin {} => client.admin().ignore(), + PerformanceQueryMsg::NodePerformance { epoch_id, node_id } => { + client.get_node_performance(epoch_id, node_id).ignore() + } + PerformanceQueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + } => client + .get_node_performance_paged(node_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id } => { + client.get_node_measurements(epoch_id, node_id).ignore() + } + PerformanceQueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + } => client + .get_epoch_measurements_paged(epoch_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + } => client + .get_epoch_performance_paged(epoch_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::FullHistoricalPerformancePaged { start_after, limit } => client + .get_full_historical_performance_paged(start_after, limit) + .ignore(), + PerformanceQueryMsg::NetworkMonitor { address } => client + .get_network_monitor(&address.parse().unwrap()) + .ignore(), + PerformanceQueryMsg::NetworkMonitorsPaged { start_after, limit } => client + .get_network_monitors_paged(start_after, limit) + .ignore(), + PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client + .get_retired_network_monitors_paged(start_after, limit) + .ignore(), + QueryMsg::LastSubmittedMeasurement {} => client.get_last_submission().ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs new file mode 100644 index 0000000000..78b960265a --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs @@ -0,0 +1,217 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nyxd::coin::Coin; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +use crate::nyxd::cosmwasm_client::ContractResponseData; +use crate::nyxd::error::NyxdError; +use crate::nyxd::{Fee, SigningCosmWasmClient}; +use crate::signing::signer::OfflineSigner; +use async_trait::async_trait; +use nym_performance_contract_common::{ + EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformance, + RemoveEpochMeasurementsResponse, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PerformanceSigningClient { + async fn execute_performance_contract( + &self, + fee: Option, + msg: PerformanceExecuteMsg, + memo: String, + funds: Vec, + ) -> Result; + + async fn update_admin( + &self, + admin: String, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::UpdateAdmin { admin }, + "PerformanceContract::UpdateAdmin".to_string(), + vec![], + ) + .await + } + + async fn submit_performance( + &self, + epoch: EpochId, + data: NodePerformance, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::Submit { epoch, data }, + "PerformanceContract::Submit".to_string(), + vec![], + ) + .await + } + + async fn batch_submit_performance( + &self, + epoch: EpochId, + data: Vec, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::BatchSubmit { epoch, data }, + "PerformanceContract::BatchSubmit".to_string(), + vec![], + ) + .await + } + + async fn authorise_network_monitor( + &self, + address: String, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::AuthoriseNetworkMonitor { address }, + "PerformanceContract::AuthoriseNetworkMonitor".to_string(), + vec![], + ) + .await + } + + async fn retire_network_monitor( + &self, + address: String, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RetireNetworkMonitor { address }, + "PerformanceContract::RetireNetworkMonitor".to_string(), + vec![], + ) + .await + } + + async fn remove_node_measurements( + &self, + epoch_id: EpochId, + node_id: NodeId, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id }, + "PerformanceContract::RemoveNodeMeasurements".to_string(), + vec![], + ) + .await + } + + async fn partial_remove_epoch_measurements( + &self, + epoch_id: EpochId, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RemoveEpochMeasurements { epoch_id }, + "PerformanceContract::RemoveEpochMeasurements".to_string(), + vec![], + ) + .await + } + + async fn remove_epoch_measurements( + &self, + epoch_id: EpochId, + fee: Option, + ) -> Result<(), NyxdError> { + loop { + let execute_res = self + .partial_remove_epoch_measurements(epoch_id, fee.clone()) + .await?; + let response = execute_res + .parse_singleton_json_contract_response::()?; + if !response.additional_entries_to_remove_remaining { + break; + } + } + Ok(()) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl PerformanceSigningClient for C +where + C: SigningCosmWasmClient + NymContractsProvider + Sync, + NyxdError: From<::Error>, +{ + async fn execute_performance_contract( + &self, + fee: Option, + msg: PerformanceExecuteMsg, + memo: String, + funds: Vec, + ) -> Result { + let performance_contract_address = &self + .performance_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?; + + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); + + let signer_address = &self.signer_addresses()?[0]; + self.execute( + signer_address, + performance_contract_address, + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_performance_contract_common::ExecuteMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_execute_variants_are_covered( + client: C, + msg: PerformanceExecuteMsg, + ) { + match msg { + PerformanceExecuteMsg::UpdateAdmin { admin } => { + client.update_admin(admin, None).ignore() + } + PerformanceExecuteMsg::Submit { epoch, data } => { + client.submit_performance(epoch, data, None).ignore() + } + PerformanceExecuteMsg::BatchSubmit { epoch, data } => { + client.batch_submit_performance(epoch, data, None).ignore() + } + PerformanceExecuteMsg::AuthoriseNetworkMonitor { address } => { + client.authorise_network_monitor(address, None).ignore() + } + PerformanceExecuteMsg::RetireNetworkMonitor { address } => { + client.retire_network_monitor(address, None).ignore() + } + ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => client + .remove_node_measurements(epoch_id, node_id, None) + .ignore(), + ExecuteMsg::RemoveEpochMeasurements { epoch_id } => client + .partial_remove_epoch_measurements(epoch_id, None) + .ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index 0c4c6c8dcb..e948dc9812 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -12,6 +12,8 @@ use tendermint_rpc::endpoint::broadcast; use tracing::error; pub use cosmrs::abci::MsgResponse; +use cosmwasm_std::from_json; +use serde::de::DeserializeOwned; pub fn parse_singleton_u32_from_contract_response(b: Vec) -> Result { if b.len() != 4 { @@ -73,6 +75,11 @@ pub fn parse_msg_responses(data: Bytes) -> Vec { // requires there's a single response message pub trait ContractResponseData: Sized { + fn parse_singleton_json_contract_response(&self) -> Result { + let b = self.to_singleton_contract_data()?; + from_json(&b).map_err(|err| err.into()) + } + fn parse_singleton_u32_contract_data(&self) -> Result { let b = self.to_singleton_contract_data()?; parse_singleton_u32_from_contract_response(b) diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 5f7cbb8e7c..80dcf01fb0 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -276,6 +276,10 @@ impl NymContractsProvider for NyxdClient { self.config.contracts.vesting_contract_address.as_ref() } + fn performance_contract_address(&self) -> Option<&AccountId> { + self.config.contracts.performance_contract_address.as_ref() + } + fn ecash_contract_address(&self) -> Option<&AccountId> { self.config.contracts.ecash_contract_address.as_ref() } diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml new file mode 100644 index 0000000000..212aa2d373 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "nym-contracts-common-testing" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } + +serde = { workspace = true } +rand_chacha = { workspace = true } +rand = { workspace = true } +cw-multi-test = { workspace = true } + +[lints] +workspace = true diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs new file mode 100644 index 0000000000..fa49db306d --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs @@ -0,0 +1,127 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{ + coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps, + Response, StdResult, Storage, +}; +use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey}; +use rand::{RngCore, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub const TEST_DENOM: &str = "unym"; +pub const TEST_PREFIX: &str = "n"; + +pub fn mock_api() -> MockApi { + MockApi::default().with_prefix(TEST_PREFIX) +} + +pub fn mock_dependencies() -> OwnedDeps> { + OwnedDeps { + storage: MockStorage::default(), + api: mock_api(), + querier: MockQuerier::default(), + custom_query_type: Default::default(), + } +} + +pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + rand_chacha::ChaCha20Rng::from_seed(dummy_seed) +} + +pub fn deps_with_balance(env: &Env) -> OwnedDeps> { + let mut deps = mock_dependencies(); + deps.querier = MockQuerier::::new(&[( + env.contract.address.as_str(), + coins(100000000000, TEST_DENOM).as_slice(), + )]); + deps +} + +pub fn generate_sorted_addresses(n: usize) -> Vec { + let mut rng = test_rng(); + let mut addrs = Vec::with_capacity(n); + for i in 0..n { + addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64()))); + } + addrs.sort(); + addrs +} + +pub fn addr>(raw: S) -> Addr { + mock_api().addr_make(raw.as_ref()) +} + +pub fn sender>(raw: S) -> MessageInfo { + message_info(&addr(raw), &[]) +} + +pub trait ExtractBankMsg { + fn unwrap_bank_msg(self) -> Option; +} + +impl ExtractBankMsg for Response { + fn unwrap_bank_msg(self) -> Option { + for msg in self.messages { + match msg.msg { + CosmosMsg::Bank(bank_msg) => return Some(bank_msg), + _ => continue, + } + } + + None + } +} + +pub trait FullReader<'a> { + type Key; + type Value: Serialize + DeserializeOwned; + + fn all_values(&self, store: &dyn Storage) -> StdResult>; + + fn all_key_values(&self, store: &dyn Storage) -> StdResult>; +} + +impl<'a, K, T> FullReader<'a> for Map +where + T: Serialize + DeserializeOwned, + K: PrimaryKey<'a> + KeyDeserialize, + K::Output: 'static, +{ + type Key = K::Output; + type Value = T; + + fn all_values(&self, store: &dyn Storage) -> StdResult> { + self.range(store, None, None, Order::Ascending) + .map(|record| record.map(|r| r.1)) + .collect() + } + + fn all_key_values(&self, store: &dyn Storage) -> StdResult> { + self.range(store, None, None, Order::Ascending).collect() + } +} + +impl<'a, K, T, B> FullReader<'a> for Prefix +where + K: KeyDeserialize + 'static, + T: Serialize + DeserializeOwned, + B: PrimaryKey<'a>, +{ + type Key = K::Output; + type Value = T; + + fn all_values(&self, store: &dyn Storage) -> StdResult> { + self.range(store, None, None, Order::Ascending) + .map(|record| record.map(|r| r.1)) + .collect() + } + + fn all_key_values(&self, store: &dyn Storage) -> StdResult> { + self.range(store, None, None, Order::Ascending).collect() + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs new file mode 100644 index 0000000000..acb4292d86 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// those are all used exclusively for testing thus unwraps, et al. are allowed +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] +#![allow(clippy::panic)] + +pub mod helpers; +pub mod tester; + +pub use helpers::*; +pub use tester::*; diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs new file mode 100644 index 0000000000..79b7607e4e --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs @@ -0,0 +1,239 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ContractTester, TestableNymContract}; +use cosmwasm_std::testing::{message_info, mock_env}; +use cosmwasm_std::{ + from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + Storage, Timestamp, +}; +use cw_multi_test::{next_block, AppResponse, Executor}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::any::type_name; +use std::fmt::Debug; + +pub trait ContractOpts { + type ExecuteMsg; + type QueryMsg; + type ContractError; + + fn deps(&self) -> Deps<'_>; + + fn deps_mut(&mut self) -> DepsMut<'_>; + + fn env(&self) -> Env; + + fn addr_make(&self, input: &str) -> Addr; + + fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) { + let env = self.env().clone(); + (self.deps_mut(), env) + } + + fn storage(&self) -> &dyn Storage; + + fn storage_mut(&mut self) -> &mut dyn Storage; + + fn read_from_contract_storage(&self, key: impl AsRef<[u8]>) -> Option; + + fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>); + + fn unchecked_read_from_contract_storage( + &self, + key: impl AsRef<[u8]>, + ) -> T { + let typ = type_name::(); + self.read_from_contract_storage(key) + .unwrap_or_else(|| panic!("value of type {typ} not present in the storage")) + } + + fn execute_raw( + &mut self, + sender: Addr, + message: Self::ExecuteMsg, + ) -> Result { + self.execute_raw_with_balance(sender, &[], message) + } + + fn execute_raw_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: Self::ExecuteMsg, + ) -> Result; +} + +impl ContractOpts for ContractTester +where + C: TestableNymContract, +{ + type ExecuteMsg = C::ExecuteMsg; + type QueryMsg = C::QueryMsg; + type ContractError = C::ContractError; + + fn deps(&self) -> Deps<'_> { + Deps { + storage: &self.storage, + api: self.app.api(), + querier: self.app.wrap(), + } + } + + fn deps_mut(&mut self) -> DepsMut<'_> { + DepsMut { + storage: &mut self.storage, + api: self.app.api(), + querier: self.app.wrap(), + } + } + + fn env(&self) -> Env { + Env { + block: self.app.block_info(), + contract: ContractInfo { + address: self.contract_address.clone(), + }, + ..mock_env() + } + } + + fn addr_make(&self, input: &str) -> Addr { + self.app.api().addr_make(input) + } + + fn storage(&self) -> &dyn Storage { + &self.storage + } + + fn storage_mut(&mut self) -> &mut dyn Storage { + &mut self.storage + } + + fn read_from_contract_storage(&self, key: impl AsRef<[u8]>) -> Option { + let raw = self.deps().storage.get(key.as_ref())?; + from_json(&raw).ok() + } + + fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) { + self.deps_mut().storage.set(key.as_ref(), value.as_ref()); + } + + fn execute_raw_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: C::ExecuteMsg, + ) -> Result { + let env = self.env(); + let info = message_info(&sender, coins); + + C::execute()(self.deps_mut(), env, info, message) + } +} + +pub trait ChainOpts: ContractOpts { + fn set_contract_balance(&mut self, balance: Coin); + + fn next_block(&mut self); + + fn set_block_time(&mut self, time: Timestamp); + + fn execute_msg( + &mut self, + sender: Addr, + message: &Self::ExecuteMsg, + ) -> anyhow::Result { + self.execute_msg_with_balance(sender, &[], message) + } + + fn execute_msg_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: &Self::ExecuteMsg, + ) -> anyhow::Result; + + fn execute_arbitrary_contract( + &mut self, + contract: Addr, + sender: MessageInfo, + message: &T, + ) -> anyhow::Result; + + fn query_arbitrary_contract( + &self, + contract: Addr, + message: &Q, + ) -> StdResult; + + fn query(&self, message: &Self::QueryMsg) -> StdResult; +} + +impl ChainOpts for ContractTester +where + C: TestableNymContract, +{ + fn set_contract_balance(&mut self, balance: Coin) { + let contract_address = &self.contract_address; + self.app + .router() + .bank + .init_balance( + &mut self.storage.inner_storage(), + contract_address, + vec![balance], + ) + .unwrap(); + } + fn next_block(&mut self) { + self.app.update_block(next_block) + } + + fn set_block_time(&mut self, time: Timestamp) { + self.app.update_block(|b| b.time = time) + } + + fn execute_msg( + &mut self, + sender: Addr, + message: &C::ExecuteMsg, + ) -> anyhow::Result { + self.execute_msg_with_balance(sender, &[], message) + } + + fn execute_msg_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: &C::ExecuteMsg, + ) -> anyhow::Result { + self.app + .execute_contract(sender, self.contract_address.clone(), message, coins) + } + + fn execute_arbitrary_contract( + &mut self, + contract: Addr, + sender: MessageInfo, + message: &T, + ) -> anyhow::Result { + let coins = &sender.funds; + let sender = sender.sender; + self.app.execute_contract(sender, contract, message, coins) + } + + fn query_arbitrary_contract( + &self, + contract: Addr, + message: &Q, + ) -> StdResult { + self.app.wrap().query_wasm_smart(contract, message) + } + + fn query(&self, message: &C::QueryMsg) -> StdResult { + self.app + .wrap() + .query_wasm_smart(self.contract_address.as_str(), message) + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs new file mode 100644 index 0000000000..75b226bd69 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs @@ -0,0 +1,305 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + CommonStorageKeys, ContractOpts, ContractTester, StorageWrapper, TestableNymContract, + TEST_DENOM, +}; +use cosmwasm_std::testing::message_info; +use cosmwasm_std::{ + coin, coins, from_json, to_json_vec, Addr, Coin, MessageInfo, StdError, StdResult, Storage, +}; +use cw_multi_test::Executor; +use cw_storage_plus::{Key, Path, PrimaryKey}; +use rand::RngCore; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::any::type_name; +use std::ops::Deref; + +pub trait StorageReader { + fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>; + + fn read_common_value(&self, key: CommonStorageKeys) -> Option { + self.read_from_contract_storage(self.common_key(key)?) + } + + fn unchecked_read_common_value(&self, key: CommonStorageKeys) -> T { + self.unchecked_read_from_contract_storage( + self.common_key(key) + .unwrap_or_else(|| panic!("no key set for {key:?}")), + ) + } + + fn read_from_contract_storage(&self, key: impl AsRef<[u8]>) -> Option; + + fn unchecked_read_from_contract_storage( + &self, + key: impl AsRef<[u8]>, + ) -> T { + let typ = type_name::(); + self.read_from_contract_storage(key) + .unwrap_or_else(|| panic!("value of type {typ} not present in the storage")) + } +} + +// technically it shouldn't rely on `StorageReader` and `common_key` should be extracted +// but this makes it a tad easier and it's only testing code so it's fine +pub trait StorageWriter: StorageReader { + fn set_common_value( + &mut self, + key: CommonStorageKeys, + value: &T, + ) -> StdResult<()> { + let key = self + .common_key(key) + .ok_or(StdError::not_found("key not found"))? + .to_vec(); + self.set_storage_value(key, value) + } + + fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>); + + fn set_storage_value( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + self.set_storage(key, &to_json_vec(value)?); + Ok(()) + } +} + +pub trait ArbitraryContractStorageReader { + fn may_read_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> Option>; + + fn must_read_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> StdResult> { + let key = key.as_ref(); + self.may_read_from_contract_storage(address, key) + .ok_or(StdError::not_found(format!("no data under {key:?}"))) + } + + fn may_read_value_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> StdResult> { + let Some(bytes) = self.may_read_from_contract_storage(address, key) else { + return Ok(None); + }; + + from_json(&bytes).map(Some) + } + + fn must_read_value_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> StdResult { + let bytes = self.must_read_from_contract_storage(address, key)?; + from_json(&bytes) + } +} + +pub trait ArbitraryContractStorageWriter { + fn set_contract_storage( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ); + + fn set_contract_storage_value( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + self.set_contract_storage(address, key, &to_json_vec(value)?); + Ok(()) + } + + // attempts to write to an arbitrary contract `cw_storage_plus::Map` + fn set_contract_map_value<'a, K, T>( + &mut self, + address: impl Into, + namespace: impl AsRef<[u8]>, + key: K, + value: &T, + ) -> StdResult<()> + where + K: PrimaryKey<'a>, + T: Serialize + DeserializeOwned, + { + let key_path: Path = Path::new( + namespace.as_ref(), + &key.key().iter().map(Key::as_ref).collect::>(), + ); + let storage_key = key_path.deref(); + self.set_contract_storage_value(address, storage_key, value) + } +} + +// contract that has an admin +pub trait AdminExt: StorageReader + StorageWriter { + fn admin(&self) -> Option { + self.read_common_value(CommonStorageKeys::Admin) + } + + fn update_admin(&mut self, admin: &Option) -> StdResult<()> { + self.set_common_value(CommonStorageKeys::Admin, admin) + } + + fn admin_unchecked(&self) -> Addr { + self.admin().expect("no admin set") + } + + fn admin_msg(&self) -> MessageInfo { + message_info(&self.admin_unchecked(), &[]) + } +} + +// contract that operates on some specific coin denom +pub trait DenomExt: StorageReader { + fn denom(&self) -> String { + self.unchecked_read_common_value(CommonStorageKeys::Denom) + } + + fn coin(&self, amount: u128) -> Coin { + coin(amount, self.denom()) + } + + fn coins(&self, amount: u128) -> Vec { + coins(amount, self.denom()) + } +} + +pub trait RandExt { + fn raw_rng(&mut self) -> &mut ChaCha20Rng; + + fn generate_account(&mut self) -> Addr; + + fn generate_account_with_balance(&mut self) -> Addr + where + Self: BankExt; +} + +pub trait BankExt { + fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()>; +} + +impl AdminExt for T where T: StorageReader + StorageWriter {} +impl DenomExt for T where T: StorageReader {} + +impl StorageReader for ContractTester { + fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]> { + self.common_storage_keys.get(&key).map(|v| &**v) + } + + fn read_from_contract_storage(&self, key: impl AsRef<[u8]>) -> Option { + ::read_from_contract_storage(self, key) + } +} + +impl StorageWriter for ContractTester { + fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) { + ::set_contract_storage(self, key, value) + } +} + +impl BankExt for ContractTester { + fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()> { + self.app + .send_tokens(self.master_address.clone(), to, &[amount])?; + Ok(()) + } +} + +impl RandExt for ContractTester { + fn raw_rng(&mut self) -> &mut ChaCha20Rng { + &mut self.rng + } + + fn generate_account(&mut self) -> Addr { + self.app + .api() + .addr_make(&format!("foomp{}", self.rng.next_u64())) + } + + fn generate_account_with_balance(&mut self) -> Addr + where + Self: BankExt, + { + let addr = self.generate_account(); + let million = 1_000_000_000_000; + self.send_tokens(addr.clone(), coin(million, TEST_DENOM)) + .unwrap(); + addr + } +} + +impl ArbitraryContractStorageReader for StorageWrapper { + fn may_read_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> Option> { + self.contract_storage_wrapper(&Addr::unchecked(address)) + .get(key.as_ref()) + } +} + +impl ArbitraryContractStorageWriter for StorageWrapper { + fn set_contract_storage( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + // yeah, we're unnecessarily cloning a Rc pointer, but this is a test code, so this inefficiency is fine + let mut wrapped_storage = self + .clone() + .contract_storage_wrapper(&Addr::unchecked(address)); + wrapped_storage.set(key.as_ref(), value.as_ref()); + } +} + +impl ArbitraryContractStorageReader for ContractTester +where + C: TestableNymContract, +{ + fn may_read_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> Option> { + self.storage + .as_inner_storage() + .may_read_from_contract_storage(address, key) + } +} + +impl ArbitraryContractStorageWriter for ContractTester +where + C: TestableNymContract, +{ + fn set_contract_storage( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + self.storage + .as_inner_storage_mut() + .set_contract_storage(address, key, value); + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs new file mode 100644 index 0000000000..2d041a0933 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs @@ -0,0 +1,276 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{mock_api, test_rng, TEST_DENOM}; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{ + coin, coins, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, QuerierWrapper, + Record, Response, Storage, +}; +use cw_multi_test::{App, AppBuilder, BankKeeper, Contract, ContractWrapper, Executor}; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; + +pub use basic_traits::*; +pub use extensions::*; + +pub use crate::tester::storage_wrapper::{ContractStorageWrapper, StorageWrapper}; + +mod basic_traits; +mod extensions; +mod storage_wrapper; +// copied from cw-multi-test (but removed generics for custom messages and querier for we don't need them for now) + +pub type ContractFn = + fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result; +pub type QueryFn = fn(deps: Deps, env: Env, msg: T) -> Result; +pub type PermissionedFn = fn(deps: DepsMut, env: Env, msg: T) -> Result; + +pub type ContractClosure = Box Result>; +pub type QueryClosure = Box Result>; + +pub trait TestableNymContract { + const NAME: &'static str; + + type InitMsg: DeserializeOwned + Serialize + Debug + 'static; + type ExecuteMsg: DeserializeOwned + Serialize + Debug + 'static; + type QueryMsg: DeserializeOwned + Serialize + Debug + 'static; + type MigrateMsg: DeserializeOwned + Serialize + Debug + 'static; + type ContractError: Display + Debug + Send + Sync + 'static; + + fn instantiate() -> ContractFn; + fn execute() -> ContractFn; + fn query() -> QueryFn; + fn migrate() -> PermissionedFn; + + fn base_init_msg() -> Self::InitMsg; + + // // for now we don't care about custom queriers + // fn contract_wrapper() -> ContractWrapper< + // Self::ExecuteMsg, + // Self::InitMsg, + // Self::QueryMsg, + // Self::ContractError, + // anyhow::Error, + // anyhow::Error, + // Empty, + // Empty, + // Empty, + // Self::ContractError, + // Self::ContractError, + // Self::MigrateMsg, + // Self::ContractError, + // > { + // ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query()) + // .with_migrate(Self::migrate()) + // } + + fn dyn_contract() -> Box> { + Box::new( + ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query()) + .with_migrate(Self::migrate()), + ) + } + + fn init() -> ContractTester + where + Self: Sized, + { + ContractTesterBuilder::new() + .instantiate::(None) + .build() + } +} + +pub struct ContractTesterBuilder { + contract: PhantomData, + master_address: Addr, + app: App, + storage: StorageWrapper, + pub well_known_contracts: HashMap<&'static str, Addr>, +} + +impl ContractTesterBuilder { + #[allow(clippy::new_without_default)] + pub fn new() -> Self + where + C: TestableNymContract, + { + let storage = StorageWrapper::new(); + + let api = mock_api(); + let master_address = api.addr_make("master-owner"); + + let app = AppBuilder::new() + .with_api(api) + .with_storage(storage.clone()) + .build(|router, _api, storage| { + router + .bank + .init_balance( + storage, + &master_address, + coins(1000000000000000, TEST_DENOM), + ) + .unwrap() + }); + + ContractTesterBuilder { + contract: Default::default(), + master_address, + app, + storage, + well_known_contracts: Default::default(), + } + } + + pub fn instantiate( + mut self, + custom_init_msg: Option, + ) -> ContractTesterBuilder { + let code_id = self.app.store_code(D::dyn_contract()); + let contract_address = self + .app + .instantiate_contract( + code_id, + self.master_address.clone(), + &custom_init_msg.unwrap_or(D::base_init_msg()), + &[], + D::NAME, + Some(self.master_address.to_string()), + ) + .unwrap(); + + // send some tokens to the contract + self.app + .send_tokens( + self.master_address.clone(), + contract_address.clone(), + &[coin(100000000, TEST_DENOM)], + ) + .unwrap(); + + self.well_known_contracts.insert(D::NAME, contract_address); + self + } + + pub fn build(self) -> ContractTester + where + C: TestableNymContract, + { + if !self.well_known_contracts.contains_key(C::NAME) { + panic!("{} contract has not been instantiated", C::NAME); + } + + let contract_address = self.well_known_contracts[C::NAME].clone(); + + ContractTester { + contract: self.contract, + app: self.app, + rng: test_rng(), + master_address: self.master_address, + storage: self.storage.contract_storage_wrapper(&contract_address), + contract_address, + common_storage_keys: Default::default(), + well_known_contracts: self.well_known_contracts, + } + } + + pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper { + self.storage.contract_storage_wrapper(contract) + } + + pub fn api(&self) -> MockApi { + *self.app.api() + } + + pub fn querier(&self) -> QuerierWrapper { + self.app.wrap() + } +} + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] +pub enum CommonStorageKeys { + Admin, + Denom, +} + +pub struct ContractTester { + contract: PhantomData, + pub app: App, + pub rng: ChaCha20Rng, + pub contract_address: Addr, + pub master_address: Addr, + pub(crate) storage: ContractStorageWrapper, + pub common_storage_keys: HashMap>, + + // TODO: limitation: doesn't allow multiple contracts of the same type (but that's fine for the time being) + pub well_known_contracts: HashMap<&'static str, Addr>, +} + +impl ContractTester +where + C: TestableNymContract, +{ + pub fn insert_common_storage_key(&mut self, key: CommonStorageKeys, value: impl AsRef<[u8]>) { + self.common_storage_keys + .insert(key, value.as_ref().to_vec()); + } + + pub fn with_common_storage_key( + mut self, + key: CommonStorageKeys, + value: impl AsRef<[u8]>, + ) -> Self { + self.insert_common_storage_key(key, value); + self + } +} + +impl Storage for ContractTester +where + C: TestableNymContract, +{ + fn get(&self, key: &[u8]) -> Option> { + self.storage.get(key) + } + + fn range<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'a> { + self.storage.range(start, end, order) + } + + fn range_keys<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> + 'a> { + self.storage.range_keys(start, end, order) + } + + fn range_values<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> + 'a> { + self.storage.range_values(start, end, order) + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + self.storage.set(key, value) + } + + fn remove(&mut self, key: &[u8]) { + self.storage.remove(key) + } +} diff --git a/contracts/nym-pool/src/testing/storage.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs similarity index 89% rename from contracts/nym-pool/src/testing/storage.rs rename to common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs index 14ba3e955a..ab60628a02 100644 --- a/contracts/nym-pool/src/testing/storage.rs +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs @@ -11,7 +11,7 @@ use std::rc::Rc; pub struct StorageWrapper(Rc>); impl StorageWrapper { - pub(super) fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper { + pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper { ContractStorageWrapper { address: contract.clone(), inner: self.clone(), @@ -24,7 +24,7 @@ impl StorageWrapper { } #[derive(Debug, Clone)] -pub(crate) struct ContractStorageWrapper { +pub struct ContractStorageWrapper { address: Addr, inner: StorageWrapper, } @@ -33,6 +33,22 @@ impl ContractStorageWrapper { pub fn inner_storage(&self) -> StorageWrapper { self.inner.clone() } + + pub fn as_inner_storage(&self) -> &StorageWrapper { + &self.inner + } + + pub fn as_inner_storage_mut(&mut self) -> &mut StorageWrapper { + &mut self.inner + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn change_contract(&self, contract: &Addr) -> Self { + ContractStorageWrapper { + address: contract.clone(), + inner: self.inner.clone(), + } + } } impl Storage for StorageWrapper { diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 4a80db6afa..c13d29a7ec 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -18,6 +18,7 @@ serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } serde_json = { workspace = true } [build-dependencies] diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index e0f5845fa0..e8657fb265 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -35,7 +35,7 @@ pub enum ContractsCommonError { /// Percent represents a value between 0 and 100% /// (i.e. between 0.0 and 1.0) #[cw_serde] -#[derive(Copy, Default, PartialOrd)] +#[derive(Copy, Default, PartialOrd, Ord, Eq)] pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); impl Percent { @@ -80,6 +80,44 @@ impl Percent { pub fn checked_pow(&self, exp: u32) -> Result { self.0.checked_pow(exp).map(Percent) } + + // truncate provided percent to only have 2 decimal places, + // e.g. convert "0.1234567" into "0.12" + // the purpose of it is to reduce storage space, in particular for the performance contract + // since that extra precision gains us nothing + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn round_to_two_decimal_places(&self) -> Self { + let raw = self.0; + + const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18 + const THRESHOLD: Decimal = Decimal::permille(5); // 0.005 + + // in case it ever changes since it's not exposed in the public API + debug_assert_eq!( + DECIMAL_FRACTIONAL, + Uint128::new(10).pow(Decimal::DECIMAL_PLACES) + ); + + let int = (raw.atomics() * Uint128::new(100)) / DECIMAL_FRACTIONAL; + + #[allow(clippy::unwrap_used)] + let floored = Decimal::from_atomics(int, 2).unwrap(); + let diff = raw - floored; + let rounded = if diff >= THRESHOLD { + // ceil + floored + Decimal::percent(1) + } else { + floored + }; + Percent(rounded) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn average(&self, other: &Self) -> Self { + let sum = self.0 + other.0; + let inner = Decimal::from_ratio(sum.numerator(), sum.denominator() * Uint128::new(2)); + Percent(inner) + } } impl Display for Percent { @@ -334,6 +372,7 @@ mod tests { } #[test] + #[cfg(feature = "naive_float")] fn naive_float_conversion() { // around 15 decimal places is the maximum precision we can handle // which is still way more than enough for what we use it for @@ -347,4 +386,41 @@ mod tests { assert!(converted.0 - converted.0 < epsilon); } + + #[test] + fn rounding_percent() { + let test_cases = vec![ + ("0", "0"), + ("0.1", "0.1"), + ("0.12", "0.12"), + ("0.12", "0.123"), + ("0.12", "0.123456789"), + ("0.13", "0.125"), + ("0.13", "0.126"), + ("0.13", "0.126436545676"), + ("0.99", "0.99"), + ("0.99", "0.994"), + ("1", "0.999"), + ("1", "0.995"), + ]; + for (expected, input) in test_cases { + let expected: Percent = expected.parse().unwrap(); + let pre_truncated: Percent = input.parse().unwrap(); + assert_eq!(expected, pre_truncated.round_to_two_decimal_places()) + } + } + + #[test] + fn calculating_average() -> anyhow::Result<()> { + fn p(raw: &str) -> Percent { + raw.parse().unwrap() + } + + assert_eq!(p("0.1").average(&p("0.1")), p("0.1")); + assert_eq!(p("0.1").average(&p("0.2")), p("0.15")); + assert_eq!(p("1").average(&p("0")), p("0.5")); + assert_eq!(p("0.123").average(&p("0.456")), p("0.2895")); + + Ok(()) + } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index abe502c50e..d0a7b3b913 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -23,7 +23,6 @@ semver = { workspace = true, features = ["serde"] } schemars = { workspace = true } thiserror = { workspace = true } contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" } -serde-json-wasm = { workspace = true } humantime-serde = { workspace = true } utoipa = { workspace = true, optional = true } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index deeb416887..72e848aef1 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -3,7 +3,7 @@ use crate::{IdentityKey, NodeId, SphinxKey}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Coin}; +use cosmwasm_std::{to_json_string, Addr, Coin}; use std::cmp::Ordering; use std::fmt::Display; @@ -154,7 +154,7 @@ pub struct GatewayConfigUpdate { impl GatewayConfigUpdate { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 64b69fe737..91b4222fbe 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -16,7 +16,7 @@ use crate::{ Percent, ProfitMarginRange, SphinxKey, }; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; +use cosmwasm_std::{to_json_string, Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -604,7 +604,7 @@ pub struct NodeCostParams { impl NodeCostParams { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } @@ -773,7 +773,7 @@ pub struct MixNodeConfigUpdate { impl MixNodeConfigUpdate { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 55bba8f9a6..e74a56e122 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -5,7 +5,7 @@ use crate::helpers::IntoBaseDecimal; use crate::nym_node::Role; use crate::{error::MixnetContractError, Percent}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Decimal; +use cosmwasm_std::{to_json_string, Decimal}; pub type Performance = Percent; pub type WorkFactor = Decimal; @@ -84,7 +84,7 @@ pub struct IntervalRewardParams { impl IntervalRewardParams { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } @@ -410,7 +410,7 @@ impl IntervalRewardingParamsUpdate { } pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index ce54caab33..374b30bf0c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -44,6 +44,17 @@ pub struct RewardEstimate { pub operating_cost: Decimal, } +impl RewardEstimate { + pub const fn zero() -> RewardEstimate { + RewardEstimate { + total_node_reward: Decimal::zero(), + operator: Decimal::zero(), + delegates: Decimal::zero(), + operating_cost: Decimal::zero(), + } + } +} + #[cw_serde] #[derive(Copy, Default)] pub struct RewardDistribution { diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml b/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml new file mode 100644 index 0000000000..078351a348 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "nym-performance-contract-common" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +thiserror = { workspace = true } +serde = { workspace = true } +schemars = { workspace = true } + +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-controllers = { workspace = true } + +nym-contracts-common = { path = "../contracts-common" } + + +[features] +schema = [] + +[lints] +workspace = true diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs new file mode 100644 index 0000000000..5f98a8e439 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod storage_keys { + pub const CONTRACT_ADMIN: &str = "contract-admin"; + pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id"; + pub const LAST_SUBMISSION: &str = "last-submission"; + pub const MIXNET_CONTRACT: &str = "mixnet-contract"; + pub const AUTHORISED_COUNT: &str = "authorised-count"; + pub const AUTHORISED: &str = "authorised"; + pub const RETIRED: &str = "retired"; + pub const PERFORMANCE_RESULTS: &str = "performance-results"; + pub const SUBMISSION_METADATA: &str = "submission-metadata"; +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs new file mode 100644 index 0000000000..68d4e8832b --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs @@ -0,0 +1,39 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{EpochId, NodeId}; +use cosmwasm_std::Addr; +use cw_controllers::AdminError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum NymPerformanceContractError { + #[error("could not perform contract migration: {comment}")] + FailedMigration { comment: String }, + + #[error(transparent)] + Admin(#[from] AdminError), + + #[error(transparent)] + StdErr(#[from] cosmwasm_std::StdError), + + #[error("{address} is already an authorised network monitor")] + AlreadyAuthorised { address: Addr }, + + #[error("{address} is not an authorised network monitor")] + NotAuthorised { address: Addr }, + + #[error("attempted to submit performance data for epoch {epoch_id} and node {node_id} whilst last submitted was {last_epoch_id} for node {last_node_id}")] + StalePerformanceSubmission { + epoch_id: EpochId, + node_id: NodeId, + last_epoch_id: EpochId, + last_node_id: NodeId, + }, + + #[error("the batch performance data has not been sorted")] + UnsortedBatchSubmission, + + #[error("node {node_id} does not appear to be bonded")] + NodeNotBonded { node_id: NodeId }, +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs new file mode 100644 index 0000000000..7e1e3cacd6 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs new file mode 100644 index 0000000000..4fc43b8bfe --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod constants; +pub mod error; +pub mod helpers; +pub mod msg; +pub mod types; + +pub use error::*; +pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +pub use types::*; diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs new file mode 100644 index 0000000000..74f512d891 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs @@ -0,0 +1,125 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{EpochId, NodeId, NodePerformance}; +use cosmwasm_schema::cw_serde; + +#[cfg(feature = "schema")] +use crate::types::{ + EpochMeasurementsPagedResponse, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, LastSubmission, NetworkMonitorResponse, + NetworkMonitorsPagedResponse, NodeMeasurementsResponse, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitorsPagedResponse, +}; + +#[cw_serde] +pub struct InstantiateMsg { + pub mixnet_contract_address: String, + pub authorised_network_monitors: Vec, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Change the admin + UpdateAdmin { admin: String }, + + /// Attempt to submit performance data of a particular node for given epoch + Submit { + epoch: EpochId, + data: NodePerformance, + }, + + /// Attempt to submit performance data of a batch of nodes for given epoch + BatchSubmit { + epoch: EpochId, + data: Vec, + }, + + /// Attempt to authorise new network monitor for submitting performance data + AuthoriseNetworkMonitor { address: String }, + + /// Attempt to retire an existing network monitor and forbid it from submitting any future performance data + RetireNetworkMonitor { address: String }, + + /// An admin method to remove submitted node measurements. Used as an escape hatch should + /// the data stored get too unwieldy. + RemoveNodeMeasurements { epoch_id: EpochId, node_id: NodeId }, + + /// An admin method to remove submitted nodes measurements. Used as an escape hatch should + /// the data stored get too unwieldy. Note: it is expected to get called multiple times + /// until the response indicates all the epoch data has been removed. + RemoveEpochMeasurements { epoch_id: EpochId }, +} + +#[cw_serde] +#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))] +pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))] + Admin {}, + + /// Returns performance of particular node for the provided epoch + #[cfg_attr(feature = "schema", returns(NodePerformanceResponse))] + NodePerformance { epoch_id: EpochId, node_id: NodeId }, + + /// Returns historical performance for particular node + #[cfg_attr(feature = "schema", returns(NodePerformancePagedResponse))] + NodePerformancePaged { + node_id: NodeId, + start_after: Option, + limit: Option, + }, + + /// Returns all submitted measurements for the particular node + #[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))] + NodeMeasurements { epoch_id: EpochId, node_id: NodeId }, + + /// Returns (paged) measurements for particular epoch + #[cfg_attr(feature = "schema", returns(EpochMeasurementsPagedResponse))] + EpochMeasurementsPaged { + epoch_id: EpochId, + start_after: Option, + limit: Option, + }, + + /// Returns (paged) performance for particular epoch + #[cfg_attr(feature = "schema", returns(EpochPerformancePagedResponse))] + EpochPerformancePaged { + epoch_id: EpochId, + start_after: Option, + limit: Option, + }, + + /// Returns full (paged) historical performance of the whole network + #[cfg_attr(feature = "schema", returns(FullHistoricalPerformancePagedResponse))] + FullHistoricalPerformancePaged { + start_after: Option<(EpochId, NodeId)>, + limit: Option, + }, + + /// Returns information about particular network monitor + #[cfg_attr(feature = "schema", returns(NetworkMonitorResponse))] + NetworkMonitor { address: String }, + + /// Returns information about all network monitors + #[cfg_attr(feature = "schema", returns(NetworkMonitorsPagedResponse))] + NetworkMonitorsPaged { + start_after: Option, + limit: Option, + }, + + /// Returns information about all retired network monitors + #[cfg_attr(feature = "schema", returns(RetiredNetworkMonitorsPagedResponse))] + RetiredNetworkMonitorsPaged { + start_after: Option, + limit: Option, + }, + + /// Returns information regarding the latest submitted performance data + #[cfg_attr(feature = "schema", returns(LastSubmission))] + LastSubmittedMeasurement {}, +} + +#[cw_serde] +pub struct MigrateMsg { + // +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs new file mode 100644 index 0000000000..383fb08280 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs @@ -0,0 +1,258 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Env, Timestamp}; +use nym_contracts_common::Percent; + +pub type EpochId = u32; +pub type NodeId = u32; + +#[cw_serde] +pub struct LastSubmission { + pub block_height: u64, + pub block_time: Timestamp, + + // not as relevant, but might as well store it + pub data: Option, +} + +#[cw_serde] +pub struct LastSubmittedData { + pub sender: Addr, + pub epoch_id: EpochId, + pub data: NodePerformance, +} + +#[cw_serde] +pub struct NetworkMonitorDetails { + pub address: Addr, + pub authorised_by: Addr, + pub authorised_at_height: u64, +} + +impl NetworkMonitorDetails { + pub fn retire(self, env: &Env, sender: &Addr) -> RetiredNetworkMonitor { + RetiredNetworkMonitor { + details: self, + retired_by: sender.clone(), + retired_at_height: env.block.height, + } + } +} + +#[cw_serde] +pub struct RetiredNetworkMonitor { + pub details: NetworkMonitorDetails, + pub retired_by: Addr, + pub retired_at_height: u64, +} + +#[cw_serde] +#[derive(Copy)] +pub struct NodePerformance { + #[serde(rename = "n")] + pub node_id: NodeId, + + // note: value is rounded to 2 decimal places. + #[serde(rename = "p")] + pub performance: Percent, +} + +#[cw_serde] +pub struct NetworkMonitorSubmissionMetadata { + pub last_submitted_epoch_id: EpochId, + pub last_submitted_node_id: NodeId, +} + +// the internal values are always sorted +#[cw_serde] +pub struct NodeResults(Vec); + +impl NodeResults { + pub fn new(initial: Percent) -> NodeResults { + NodeResults(vec![initial.round_to_two_decimal_places()]) + } + + // ASSUMPTION: number of NM will be relatively small, so loading the whole vector of values + // to insert new one and resave is cheap + pub fn insert_new(&mut self, result: Percent) { + let result = result.round_to_two_decimal_places(); + let pos = self.0.binary_search(&result).unwrap_or_else(|e| e); + self.0.insert(pos, result); + } + + // SAFETY: there are no codepaths that allow constructing empty struct + pub fn median(&self) -> Percent { + let len = self.0.len(); + if len % 2 == 1 { + // odd number of elements: return the middle one + self.0[len / 2] + } else { + // even number: average the two middle elements + let mid1 = self.0[len / 2 - 1]; + let mid2 = self.0[len / 2]; + mid1.average(&mid2).round_to_two_decimal_places() + } + } + + pub fn inner(&self) -> &[Percent] { + &self.0 + } +} + +#[cw_serde] +pub struct NodePerformanceResponse { + pub performance: Option, +} + +#[cw_serde] +pub struct NodeMeasurementsResponse { + pub measurements: Option, +} + +#[cw_serde] +#[derive(Copy)] +pub struct EpochNodePerformance { + pub epoch: EpochId, + pub performance: Option, +} + +#[cw_serde] +pub struct NodePerformancePagedResponse { + pub node_id: NodeId, + pub performance: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +pub struct EpochPerformancePagedResponse { + pub epoch_id: EpochId, + pub performance: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +pub struct NodeMeasurement { + pub node_id: NodeId, + pub measurements: NodeResults, +} + +#[cw_serde] +pub struct EpochMeasurementsPagedResponse { + pub epoch_id: EpochId, + pub measurements: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +#[derive(Copy)] +pub struct HistoricalPerformance { + pub epoch_id: EpochId, + pub node_id: NodeId, + pub performance: Percent, +} + +#[cw_serde] +pub struct FullHistoricalPerformancePagedResponse { + pub performance: Vec, + pub start_next_after: Option<(EpochId, NodeId)>, +} + +#[cw_serde] +pub struct NetworkMonitorInformation { + pub details: NetworkMonitorDetails, + pub current_submission_metadata: NetworkMonitorSubmissionMetadata, +} + +#[cw_serde] +pub struct NetworkMonitorResponse { + pub info: Option, +} + +#[cw_serde] +pub struct NetworkMonitorsPagedResponse { + pub info: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +pub struct RetiredNetworkMonitorsPagedResponse { + pub info: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +pub struct RemoveEpochMeasurementsResponse { + pub additional_entries_to_remove_remaining: bool, +} + +#[cw_serde] +#[derive(Default)] +pub struct BatchSubmissionResult { + pub accepted_scores: u64, + pub non_existent_nodes: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn p(raw: impl AsRef) -> Percent { + raw.as_ref().parse().unwrap() + } + + fn ps(raw: &[&str]) -> Vec { + raw.iter().map(p).collect() + } + + #[test] + fn node_results_insertion() { + let initial = NodeResults::new(p("0.5")); + + let mut smaller = initial.clone(); + let mut greater = initial.clone(); + + smaller.insert_new(p("0.4")); + greater.insert_new(p("0.6")); + + assert_eq!(smaller.0, ps(&["0.4", "0.5"])); + assert_eq!(greater.0, ps(&["0.5", "0.6"])); + + let mut another = NodeResults(ps(&["0.1", "0.4", "0.5", "0.6", "0.6", "1.0"])); + another.insert_new(p("0.6")); + another.insert_new(p("0.2")); + another.insert_new(p("0.7")); + another.insert_new(p("0.3")); + another.insert_new(p("0.3")); + another.insert_new(p("0.55")); + + assert_eq!( + another.0, + ps(&[ + "0.1", "0.2", "0.3", "0.3", "0.4", "0.5", "0.55", "0.6", "0.6", "0.6", "0.7", "1.0" + ]) + ); + } + + #[test] + fn node_results_median() { + let results = NodeResults(ps(&["0.1"])); + assert_eq!(results.median(), p("0.1")); + + let results = NodeResults(ps(&["0.1", "0.2"])); + assert_eq!(results.median(), p("0.15")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3"])); + assert_eq!(results.median(), p("0.2")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4"])); + assert_eq!(results.median(), p("0.25")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4", "0.5"])); + assert_eq!(results.median(), p("0.3")); + + let results = NodeResults(ps(&["0", "0", "1", "1", "1", "1", "1"])); + assert_eq!(results.median(), p("1")); + } +} diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index b822d49db6..aa9b615805 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -20,6 +20,8 @@ nym-credentials = { path = "../credentials" } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } nym-ecash-time = { path = "../ecash-time" } +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard] +path = "../../sqlx-pool-guard" [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true @@ -31,8 +33,13 @@ features = ["rt-multi-thread", "net", "signal", "fs"] [build-dependencies] -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [features] -persistent-storage = ["bincode", "serde"] \ No newline at end of file +persistent-storage = ["bincode", "serde"] diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 3d91741ee3..6ac45aec47 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -7,10 +7,11 @@ use crate::models::{ }; use nym_ecash_time::Date; use sqlx::{Executor, Sqlite, Transaction}; +use sqlx_pool_guard::SqlitePoolGuard; #[derive(Clone)] pub struct SqliteEcashTicketbookManager { - connection_pool: sqlx::SqlitePool, + connection_pool: SqlitePoolGuard, } impl SqliteEcashTicketbookManager { @@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager { /// # Arguments /// /// * `connection_pool`: database connection pool to use. - pub fn new(connection_pool: sqlx::SqlitePool) -> Self { + pub fn new(connection_pool: SqlitePoolGuard) -> Self { SqliteEcashTicketbookManager { connection_pool } } @@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager { "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", deadline ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager { data, expiration_date, ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) @@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager { epoch_id, total_tickets, used_tickets, - ).execute(&self.connection_pool).await?; + ).execute(&*self.connection_pool).await?; Ok(()) } @@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager { "#, ) .bind(data) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await? .is_some(); @@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager { FROM ecash_ticketbook "#, ) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager { ticketbook_id, expected_current_total_spent ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await? .rows_affected(); Ok(affected > 0) @@ -153,7 +154,7 @@ impl SqliteEcashTicketbookManager { &self, ) -> Result, sqlx::Error> { sqlx::query_as("SELECT * FROM pending_issuance") - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager { "DELETE FROM pending_issuance WHERE deposit_id = ?", pending_id ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager { "#, epoch_id ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, epoch_id ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager { "#, epoch_id ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, epoch_id, ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager { "#, expiration_date ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, expiration_date ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index 565ab94513..3c057532ad 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -37,6 +37,7 @@ use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; +use sqlx_pool_guard::SqlitePoolGuard; use std::path::Path; use zeroize::Zeroizing; @@ -54,15 +55,15 @@ impl PersistentStorage { /// * `database_path`: path to the database. pub async fn init>(database_path: P) -> Result { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); let opts = sqlx::sqlite::SqliteConnectOptions::new() .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) .auto_vacuum(SqliteAutoVacuum::Incremental) - .filename(database_path) + .filename(&database_path) .create_if_missing(true) .disable_statement_logging(); @@ -74,13 +75,17 @@ impl PersistentStorage { } }; - if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + let connection_pool = + SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool); + + if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await { error!("Failed to perform migration on the SQLx database: {err}"); + connection_pool.close().await; return Err(err.into()); } Ok(PersistentStorage { - storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()), + storage_manager: SqliteEcashTicketbookManager::new(connection_pool), }) } } diff --git a/common/credential-verification/src/bandwidth_storage_manager.rs b/common/credential-verification/src/bandwidth_storage_manager.rs index c861ef123f..5d51091346 100644 --- a/common/credential-verification/src/bandwidth_storage_manager.rs +++ b/common/credential-verification/src/bandwidth_storage_manager.rs @@ -88,7 +88,8 @@ impl BandwidthStorageManager { debug!(available = available_bi2, required = required_bi2); self.consume_bandwidth(required_bandwidth).await?; - Ok(available_bandwidth) + let remaining_bandwidth = self.client_bandwidth.available().await; + Ok(remaining_bandwidth) } async fn expire_bandwidth(&mut self) -> Result<()> { diff --git a/common/dkg/benches/benchmarks.rs b/common/dkg/benches/benchmarks.rs index 2951a57a7c..d12305bba7 100644 --- a/common/dkg/benches/benchmarks.rs +++ b/common/dkg/benches/benchmarks.rs @@ -14,6 +14,7 @@ use nym_dkg::bte::{ }; use nym_dkg::interpolation::polynomial::Polynomial; use nym_dkg::{combine_shares, Dealing, NodeIndex, Share, Threshold}; +use rand::CryptoRng; use rand_core::{RngCore, SeedableRng}; use std::collections::BTreeMap; @@ -31,7 +32,7 @@ pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) { } fn prepare_keys( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, nodes: usize, ) -> (BTreeMap, Vec) { let params = setup(); @@ -50,7 +51,7 @@ fn prepare_keys( } fn prepare_resharing( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, params: &Params, nodes: usize, threshold: Threshold, @@ -68,7 +69,7 @@ fn prepare_resharing( for (i, ref mut dk) in dks.iter_mut().enumerate() { let shares = first_dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(params, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -154,7 +155,9 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -237,7 +240,9 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -320,7 +325,9 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -547,7 +554,7 @@ pub fn share_decryption(c: &mut Criterion) { let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng); c.bench_function("single share decryption", |b| { - b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None))) + b.iter(|| black_box(decrypt_share(¶ms, &dk, 0, &ciphertexts, None))) }); } diff --git a/common/dkg/src/bte/encryption.rs b/common/dkg/src/bte/encryption.rs index 8cd6a3e774..81b304bc7b 100644 --- a/common/dkg/src/bte/encryption.rs +++ b/common/dkg/src/bte/encryption.rs @@ -9,6 +9,7 @@ use crate::{Chunk, ChunkedShare, Share}; use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar}; use ff::Field; use group::{Curve, Group, GroupEncoding}; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::HashMap; use std::ops::Neg; @@ -191,7 +192,7 @@ impl HazmatRandomness { pub fn encrypt_shares( shares: &[(&Share, &PublicKey)], params: &Params, - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, ) -> (Ciphertexts, HazmatRandomness) { let g1 = G1Projective::generator(); @@ -262,6 +263,7 @@ pub fn encrypt_shares( } pub fn decrypt_share( + params: &Params, dk: &DecryptionKey, // in the case of multiple receivers, specifies which index of ciphertext chunks should be used i: usize, @@ -270,6 +272,10 @@ pub fn decrypt_share( ) -> Result { let mut plaintext = ChunkedShare::default(); + if !ciphertext.verify_integrity(params) { + return Err(DkgError::FailedCiphertextIntegrityCheck); + } + if i >= ciphertext.ciphertext_chunks.len() { return Err(DkgError::UnavailableCiphertext(i)); } @@ -461,10 +467,22 @@ mod tests { let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); - let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); - let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); + let recovered1 = decrypt_share( + ¶ms, + &decryption_key1, + 0, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); + let recovered2 = decrypt_share( + ¶ms, + &decryption_key2, + 1, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } @@ -490,10 +508,22 @@ mod tests { let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); - let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); - let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); + let recovered1 = decrypt_share( + ¶ms, + &decryption_key1, + 0, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); + let recovered2 = decrypt_share( + ¶ms, + &decryption_key2, + 1, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } @@ -574,7 +604,10 @@ mod tests { #[test] fn ciphertexts_roundtrip() { - fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts { + fn random_ciphertexts( + mut rng: impl RngCore + CryptoRng, + num_receivers: usize, + ) -> Ciphertexts { Ciphertexts { rr: (0..NUM_CHUNKS) .map(|_| G1Projective::random(&mut rng)) diff --git a/common/dkg/src/bte/keys.rs b/common/dkg/src/bte/keys.rs index 132f161f5e..20d896a547 100644 --- a/common/dkg/src/bte/keys.rs +++ b/common/dkg/src/bte/keys.rs @@ -9,11 +9,15 @@ use bls12_381::{G1Projective, G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; +use rand::CryptoRng; use rand_core::RngCore; use zeroize::Zeroize; // produces public key and a decryption key for the root of the tree -pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) { +pub fn keygen( + params: &Params, + mut rng: impl RngCore + CryptoRng, +) -> (DecryptionKey, PublicKeyWithProof) { let g1 = G1Projective::generator(); let g2 = G2Projective::generator(); @@ -244,7 +248,7 @@ pub struct KeyPair { } impl KeyPair { - pub fn new(params: &Params, rng: impl RngCore) -> Self { + pub fn new(params: &Params, rng: impl RngCore + CryptoRng) -> Self { let (dk, pk) = keygen(params, rng); Self { private_key: dk, diff --git a/common/dkg/src/bte/proof_chunking.rs b/common/dkg/src/bte/proof_chunking.rs index 1365aecbd7..2da6e83b33 100644 --- a/common/dkg/src/bte/proof_chunking.rs +++ b/common/dkg/src/bte/proof_chunking.rs @@ -10,7 +10,7 @@ use crate::utils::{deserialize_scalar, RandomOracleBuilder}; use bls12_381::{G1Projective, Scalar}; use ff::Field; use group::{Group, GroupEncoding}; -use rand::Rng; +use rand::{CryptoRng, Rng}; use rand_core::{RngCore, SeedableRng}; const CHUNKING_ORACLE_DOMAIN: &[u8] = @@ -28,6 +28,7 @@ const SECURITY_PARAMETER: usize = 256; /// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper const NUM_CHALLENGE_BITS: usize = SECURITY_PARAMETER.div_ceil(PARALLEL_RUNS); +const EE: usize = 1 << NUM_CHALLENGE_BITS; // type alias for ease of use type FirstChallenge = Vec>>; @@ -94,7 +95,7 @@ impl ProofOfChunking { // Scalar(-1) would in reality be Scalar(q - 1), which is greater than Scalar(1) and opposite to // what we wanted. pub fn construct( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, instance: Instance, witness_r: &[Scalar; NUM_CHUNKS], witnesses_s: &[Share], @@ -110,21 +111,20 @@ impl ProofOfChunking { // define bounds for the blinding factors let n = instance.public_keys.len(); let m = NUM_CHUNKS; - let ee = 1 << NUM_CHALLENGE_BITS; - // CHUNK_MAX corresponds to paper's B - let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; - let zz = (2 * (PARALLEL_RUNS as u64)) - .checked_mul(ss) - .expect("overflow in Z = 2 * l * S"); + // ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) + // Z = 2 * l * S + let (ss, zz): (u64, u64) = compute_ss_zz(n, m)?; let ss_scalar = Scalar::from(ss); // rather than generating blinding factors in [-S, Z-1] directly, // do it via [0, Z - 1 + S + 1] and deal with the shift later. - let combined_upper_range = (zz - 1) - .checked_add(ss + 1) - .expect("overflow in Z - 1 + S + 1"); + // combined_upper_range = Z - 1 + S + 1 + + let combined_upper_range = zz.checked_add(ss).ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | Z - 1 + S + 1", + })?; let mut betas = Vec::with_capacity(PARALLEL_RUNS); let mut bs = Vec::with_capacity(PARALLEL_RUNS); @@ -178,12 +178,23 @@ impl ProofOfChunking { // I think this part is more readable with a range loop #[allow(clippy::needless_range_loop)] for l in 0..PARALLEL_RUNS { - let mut sum = 0; + let mut sum: u64 = 0; for (i, witness_i) in witnesses_s.iter().enumerate() { for (j, witness_ij) in witness_i.to_chunks().chunks.iter().enumerate() { debug_assert!(std::mem::size_of::() <= std::mem::size_of::()); - sum += first_challenge[i][j][l] * (*witness_ij as u64) + // sum += first_challenge[i][j][l] * (*witness_ij as u64) + sum = sum + .checked_add( + first_challenge[i][j][l] + .checked_mul(*witness_ij as u64) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | first_challenge[i][j][l] * witness_ij", + })?, + ) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | sum + (first_challenge[i][j][l] * witness_ij)", + })?; } } @@ -191,7 +202,18 @@ impl ProofOfChunking { continue 'retry_loop; } // shifted_blinding_factors[l] - ss restores it to "proper" [-S, Z - 1] range - let response = sum + shifted_blinding_factors[l] - ss; + // let response = sum + shifted_blinding_factors[l] - ss; + let response = sum + .checked_add(shifted_blinding_factors[l]) + .ok_or(DkgError::ArithmeticOverflow { + info: + "ProofOfChunking::construct | sum + (shifted_blinding_factors[l] - ss)", + })? + .checked_sub(ss) + .ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::construct | shifted_blinding_factors[l] - ss", + })?; + if response < zz { responses_chunks.push(response) } else { @@ -276,11 +298,13 @@ impl ProofOfChunking { ensure_len!(&self.responses_r, n); ensure_len!(&self.responses_chunks, PARALLEL_RUNS); - let ee = 1 << NUM_CHALLENGE_BITS; + // ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) + // Z = 2 * l * S - // CHUNK_MAX corresponds to paper's B - let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; - let zz = 2 * (PARALLEL_RUNS as u64) * ss; + let zz: u64 = match compute_ss_zz(n, m) { + Ok((_, zz_res)) => zz_res, + _ => return false, + }; for response_chunk in &self.responses_chunks { if response_chunk >= &zz { @@ -411,7 +435,7 @@ impl ProofOfChunking { random_oracle_builder.update(lambda_e.to_be_bytes()); let mut oracle = rand_chacha::ChaCha20Rng::from_seed(random_oracle_builder.finalize()); - let range_max_excl = 1 << NUM_CHALLENGE_BITS; + let range_max_excl = EE as u64; (0..n) .map(|_| { @@ -637,6 +661,50 @@ impl ProofOfChunking { } } +fn compute_ss_zz(n: usize, m: usize) -> Result<(u64, u64), DkgError> { + // let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; + // CHUNK_MAX corresponds to paper's B + + let ee = EE; + + let ss = n + .checked_mul(m) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | n * m", + })? + .checked_mul( + CHUNK_SIZE + .checked_sub(1) + .ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1)", + })? + .checked_mul(ee.checked_sub(1).ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::compute_ss_zz | (ee - 1)", + })?) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1) * (ee - 1)", + })?, + ) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | ss_lhs * ss_rhs", + })? as u64; + + // let zz = 2 * PARALLEL_RUNS as u64 * ss; + // Z = 2 * l * S + + let zz = 2u64 + .checked_mul(PARALLEL_RUNS as u64) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | 2 * l", + })? + .checked_mul(ss) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | (2 * l) * S", + })?; + + Ok((ss, zz)) +} + #[cfg(test)] mod tests { use super::*; @@ -652,7 +720,9 @@ mod tests { ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>, } - fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec) { + fn setup( + mut rng: impl RngCore + CryptoRng, + ) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec) { let g1 = G1Projective::generator(); let mut pks = Vec::with_capacity(NODES); diff --git a/common/dkg/src/bte/proof_discrete_log.rs b/common/dkg/src/bte/proof_discrete_log.rs index 95eef6eb42..3956e3837d 100644 --- a/common/dkg/src/bte/proof_discrete_log.rs +++ b/common/dkg/src/bte/proof_discrete_log.rs @@ -5,6 +5,7 @@ use crate::utils::hash_to_scalar; use bls12_381::{G1Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use zeroize::Zeroize; @@ -20,7 +21,11 @@ pub struct ProofOfDiscreteLog { } impl ProofOfDiscreteLog { - pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self { + pub fn construct( + mut rng: impl RngCore + CryptoRng, + public: &G1Projective, + witness: &Scalar, + ) -> Self { let mut rand_x = Scalar::random(&mut rng); let rand_commitment = G1Projective::generator() * rand_x; let challenge = Self::compute_challenge(public, &rand_commitment); diff --git a/common/dkg/src/bte/proof_sharing.rs b/common/dkg/src/bte/proof_sharing.rs index 4b843bb8f4..fe088c772f 100644 --- a/common/dkg/src/bte/proof_sharing.rs +++ b/common/dkg/src/bte/proof_sharing.rs @@ -9,6 +9,7 @@ use crate::{NodeIndex, Share}; use bls12_381::{G1Projective, G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::BTreeMap; @@ -87,7 +88,7 @@ pub struct ProofOfSecretSharing { impl ProofOfSecretSharing { pub fn construct( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, instance: Instance, witness_r: &Scalar, witnesses_s: &[Share], @@ -309,13 +310,14 @@ mod tests { use super::*; use crate::interpolation::polynomial::Polynomial; use group::Group; + use rand::CryptoRng; use rand_core::SeedableRng; const NODES: u64 = 50; const THRESHOLD: u64 = 40; fn setup( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, ) -> ( BTreeMap, PublicCoefficients, diff --git a/common/dkg/src/dealing.rs b/common/dkg/src/dealing.rs index a051f9f88c..e9a96886c2 100644 --- a/common/dkg/src/dealing.rs +++ b/common/dkg/src/dealing.rs @@ -13,6 +13,7 @@ use crate::utils::deserialize_g2; use crate::{NodeIndex, Share, Threshold}; use bls12_381::{G2Projective, Scalar}; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::BTreeMap; use zeroize::Zeroize; @@ -94,7 +95,7 @@ impl Dealing { // I'm not a big fan of this function signature, but I'm not clear on how to improve it while // allowing the dealer to skip decryption of its own share if it was also one of the receivers pub fn create( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng + CryptoRng, params: &Params, dealer_index: NodeIndex, threshold: Threshold, @@ -484,7 +485,7 @@ mod tests { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); derived_secrets.push( combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(), @@ -593,7 +594,7 @@ mod tests { for (i, (dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = combine_shares(shares, &dealer_indices).unwrap(); diff --git a/common/dkg/src/error.rs b/common/dkg/src/error.rs index 9329f32794..5bea0fe3a3 100644 --- a/common/dkg/src/error.rs +++ b/common/dkg/src/error.rs @@ -99,6 +99,12 @@ pub enum DkgError { "The reshared dealing has different public constant coefficient than its prior variant" )] InvalidResharing, + + #[error("Arithmetic Overflow: {info}")] + ArithmeticOverflow { info: &'static str }, + + #[error("Arithmetic Underflow: {info}")] + ArithmeticUnderflow { info: &'static str }, } impl DkgError { diff --git a/common/dkg/src/interpolation/polynomial.rs b/common/dkg/src/interpolation/polynomial.rs index 671b8e6832..f3874806e1 100644 --- a/common/dkg/src/interpolation/polynomial.rs +++ b/common/dkg/src/interpolation/polynomial.rs @@ -6,6 +6,7 @@ use crate::utils::deserialize_g2; use bls12_381::{G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::ops::{Add, Index, IndexMut}; use zeroize::Zeroize; @@ -120,7 +121,7 @@ impl Polynomial { // for polynomial of degree n, we generate n+1 values // (for example for degree 1, like y = x + 2, we need [2,1]) /// Creates new pseudorandom polynomial of specified degree. - pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self { + pub fn new_random(mut rng: impl RngCore + CryptoRng + CryptoRng, degree: u64) -> Self { Polynomial { coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(), } diff --git a/common/dkg/tests/integration.rs b/common/dkg/tests/integration.rs index 81c861425d..e34195488e 100644 --- a/common/dkg/tests/integration.rs +++ b/common/dkg/tests/integration.rs @@ -53,11 +53,12 @@ fn single_sender() { // make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check) for (i, (ref dk, _)) in full_keys.iter().enumerate() { - let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap(); + let _recovered = decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap(); } // and for good measure, check that the dealer's share matches decryption result - let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap(); + let recovered_dealer = + decrypt_share(¶ms, &full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap(); assert_eq!(recovered_dealer, dealer_share.unwrap()); } @@ -115,7 +116,7 @@ fn full_threshold_secret_sharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); // we know dealer_share matches, but it would be inconvenient to try to put them in here, @@ -189,7 +190,7 @@ fn full_threshold_secret_resharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = first_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -240,7 +241,7 @@ fn full_threshold_secret_resharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = resharing_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -305,7 +306,7 @@ fn full_threshold_secret_resharing_left_party() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = first_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -369,7 +370,7 @@ fn full_threshold_secret_resharing_left_party() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = resharing_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = combine_shares(shares, &node_indices).unwrap(); diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index d60f6adcc6..c3418649cf 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -112,6 +112,10 @@ impl ServerResponse { _ => false, } } + + pub fn is_send(&self) -> bool { + matches!(self, ServerResponse::Send { .. }) + } } impl From for Message { diff --git a/common/gateway-stats-storage/src/lib.rs b/common/gateway-stats-storage/src/lib.rs index 8ad34bbca4..d29de3a1b0 100644 --- a/common/gateway-stats-storage/src/lib.rs +++ b/common/gateway-stats-storage/src/lib.rs @@ -33,8 +33,8 @@ impl PersistentStatsStorage { /// * `database_path`: path to the database. pub async fn init + Send>(database_path: P) -> Result { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); // TODO: we can inject here more stuff based on our gateway global config diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 2d05d43fe7..ea534b8c1b 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -82,8 +82,8 @@ impl GatewayStorage { message_retrieval_limit: i64, ) -> Result { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); // TODO: we can inject here more stuff based on our gateway global config diff --git a/common/network-defaults/build.rs b/common/network-defaults/build.rs index 3ba632535d..2164bd2820 100644 --- a/common/network-defaults/build.rs +++ b/common/network-defaults/build.rs @@ -23,7 +23,6 @@ fn main() { "REWARDING_VALIDATOR_ADDRESS", "NYM_API", "NYXD_WS", - "EXPLORER_API", "NYM_VPN_API", ]; diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 2b54d17406..ab597906c2 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #[cfg(feature = "network")] -use crate::{DenomDetails, ValidatorDetails}; +use crate::{ApiUrlConst, DenomDetails, ValidatorDetails}; pub const NETWORK_NAME: &str = "mainnet"; @@ -17,6 +17,11 @@ pub const MIXNET_CONTRACT_ADDRESS: &str = "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr"; pub const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; + +// \/ TODO: this has to be updated once the contract is deployed +pub const PERFORMANCE_CONTRACT_ADDRESS: &str = ""; +// /\ TODO: this has to be updated once the contract is deployed + pub const ECASH_CONTRACT_ADDRESS: &str = "n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun"; pub const GROUP_CONTRACT_ADDRESS: &str = @@ -29,10 +34,37 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; pub const NYXD_URL: &str = "https://rpc.nymtech.net"; -pub const NYM_API: &str = "https://validator.nymtech.net/api/"; pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket"; -pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/"; + +pub const NYM_API: &str = "https://validator.nymtech.net/api/"; +#[cfg(feature = "network")] +pub const NYM_APIS: &[ApiUrlConst] = &[ + ApiUrlConst { + url: NYM_API, + front_hosts: None, + }, + ApiUrlConst { + url: "https://nym-fronntdoor.vercel.app/api/", + front_hosts: Some(&["vercel.app", "vercel.com"]), + }, + ApiUrlConst { + url: "https://nym-frontdoor.global.ssl.fastly.net/api/", + front_hosts: Some(&["yelp.global.ssl.fastly.net"]), + }, +]; + pub const NYM_VPN_API: &str = "https://nymvpn.com/api/"; +#[cfg(feature = "network")] +pub const NYM_VPN_APIS: &[ApiUrlConst] = &[ + ApiUrlConst { + url: NYM_VPN_API, + front_hosts: Some(&["vercel.app", "vercel.com"]), + }, + ApiUrlConst { + url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/", + front_hosts: Some(&["yelp.global.ssl.fastly.net"]), + }, +]; // I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging pub const EXIT_POLICY_URL: &str = @@ -123,7 +155,6 @@ pub fn export_to_env() { set_var_to_default(var_names::NYXD, NYXD_URL); set_var_to_default(var_names::NYM_API, NYM_API); set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); - set_var_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API); } @@ -165,6 +196,5 @@ pub fn export_to_env_if_not_set() { set_var_conditionally_to_default(var_names::NYXD, NYXD_URL); set_var_conditionally_to_default(var_names::NYM_API, NYM_API); set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); - set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); } diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 8e7b42543f..e8fd8fcfa8 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -20,6 +20,8 @@ pub struct ChainDetails { pub struct NymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, + #[serde(default)] + pub performance_contract_address: Option, pub ecash_contract_address: Option, pub group_contract_address: Option, pub multisig_contract_address: Option, @@ -35,8 +37,38 @@ pub struct NymNetworkDetails { pub chain_details: ChainDetails, pub endpoints: Vec, pub contracts: NymContracts, - pub explorer_api: Option, pub nym_vpn_api_url: Option, + pub nym_api_urls: Option>, + pub nym_vpn_api_urls: Option>, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub struct ApiUrl { + /// Expects a string formatted Url + /// + /// see https://docs.rs/url/latest/url/struct.Url.html + pub url: String, + /// Optional alternative equivalent hostnames. Each entry must parse as valid Host + /// + /// see https://docs.rs/url/latest/url/enum.Host.html + pub front_hosts: Option>, +} + +pub struct ApiUrlConst<'a> { + pub url: &'a str, + pub front_hosts: Option<&'a [&'a str]>, +} + +impl From> for ApiUrl { + fn from(value: ApiUrlConst) -> Self { + ApiUrl { + url: value.url.to_string(), + front_hosts: value + .front_hosts + .map(|slice| slice.iter().map(|s| s.to_string()).collect()), + } + } } // by default we assume the same defaults as mainnet, i.e. same prefixes and denoms @@ -65,8 +97,9 @@ impl NymNetworkDetails { }, endpoints: Default::default(), contracts: Default::default(), - explorer_api: Default::default(), nym_vpn_api_url: Default::default(), + nym_api_urls: Default::default(), + nym_vpn_api_urls: Default::default(), } } @@ -124,7 +157,6 @@ impl NymNetworkDetails { .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS)) .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS)) .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS)) - .with_explorer_api(get_optional_env(var_names::EXPLORER_API)) .with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API)) } @@ -145,6 +177,9 @@ impl NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), + performance_contract_address: parse_optional_str( + mainnet::PERFORMANCE_CONTRACT_ADDRESS, + ), ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), @@ -152,8 +187,9 @@ impl NymNetworkDetails { mainnet::COCONUT_DKG_CONTRACT_ADDRESS, ), }, - explorer_api: parse_optional_str(mainnet::EXPLORER_API), nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API), + nym_api_urls: None, + nym_vpn_api_urls: None, } } @@ -193,7 +229,6 @@ impl NymNetworkDetails { set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address); set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address); - set_optional_var(var_names::EXPLORER_API, self.explorer_api); set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url); } @@ -297,12 +332,6 @@ impl NymNetworkDetails { self } - #[must_use] - pub fn with_explorer_api>(mut self, endpoint: Option) -> Self { - self.explorer_api = endpoint.map(Into::into); - self - } - #[must_use] pub fn with_nym_vpn_api_url>(mut self, endpoint: Option) -> Self { self.nym_vpn_api_url = endpoint.map(Into::into); diff --git a/common/network-defaults/src/sandbox.rs b/common/network-defaults/src/sandbox.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index bf92b95799..5bcebe85eb 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -22,7 +22,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS"; pub const NYXD: &str = "NYXD"; pub const NYM_API: &str = "NYM_API"; pub const NYXD_WEBSOCKET: &str = "NYXD_WS"; -pub const EXPLORER_API: &str = "EXPLORER_API"; pub const EXIT_POLICY_URL: &str = "EXIT_POLICY"; pub const NYM_VPN_API: &str = "NYM_VPN_API"; pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER"; diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 7926c976ed..6f3dd9137e 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -56,6 +56,13 @@ impl ReplySurb { packet_size.plaintext_size() - ack_overhead - ReplySurbKeyDigestAlgorithm::output_size() - 1 } + /// Construct a ResplySurb object. Selects mix hops for the surb unique to this + /// individual construction. + /// + /// If mix hops are disabled, the route will consistency of the recipient + /// (i.e. the ingress hop) only. When `disable_mix_hops` is enabled + /// `use_legacy_surb_format` is ignored as disabled mix hops requires use of + /// the updated SURB format. // TODO: should this return `ReplySURBError` for consistency sake // or keep `NymTopologyError` because it's the only error it can actually return? pub fn construct( @@ -64,17 +71,21 @@ impl ReplySurb { average_delay: Duration, use_legacy_surb_format: bool, topology: &NymRouteProvider, - _disable_mix_hops: bool, // TODO: support SURBs with no mix hops after changes to surb format / construction + disable_mix_hops: bool, ) -> Result where R: RngCore + CryptoRng, { - let route = topology.random_route_to_egress(rng, recipient.gateway())?; + let route = if disable_mix_hops { + topology.empty_route_to_egress(recipient.gateway())? + } else { + topology.random_route_to_egress(rng, recipient.gateway())? + }; let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); let mut surb_material = SURBMaterial::new(route, delays, destination); - if use_legacy_surb_format { + if use_legacy_surb_format && !disable_mix_hops { surb_material = surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION) } diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 9466ebe716..038f1c4b7a 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -335,6 +335,9 @@ pub struct MessagePreparer { /// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced/disabled mix hops + /// requires use of the updated SURB packet format. pub disable_mix_hops: bool, } @@ -388,7 +391,7 @@ where self.average_packet_delay, use_legacy_reply_surb_format, topology, - disabled_mix_hops, // TODO: support SURBs with no mix hops after changes to surb format / construction + disabled_mix_hops, )? .with_key_rotation(key_rotation); reply_surbs.push(reply_surb) diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index c7eb706180..96362c852c 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -201,6 +201,9 @@ pub struct TrafficWasm { /// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced/disabeld mix hops + /// requires use of the updated SURB packet format. pub disable_mix_hops: bool, } diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 404d5b287e..a434eb8778 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "ark-bls12-381" @@ -1133,6 +1133,19 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-contracts-common-testing" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus", + "rand", + "rand_chacha", + "serde", +] + [[package]] name = "nym-crypto" version = "0.4.0" @@ -1214,7 +1227,9 @@ dependencies = [ "cw2", "easy-addr", "nym-contracts-common", + "nym-contracts-common-testing", "nym-crypto", + "nym-mixnet-contract", "nym-mixnet-contract-common", "nym-vesting-contract-common", "rand", @@ -1238,7 +1253,6 @@ dependencies = [ "schemars", "semver", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -1276,6 +1290,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-performance-contract" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw2", + "nym-contracts-common", + "nym-contracts-common-testing", + "nym-crypto", + "nym-mixnet-contract", + "nym-mixnet-contract-common", + "nym-performance-contract-common", + "serde", +] + +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars", + "serde", + "thiserror 2.0.12", +] + [[package]] name = "nym-pool-contract" version = "0.1.0" @@ -1284,14 +1330,11 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", - "cw-multi-test", "cw-storage-plus", "cw2", "nym-contracts-common", + "nym-contracts-common-testing", "nym-pool-contract-common", - "rand", - "rand_chacha", - "serde", ] [[package]] diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index c8cee21dfa..ec3239772c 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -9,6 +9,7 @@ members = [ "multisig/cw3-flex-multisig", "multisig/cw4-group", "vesting", + "performance", ] [workspace.package] @@ -64,4 +65,4 @@ dbg_macro = "deny" exit = "deny" panic = "deny" unimplemented = "deny" -unreachable = "deny" \ No newline at end of file +unreachable = "deny" diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 9fa22fe0db..ae2ad99d36 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -26,13 +26,13 @@ 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.6.0" } -vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.7.0" } -nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" } +mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" } +vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" } +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true } cosmwasm-schema = { workspace = true, optional = true } cosmwasm-std = { workspace = true } - cw-controllers = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } @@ -41,16 +41,22 @@ bs58 = { workspace = true } serde = { workspace = true, default-features = false, features = ["derive"] } semver = { workspace = true } + [dev-dependencies] anyhow.workspace = true -rand_chacha = "0.3" -rand = "0.8.5" +rand_chacha = { workspace = true } +rand = { workspace = true } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" } +# activate the `testable-mixnet-contract` in tests (weird workaround, but it does the trick) +nym-mixnet-contract = { path = ".", features = ["testable-mixnet-contract"] } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } + [features] default = [] contract-testing = ["mixnet-contract-common/contract-testing"] +testable-mixnet-contract = ["nym-contracts-common-testing"] schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"] [lints] diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 522017c762..0703e6dc23 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -649,7 +649,7 @@ pub fn migrate( mod tests { use super::*; use crate::rewards::storage as rewards_storage; - use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + use cosmwasm_std::testing::{message_info, mock_env}; use cosmwasm_std::{Decimal, Uint128}; use mixnet_contract_common::reward_params::{ IntervalRewardParams, RewardedSetParams, RewardingParams, @@ -657,6 +657,7 @@ mod tests { use mixnet_contract_common::{ InitialRewardingParams, OperatingCostRange, Percent, ProfitMarginRange, }; + use nym_contracts_common_testing::mock_dependencies; use std::time::Duration; #[test] diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 8e1450421d..559f6e7ee9 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -22,3 +22,6 @@ mod support; #[cfg(feature = "contract-testing")] mod testing; mod vesting_migration; + +#[cfg(feature = "testable-mixnet-contract")] +pub mod testable_mixnet_contract; diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 0175e4a279..8d1202df71 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -130,20 +130,22 @@ pub mod tests { use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::support::tests::test_helpers; - use cosmwasm_std::testing::{message_info, MockApi}; + use cosmwasm_std::testing::message_info; use cosmwasm_std::{Coin, Uint128}; use cw_controllers::AdminError::NotAdmin; use mixnet_contract_common::OperatorsParamsUpdate; + use nym_contracts_common_testing::mock_api; #[test] fn update_contract_rewarding_validator_address() { let mut deps = test_helpers::init_contract(); + let mock_api = mock_api(); let info = message_info(&deps.api.addr_make("not-the-creator"), &[]); let res = try_update_rewarding_validator_address( deps.as_mut(), info, - MockApi::default().addr_make("not-the-creator").to_string(), + mock_api.addr_make("not-the-creator").to_string(), ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); @@ -151,14 +153,14 @@ pub mod tests { let res = try_update_rewarding_validator_address( deps.as_mut(), info, - MockApi::default().addr_make("new-good-address").to_string(), + mock_api.addr_make("new-good-address").to_string(), ); assert_eq!( res, Ok( Response::default().add_event(new_rewarding_validator_address_update_event( - MockApi::default().addr_make("rewarder"), - MockApi::default().addr_make("new-good-address") + mock_api.addr_make("rewarder"), + mock_api.addr_make("new-good-address") )) ) ); @@ -166,7 +168,7 @@ pub mod tests { let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap(); assert_eq!( state.rewarding_validator_address, - MockApi::default().addr_make("new-good-address") + mock_api.addr_make("new-good-address") ); assert_eq!( diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 1418f0d4cf..29aafaa92e 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -51,11 +51,11 @@ pub mod test_helpers { use crate::support::helpers::ensure_no_existing_bond; use crate::support::tests; use crate::support::tests::fixtures::{ - good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM, + good_gateway_pledge, good_mixnode_pledge, good_node_plegge, }; use crate::support::tests::{legacy, test_helpers}; + use crate::testable_mixnet_contract::MixnetContract; use cosmwasm_std::testing::message_info; - use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; @@ -74,22 +74,24 @@ pub mod test_helpers { use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; use mixnet_contract_common::reward_params::{ - NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor, + NodeRewardingParameters, Performance, RewardingParams, WorkFactor, }; use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode; use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ ContractStateParamsUpdate, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, - Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg, - Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, - NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange, - OperatorsParamsUpdate, Percent, ProfitMarginRange, RoleAssignment, - SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg, + Gateway, GatewayBondingPayload, IdentityKey, Interval, MixNode, MixNodeBond, + MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, NymNodeBondingPayload, + NymNodeDetails, OperatingCostRange, OperatorsParamsUpdate, ProfitMarginRange, + RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, }; + use nym_contracts_common_testing::TestableNymContract; + use nym_contracts_common_testing::{mock_api, mock_dependencies}; use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::ed25519::KeyPair; use rand::distributions::WeightedIndex; @@ -100,13 +102,12 @@ pub mod test_helpers { use std::collections::HashMap; use std::fmt::Debug; use std::str::FromStr; - use std::time::Duration; pub(crate) fn sorted_addresses(n: usize) -> Vec { let mut rng = test_rng(); let mut addrs = Vec::with_capacity(n); for i in 0..n { - addrs.push(MockApi::default().addr_make(&format!("addr{i}{}", rng.next_u64()))); + addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64()))); } addrs.sort(); addrs @@ -1820,46 +1821,9 @@ pub mod test_helpers { SignableGatewayBondingMsg::new(nonce, content) } - fn intial_rewarded_set_params() -> RewardedSetParams { - RewardedSetParams { - entry_gateways: 50, - exit_gateways: 70, - mixnodes: 120, - standby: 50, - } - } - - fn initial_rewarding_params() -> InitialRewardingParams { - let reward_pool = 250_000_000_000_000u128; - let staking_supply = 100_000_000_000_000u128; - - InitialRewardingParams { - initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) - initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M - staking_supply_scale_factor: Percent::hundred(), - sybil_resistance: Percent::from_percentage_value(30).unwrap(), - active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), - interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_params: intial_rewarded_set_params(), - } - } - pub fn init_contract() -> OwnedDeps> { let mut deps = mock_dependencies(); - let msg = InstantiateMsg { - rewarding_validator_address: deps.api.addr_make("rewarder").to_string(), - vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(), - rewarding_denom: TEST_COIN_DENOM.to_string(), - epochs_in_interval: 720, - epoch_duration: Duration::from_secs(60 * 60), - initial_rewarding_params: initial_rewarding_params(), - current_nym_node_version: "1.1.10".to_string(), - version_score_weights: Default::default(), - version_score_params: Default::default(), - profit_margin: Default::default(), - interval_operating_cost: Default::default(), - key_validity_in_epochs: None, - }; + let msg = MixnetContract::base_init_msg(); let env = mock_env(); let info = sender("creator"); instantiate(deps.as_mut(), env, info, msg).unwrap(); diff --git a/contracts/mixnet/src/testable_mixnet_contract.rs b/contracts/mixnet/src/testable_mixnet_contract.rs new file mode 100644 index 0000000000..498e121222 --- /dev/null +++ b/contracts/mixnet/src/testable_mixnet_contract.rs @@ -0,0 +1,89 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// fine in test code +#![allow(clippy::unwrap_used)] + +use crate::contract::{execute, instantiate, migrate, query}; +use cosmwasm_std::Decimal; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::reward_params::RewardedSetParams; +use mixnet_contract_common::{ + ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg, +}; +use nym_contracts_common::Percent; +use nym_contracts_common_testing::{ + mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM, +}; +use std::time::Duration; + +pub use nym_contracts_common_testing::TestableNymContract; + +pub struct MixnetContract; + +fn initial_rewarded_set_params() -> RewardedSetParams { + RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 50, + } +} + +fn initial_rewarding_params() -> InitialRewardingParams { + let reward_pool = 250_000_000_000_000u128; + let staking_supply = 100_000_000_000_000u128; + + InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) + initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + staking_supply_scale_factor: Percent::hundred(), + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(2).unwrap(), + rewarded_set_params: initial_rewarded_set_params(), + } +} + +impl TestableNymContract for MixnetContract { + const NAME: &'static str = "mixnet-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = MixnetContractError; + + fn instantiate() -> ContractFn { + instantiate + } + + fn execute() -> ContractFn { + execute + } + + fn query() -> QueryFn { + query + } + + fn migrate() -> PermissionedFn { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + let deps = mock_dependencies(); + InstantiateMsg { + rewarding_validator_address: deps.api.addr_make("rewarder").to_string(), + vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(), + rewarding_denom: TEST_DENOM.to_string(), + epochs_in_interval: 720, + epoch_duration: Duration::from_secs(60 * 60), + initial_rewarding_params: initial_rewarding_params(), + current_nym_node_version: "1.1.10".to_string(), + version_score_weights: Default::default(), + version_score_params: Default::default(), + profit_margin: Default::default(), + interval_operating_cost: Default::default(), + key_validity_in_epochs: None, + } + } +} diff --git a/contracts/nym-pool/Cargo.toml b/contracts/nym-pool/Cargo.toml index 4011deaa30..5bd54bb3bc 100644 --- a/contracts/nym-pool/Cargo.toml +++ b/contracts/nym-pool/Cargo.toml @@ -25,13 +25,9 @@ cosmwasm-schema = { workspace = true, optional = true } nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } nym-pool-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-pool-contract" } - [dev-dependencies] anyhow = { workspace = true } -serde = { workspace = true } -rand_chacha = { workspace = true } -rand = { workspace = true } -cw-multi-test = { workspace = true } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } [features] schema-gen = ["nym-pool-contract-common/schema", "cosmwasm-schema"] diff --git a/contracts/nym-pool/src/contract.rs b/contracts/nym-pool/src/contract.rs index 9978d39fe6..476aaad5f7 100644 --- a/contracts/nym-pool/src/contract.rs +++ b/contracts/nym-pool/src/contract.rs @@ -193,8 +193,8 @@ mod tests { #[cfg(test)] mod setting_initial_grants { use super::*; - use crate::testing::deps_with_balance; use cosmwasm_std::{coin, Order, Storage}; + use nym_contracts_common_testing::deps_with_balance; use nym_pool_contract_common::{Allowance, BasicAllowance, Grant, GranteeAddress}; use std::collections::HashMap; diff --git a/contracts/nym-pool/src/helpers.rs b/contracts/nym-pool/src/helpers.rs index e71b9f1148..129c9089e6 100644 --- a/contracts/nym-pool/src/helpers.rs +++ b/contracts/nym-pool/src/helpers.rs @@ -26,12 +26,13 @@ pub fn validate_usage_coin(storage: &dyn Storage, coin: &Coin) -> Result<(), Nym mod tests { use super::*; use crate::storage::NymPoolStorage; - use crate::testing::TestSetup; + use crate::testing::init_contract_tester; use cosmwasm_std::coin; + use nym_contracts_common_testing::ContractOpts; #[test] fn validating_coin_usage() -> anyhow::Result<()> { - let test = TestSetup::init(); + let test = init_contract_tester(); let storage = NymPoolStorage::new(); let denom = storage.pool_denomination.load(test.storage())?; diff --git a/contracts/nym-pool/src/queries.rs b/contracts/nym-pool/src/queries.rs index 997d2c5d67..2d8dcc57a1 100644 --- a/contracts/nym-pool/src/queries.rs +++ b/contracts/nym-pool/src/queries.rs @@ -182,20 +182,22 @@ pub fn query_granters_paged( mod tests { use super::*; use crate::contract::instantiate; - use crate::testing::{TestSetup, TEST_DENOM}; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM}; use cosmwasm_std::testing::{message_info, mock_dependencies_with_balance, mock_env}; use cosmwasm_std::{coin, Uint128}; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt}; use nym_pool_contract_common::{Allowance, BasicAllowance, GranterInformation, InstantiateMsg}; #[cfg(test)] mod admin_query { use super::*; - use crate::testing::TestSetup; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt}; use nym_pool_contract_common::ExecuteMsg; #[test] fn returns_current_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let initial_admin = test.admin_unchecked(); @@ -255,7 +257,7 @@ mod tests { #[test] fn total_locked_tokens_query() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let locked = query_total_locked_tokens(test.deps()).unwrap().locked; assert!(locked.amount.is_zero()); @@ -271,7 +273,7 @@ mod tests { #[test] fn locked_tokens_query() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee1 = test.add_dummy_grant().grantee; test.lock_allowance(grantee1.as_str(), Uint128::new(1234)); @@ -295,8 +297,13 @@ mod tests { #[cfg(test)] mod locked_tokens_paged_query { use super::*; + use crate::testing::NymPoolContract; + use nym_contracts_common_testing::ContractTester; - fn lock_sorted(test: &mut TestSetup, count: usize) -> Vec { + fn lock_sorted( + test: &mut ContractTester, + count: usize, + ) -> Vec { let mut grantees = Vec::new(); for _ in 0..count { @@ -314,7 +321,7 @@ mod tests { #[test] fn obeys_limits() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _locked = lock_sorted(&mut test, 1000); let limit = 42; @@ -324,7 +331,7 @@ mod tests { #[test] fn has_default_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _locked = lock_sorted(&mut test, 1000); // query without explicitly setting a limit @@ -337,7 +344,7 @@ mod tests { #[test] fn has_max_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _locked = lock_sorted(&mut test, 1000); // query with a crazily high limit in an attempt to use too many resources @@ -352,7 +359,7 @@ mod tests { #[test] fn pagination_works() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let locked = lock_sorted(&mut test, 1000); // first page should return 2 results... @@ -371,7 +378,7 @@ mod tests { #[test] fn grant_query() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); // bad address @@ -433,7 +440,7 @@ mod tests { #[test] fn granter_query() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let admin = test.admin_unchecked(); let env = test.env(); @@ -482,8 +489,13 @@ mod tests { #[cfg(test)] mod granters_paged_query { use super::*; + use crate::testing::NymPoolContract; + use nym_contracts_common_testing::ContractTester; - fn granters_sorted(test: &mut TestSetup, count: usize) -> Vec { + fn granters_sorted( + test: &mut ContractTester, + count: usize, + ) -> Vec { let mut granters = Vec::new(); for _ in 0..count { @@ -504,7 +516,7 @@ mod tests { #[test] fn obeys_limits() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _granters = granters_sorted(&mut test, 1000); let limit = 42; @@ -514,7 +526,7 @@ mod tests { #[test] fn has_default_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _granters = granters_sorted(&mut test, 1000); // query without explicitly setting a limit @@ -527,7 +539,7 @@ mod tests { #[test] fn has_max_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _granters = granters_sorted(&mut test, 1000); // query with a crazily high limit in an attempt to use too many resources @@ -542,7 +554,7 @@ mod tests { #[test] fn pagination_works() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let locked = granters_sorted(&mut test, 1000); // first page should return 2 results... @@ -562,8 +574,13 @@ mod tests { #[cfg(test)] mod grants_paged_query { use super::*; + use crate::testing::{init_contract_tester, NymPoolContract}; + use nym_contracts_common_testing::{ContractOpts, ContractTester}; - fn grants_sorted(test: &mut TestSetup, count: usize) -> Vec { + fn grants_sorted( + test: &mut ContractTester, + count: usize, + ) -> Vec { let mut grantees = Vec::new(); for _ in 0..count { @@ -580,7 +597,7 @@ mod tests { #[test] fn obeys_limits() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _grantees = grants_sorted(&mut test, 1000); let limit = 42; @@ -590,7 +607,7 @@ mod tests { #[test] fn has_default_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _grantees = grants_sorted(&mut test, 1000); // query without explicitly setting a limit @@ -603,7 +620,7 @@ mod tests { #[test] fn has_max_limit() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let _grantees = grants_sorted(&mut test, 1000); // query with a crazily high limit in an attempt to use too many resources @@ -619,7 +636,7 @@ mod tests { #[test] fn pagination_works() { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grants = grants_sorted(&mut test, 1000); // first page should return 2 results... diff --git a/contracts/nym-pool/src/storage.rs b/contracts/nym-pool/src/storage.rs index 03d55ef941..9d01d5796f 100644 --- a/contracts/nym-pool/src/storage.rs +++ b/contracts/nym-pool/src/storage.rs @@ -489,19 +489,21 @@ mod tests { #[cfg(test)] mod nympool_storage { use super::*; - use crate::testing::{TestSetup, TEST_DENOM}; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM}; use cosmwasm_std::testing::{ mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage, }; use cosmwasm_std::{coin, coins, Empty, OwnedDeps}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; use nym_pool_contract_common::BasicAllowance; #[cfg(test)] mod initialisation { use super::*; - use crate::testing::{deps_with_balance, TEST_DENOM}; + use crate::testing::TEST_DENOM; use cosmwasm_std::testing::{mock_dependencies, mock_env}; use cosmwasm_std::{coin, Order}; + use nym_contracts_common_testing::deps_with_balance; use nym_pool_contract_common::BasicAllowance; fn all_grants(storage: &dyn Storage) -> HashMap { @@ -914,7 +916,7 @@ mod tests { #[test] fn loading_granter_information() -> anyhow::Result<()> { let storage = NymPoolStorage::new(); - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let granter = test.generate_account(); @@ -941,7 +943,7 @@ mod tests { #[test] fn checking_granter_permission() -> anyhow::Result<()> { let storage = NymPoolStorage::new(); - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let granter = test.generate_account(); test.add_granter(&granter); @@ -957,7 +959,7 @@ mod tests { #[test] fn ensuring_granter_permission() -> anyhow::Result<()> { let storage = NymPoolStorage::new(); - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let granter = test.generate_account(); test.add_granter(&granter); @@ -1047,7 +1049,7 @@ mod tests { #[test] fn attempting_to_load_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); // doesn't exist... @@ -1070,7 +1072,7 @@ mod tests { #[test] fn loading_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); // doesn't exist... @@ -1094,11 +1096,12 @@ mod tests { #[cfg(test)] mod adding_new_granter { use super::*; + use crate::testing::init_contract_tester; use cw_controllers::AdminError; #[test] fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1121,7 +1124,7 @@ mod tests { #[test] fn can_only_be_performed_if_account_is_not_already_a_granter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1143,7 +1146,7 @@ mod tests { #[test] fn saves_basic_metadata() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1194,10 +1197,11 @@ mod tests { #[cfg(test)] mod removing_granter { use super::*; + use crate::testing::init_contract_tester; #[test] fn requires_granter_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1217,7 +1221,7 @@ mod tests { #[test] fn can_only_be_performed_by_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let random_address = test.generate_account(); @@ -1259,7 +1263,7 @@ mod tests { #[test] fn removes_it_from_granter_list() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1284,11 +1288,12 @@ mod tests { #[cfg(test)] mod adding_new_grant { use super::*; + use crate::testing::init_contract_tester; use nym_pool_contract_common::ClassicPeriodicAllowance; #[test] fn can_only_be_done_by_whitelisted_granter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let not_valid_granter = test.generate_account(); @@ -1319,7 +1324,7 @@ mod tests { #[test] fn cant_be_done_if_grant_already_existed() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1340,7 +1345,7 @@ mod tests { #[test] fn only_accepts_valid_allowances() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); // allowance with 0 limit and wrong denom @@ -1364,7 +1369,7 @@ mod tests { #[test] fn explicit_limit_cant_be_larger_than_available_tokens() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1401,7 +1406,7 @@ mod tests { assert!(res.is_ok()); // and below the available - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let mut limit = available.clone(); limit.amount -= Uint128::new(1); let allowance = Allowance::Basic(BasicAllowance { @@ -1418,7 +1423,7 @@ mod tests { #[test] fn updates_allowances_initial_state_and_saves_it_to_storage() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1456,10 +1461,11 @@ mod tests { #[cfg(test)] mod spending_part_of_grant { use super::*; + use crate::testing::init_contract_tester; #[test] fn requires_grant_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.generate_account(); @@ -1485,7 +1491,7 @@ mod tests { #[test] fn requires_grant_to_be_spendable() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1514,7 +1520,7 @@ mod tests { #[test] fn updates_stored_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1548,7 +1554,7 @@ mod tests { #[test] fn removes_grant_from_storage_if_its_used_up() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1612,7 +1618,7 @@ mod tests { #[test] fn removing_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.generate_account(); @@ -1656,10 +1662,11 @@ mod tests { #[cfg(test)] mod revoking_grant { use super::*; + use crate::testing::init_contract_tester; #[test] fn requires_grant_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1684,7 +1691,7 @@ mod tests { #[test] fn can_always_be_called_by_current_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.add_dummy_grant().grantee; @@ -1717,7 +1724,7 @@ mod tests { #[test] fn can_be_called_by_original_granter_if_its_still_whitelisted() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1761,7 +1768,7 @@ mod tests { #[test] fn removes_the_underlying_grant() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); @@ -1780,10 +1787,12 @@ mod tests { #[cfg(test)] mod locking_part_of_allowance { use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::DenomExt; #[test] fn requires_providing_valid_coin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.add_dummy_grant().grantee; @@ -1804,7 +1813,7 @@ mod tests { #[test] fn requires_grant_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.generate_account(); let env = test.env(); @@ -1826,7 +1835,7 @@ mod tests { #[test] fn does_not_allow_locking_more_than_spend_limit() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); let env = test.env(); @@ -1853,7 +1862,7 @@ mod tests { #[test] fn deducts_locked_amount_from_the_allowance() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); let env = test.env(); @@ -1892,7 +1901,7 @@ mod tests { #[test] fn preserves_grant_even_if_resultant_allowance_is_zero() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); let env = test.env(); @@ -1918,7 +1927,7 @@ mod tests { #[test] fn updates_internal_locked_counter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let env = test.env(); let grantee = test.add_dummy_grant().grantee; @@ -1953,8 +1962,10 @@ mod tests { #[cfg(test)] mod unlocking_part_of_allowance { use super::*; + use crate::testing::{init_contract_tester, NymPoolContract}; + use nym_contracts_common_testing::{ContractTester, DenomExt}; - fn setup_locked_grant(test: &mut TestSetup) -> Addr { + fn setup_locked_grant(test: &mut ContractTester) -> Addr { let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(100)); grantee @@ -1962,7 +1973,7 @@ mod tests { #[test] fn requires_providing_valid_coin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = setup_locked_grant(&mut test); @@ -1981,7 +1992,7 @@ mod tests { #[test] fn does_not_allow_unlocking_more_than_currently_locked() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = setup_locked_grant(&mut test); @@ -1999,7 +2010,7 @@ mod tests { #[test] fn requires_grant_to_exist() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.generate_account(); @@ -2018,7 +2029,7 @@ mod tests { #[test] fn requires_having_locked_coins() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let grantee = test.add_dummy_grant().grantee; @@ -2036,7 +2047,7 @@ mod tests { #[test] fn increases_internal_grant_spend_limit() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); let admin = test.admin_unchecked(); let env = test.env(); @@ -2082,7 +2093,7 @@ mod tests { #[test] fn updates_internal_locked_counter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = NymPoolStorage::new(); // 100tokens locked @@ -2116,8 +2127,9 @@ mod tests { #[cfg(test)] mod locked_storage { use super::*; - use crate::testing::TestSetup; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; use cosmwasm_std::testing::mock_dependencies; + use nym_contracts_common_testing::{ContractOpts, RandExt}; #[test] fn is_initialised_with_zero_total_locked() -> anyhow::Result<()> { @@ -2136,7 +2148,7 @@ mod tests { #[test] fn getting_grantee_locked() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.generate_account(); let storage = LockedStorage::new(); @@ -2167,7 +2179,7 @@ mod tests { #[test] fn getting_maybe_grantee_locked() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.generate_account(); let storage = LockedStorage::new(); @@ -2198,7 +2210,7 @@ mod tests { #[test] fn locking_tokens() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = LockedStorage::new(); let grantee1 = test.generate_account(); @@ -2259,7 +2271,7 @@ mod tests { #[test] fn unlocking_tokens() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let storage = LockedStorage::new(); let grantee1 = test.generate_account(); diff --git a/contracts/nym-pool/src/testing/mod.rs b/contracts/nym-pool/src/testing/mod.rs index 701dd953de..640b8eccd0 100644 --- a/contracts/nym-pool/src/testing/mod.rs +++ b/contracts/nym-pool/src/testing/mod.rs @@ -1,226 +1,70 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::contract; use crate::contract::{execute, instantiate, migrate, query}; use crate::storage::NYM_POOL_STORAGE; -use crate::testing::storage::{ContractStorageWrapper, StorageWrapper}; -use cosmwasm_std::testing::{message_info, mock_env, MockApi, MockQuerier, MockStorage}; -use cosmwasm_std::{ - coin, coins, Addr, Coin, ContractInfo, Deps, DepsMut, Empty, Env, MemoryStorage, MessageInfo, - Order, OwnedDeps, Response, StdResult, Storage, Uint128, -}; -use cw_multi_test::{ - next_block, App, AppBuilder, AppResponse, BankKeeper, Contract, ContractWrapper, Executor, +use cosmwasm_std::{Addr, Order, Uint128}; +use nym_contracts_common_testing::{ + AdminExt, ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractTester, DenomExt, + PermissionedFn, QueryFn, RandExt, TestableNymContract, }; +use nym_pool_contract_common::constants::storage_keys; use nym_pool_contract_common::{ - Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, NymPoolContractError, QueryMsg, + Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, MigrateMsg, NymPoolContractError, + QueryMsg, }; -use rand::{RngCore, SeedableRng}; -use rand_chacha::ChaCha20Rng; -use serde::de::DeserializeOwned; use std::collections::HashMap; -mod storage; +pub use nym_contracts_common_testing::TEST_DENOM; -pub fn test_rng() -> ChaCha20Rng { - let dummy_seed = [42u8; 32]; - ChaCha20Rng::from_seed(dummy_seed) -} +pub struct NymPoolContract; -pub fn deps_with_balance(env: &Env) -> OwnedDeps> { - OwnedDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[( - env.contract.address.as_str(), - coins(100000000000, TEST_DENOM).as_slice(), - )]), - custom_query_type: Default::default(), +impl TestableNymContract for NymPoolContract { + const NAME: &'static str = "nym-pool-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = NymPoolContractError; + + fn instantiate() -> ContractFn { + instantiate + } + + fn execute() -> ContractFn { + execute + } + + fn query() -> QueryFn { + query + } + + fn migrate() -> PermissionedFn { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants: Default::default(), + } } } -pub const TEST_DENOM: &str = "unym"; - -pub struct TestSetup { - pub app: App, - pub rng: ChaCha20Rng, - pub contract_address: Addr, - pub master_address: Addr, - pub(crate) storage: ContractStorageWrapper, +pub fn init_contract_tester() -> ContractTester { + NymPoolContract::init() + .with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN) + .with_common_storage_key(CommonStorageKeys::Denom, storage_keys::POOL_DENOMINATION) } -pub fn contract() -> Box> { - let contract = ContractWrapper::new(execute, instantiate, query).with_migrate(migrate); - Box::new(contract) -} - -impl TestSetup { - pub fn init() -> TestSetup { - let storage = StorageWrapper::new(); - - let api = MockApi::default().with_prefix("n"); - let master_address = api.addr_make("master-owner"); - - let mut app = AppBuilder::new() - .with_api(api) - .with_storage(storage.clone()) - .build(|router, _api, storage| { - router - .bank - .init_balance( - storage, - &master_address, - coins(1000000000000000, TEST_DENOM), - ) - .unwrap() - }); - let code_id = app.store_code(contract()); - let contract_address = app - .instantiate_contract( - code_id, - master_address.clone(), - &InstantiateMsg { - pool_denomination: TEST_DENOM.to_string(), - grants: Default::default(), - }, - &[], - "nym-pool-contract", - Some(master_address.to_string()), - ) - .unwrap(); - - // send some tokens to the contract - app.send_tokens( - master_address.clone(), - contract_address.clone(), - &[coin(100000000, TEST_DENOM)], - ) - .unwrap(); - - TestSetup { - app, - rng: test_rng(), - storage: storage.contract_storage_wrapper(&contract_address), - contract_address, - master_address, - } - } - - pub fn set_contract_balance(&mut self, balance: Coin) { - let contract_address = &self.contract_address; - self.app - .router() - .bank - .init_balance( - &mut self.storage.inner_storage(), - contract_address, - vec![balance], - ) - .unwrap(); - } - - pub fn deps(&self) -> Deps<'_> { - Deps { - storage: &self.storage, - api: self.app.api(), - querier: self.app.wrap(), - } - } - - pub fn deps_mut(&mut self) -> DepsMut<'_> { - DepsMut { - storage: &mut self.storage, - api: self.app.api(), - querier: self.app.wrap(), - } - } - - pub fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) { - let env = self.env().clone(); - (self.deps_mut(), env) - } - - pub fn storage(&self) -> &dyn Storage { - &self.storage - } - - pub fn storage_mut(&mut self) -> &mut dyn Storage { - &mut self.storage - } - - pub fn env(&self) -> Env { - Env { - block: self.app.block_info(), - contract: ContractInfo { - address: self.contract_address.clone(), - }, - ..mock_env() - } - } - - pub fn next_block(&mut self) { - self.app.update_block(next_block) - } - - pub fn execute_raw( - &mut self, - sender: Addr, - message: ExecuteMsg, - ) -> Result { - self.execute_raw_with_balance(sender, &[], message) - } - - pub fn execute_raw_with_balance( - &mut self, - sender: Addr, - coins: &[Coin], - message: ExecuteMsg, - ) -> Result { - let env = self.env(); - let info = message_info(&sender, coins); - contract::execute(self.deps_mut(), env, info, message) - } - - pub fn execute_msg( - &mut self, - sender: Addr, - message: &ExecuteMsg, - ) -> anyhow::Result { - self.execute_msg_with_balance(sender, &[], message) - } - - pub fn execute_msg_with_balance( - &mut self, - sender: Addr, - coins: &[Coin], - message: &ExecuteMsg, - ) -> anyhow::Result { - self.app - .execute_contract(sender, self.contract_address.clone(), message, coins) - } - - pub fn query(&self, message: &QueryMsg) -> StdResult { - self.app - .wrap() - .query_wasm_smart(self.contract_address.as_str(), message) - } - - pub fn generate_account(&mut self) -> Addr { - self.app - .api() - .addr_make(&format!("foomp{}", self.rng.next_u64())) - } - - pub fn admin_unchecked(&self) -> Addr { - NYM_POOL_STORAGE - .contract_admin - .get(self.deps()) - .unwrap() - .unwrap() - } - - pub fn change_admin(&mut self, new_admin: &Addr) { +pub trait NymPoolContractTesterExt: + ContractOpts + + ChainOpts + + AdminExt + + DenomExt + + RandExt +{ + fn change_admin(&mut self, new_admin: &Addr) { self.execute_msg( self.admin_unchecked(), &ExecuteMsg::UpdateAdmin { @@ -231,33 +75,14 @@ impl TestSetup { .unwrap(); } - pub fn admin_msg(&self) -> MessageInfo { - message_info(&self.admin_unchecked(), &[]) - } - - pub fn denom(&self) -> String { - NYM_POOL_STORAGE - .pool_denomination - .load(self.storage()) - .unwrap() - } - - pub fn coin(&self, amount: u128) -> Coin { - coin(amount, self.denom()) - } - - pub fn coins(&self, amount: u128) -> Vec { - coins(amount, self.denom()) - } - #[track_caller] - pub fn add_dummy_grant(&mut self) -> Grant { + fn add_dummy_grant(&mut self) -> Grant { let grantee = self.generate_account(); self.add_dummy_grant_for(&grantee) } #[track_caller] - pub fn add_dummy_grant_for(&mut self, grantee: impl Into) -> Grant { + fn add_dummy_grant_for(&mut self, grantee: impl Into) -> Grant { let grantee = Addr::unchecked(grantee); let granter = self.admin_unchecked(); let env = self.env(); @@ -275,23 +100,18 @@ impl TestSetup { } #[track_caller] - pub fn lock_allowance(&mut self, grantee: impl Into, amount: impl Into) { - let denom = NYM_POOL_STORAGE - .pool_denomination - .load(self.deps().storage) - .unwrap(); - + fn lock_allowance(&mut self, grantee: impl Into, amount: impl Into) { self.execute_msg( Addr::unchecked(grantee), &ExecuteMsg::LockAllowance { - amount: coin(amount.into().u128(), denom), + amount: self.coin(amount.into().u128()), }, ) .unwrap(); } #[track_caller] - pub fn full_locked_map(&self) -> HashMap { + fn full_locked_map(&self) -> HashMap { NYM_POOL_STORAGE .locked .grantees @@ -301,7 +121,7 @@ impl TestSetup { } #[track_caller] - pub fn add_granter(&mut self, granter: &Addr) { + fn add_granter(&mut self, granter: &Addr) { let env = self.env(); let admin = self.admin_unchecked(); NYM_POOL_STORAGE @@ -309,3 +129,5 @@ impl TestSetup { .unwrap(); } } + +impl NymPoolContractTesterExt for ContractTester {} diff --git a/contracts/nym-pool/src/transactions.rs b/contracts/nym-pool/src/transactions.rs index 59181bd07b..3274a31c34 100644 --- a/contracts/nym-pool/src/transactions.rs +++ b/contracts/nym-pool/src/transactions.rs @@ -262,21 +262,22 @@ pub fn try_remove_expired( #[cfg(test)] mod tests { use super::*; - use crate::testing::TestSetup; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, DenomExt, RandExt}; use nym_pool_contract_common::ExecuteMsg; #[cfg(test)] mod updating_contract_admin { use super::*; - use crate::testing::TestSetup; use cosmwasm_std::{Deps, Order}; use cw_controllers::AdminError; + use nym_contracts_common_testing::{AdminExt, RandExt}; use nym_pool_contract_common::{ExecuteMsg, GranterAddress, GranterInformation}; use std::collections::HashMap; #[test] fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let random_acc = test.generate_account(); let new_admin = test.generate_account(); @@ -310,7 +311,7 @@ mod tests { #[test] fn requires_providing_valid_address() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let bad_account = "definitely-not-valid-account"; let res = test.execute_raw( @@ -347,7 +348,7 @@ mod tests { .collect() } - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let current_admin = test.admin_unchecked(); let new_admin = test.generate_account(); @@ -369,7 +370,7 @@ mod tests { // // - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let current_admin = test.admin_unchecked(); let new_admin = test.generate_account(); let old_granters = granters(test.deps()); @@ -392,13 +393,13 @@ mod tests { #[cfg(test)] mod granting_allowance { use super::*; - use crate::testing::TestSetup; use cosmwasm_std::StdError; + use nym_contracts_common_testing::{AdminExt, RandExt}; use nym_pool_contract_common::BasicAllowance; #[test] fn requires_providing_valid_grantee_address() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let admin = test.admin_msg(); @@ -433,12 +434,12 @@ mod tests { #[cfg(test)] mod revoking_allowance { use super::*; - use crate::testing::TestSetup; use cosmwasm_std::StdError; + use nym_contracts_common_testing::{AdminExt, RandExt}; #[test] fn requires_providing_valid_grantee_address() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let admin = test.admin_msg(); @@ -487,12 +488,12 @@ mod tests { #[cfg(test)] mod using_allowance { use super::*; - use crate::testing::TestSetup; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; use nym_pool_contract_common::{BasicAllowance, ExecuteMsg}; #[test] fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw(grantee, ExecuteMsg::UseAllowance { recipients: vec![] }); @@ -504,7 +505,7 @@ mod tests { #[test] fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> { // 1 bad receiver - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -519,7 +520,7 @@ mod tests { assert!(res.is_err()); // 3 receivers, one invalid - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let addr1 = test.generate_account(); @@ -547,7 +548,7 @@ mod tests { assert!(res.is_err()); // all fine - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -576,7 +577,7 @@ mod tests { #[test] fn requires_the_total_to_be_available_for_spending() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let recipient = test.generate_account(); // contract balance < required @@ -665,7 +666,7 @@ mod tests { #[test] fn requires_the_total_to_be_within_spend_limit() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: Some(test.coin(100)), expiration_unix_timestamp: None, @@ -712,7 +713,7 @@ mod tests { #[test] fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; @@ -774,7 +775,7 @@ mod tests { #[test] fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -810,13 +811,14 @@ mod tests { #[cfg(test)] mod withdrawing_from_allowance { use super::*; - use crate::testing::TestSetup; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; use cosmwasm_std::coin; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt}; use nym_pool_contract_common::{BasicAllowance, ExecuteMsg}; #[test] fn requires_valid_coin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -848,7 +850,7 @@ mod tests { #[test] fn requires_the_amount_to_be_available_for_spending() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); // contract balance < required let grantee = test.add_dummy_grant().grantee; @@ -912,7 +914,7 @@ mod tests { #[test] fn requires_the_amount_to_be_within_spend_limit() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: Some(test.coin(100)), expiration_unix_timestamp: None, @@ -952,7 +954,7 @@ mod tests { #[test] fn attaches_appropriate_bank_message() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; @@ -978,7 +980,7 @@ mod tests { #[test] fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -1011,7 +1013,7 @@ mod tests { #[test] fn locking_allowance() -> anyhow::Result<()> { // internals got tested in storage tests, so this is mostly about checking events (TODO) - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -1035,7 +1037,7 @@ mod tests { #[test] fn unlocking_allowance() -> anyhow::Result<()> { // internals got tested in storage tests, so this is mostly about checking events (TODO) - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(100)); @@ -1060,11 +1062,12 @@ mod tests { #[cfg(test)] mod using_locked_allowance { use super::*; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; use nym_pool_contract_common::BasicAllowance; #[test] fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; let res = test.execute_raw( @@ -1079,7 +1082,7 @@ mod tests { #[test] fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> { // 1 bad receiver - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1095,7 +1098,7 @@ mod tests { assert!(res.is_err()); // 3 receivers, one invalid - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1124,7 +1127,7 @@ mod tests { assert!(res.is_err()); // all fine - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1154,7 +1157,7 @@ mod tests { #[test] fn requires_the_total_to_be_locked_by_grantee() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(100)); @@ -1193,7 +1196,7 @@ mod tests { #[test] fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1256,7 +1259,7 @@ mod tests { #[test] fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -1294,11 +1297,12 @@ mod tests { mod withdrawing_from_locked_allowance { use super::*; use cosmwasm_std::coin; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; use nym_pool_contract_common::BasicAllowance; #[test] fn requires_valid_coin() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1331,7 +1335,7 @@ mod tests { #[test] fn attaches_appropriate_bank_message() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(10000)); @@ -1358,7 +1362,7 @@ mod tests { #[test] fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -1391,7 +1395,7 @@ mod tests { #[test] fn requires_the_amount_to_be_locked_by_grantee() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let grantee = test.add_dummy_grant().grantee; test.lock_allowance(&grantee, Uint128::new(100)); @@ -1424,7 +1428,7 @@ mod tests { #[test] fn adding_new_granter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let bad_address = "foomp"; let good_address = test.generate_account(); @@ -1456,7 +1460,7 @@ mod tests { #[test] fn revoking_granter() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let bad_address = "foomp"; let good_address = test.generate_account(); let granter_address = test.generate_account(); @@ -1500,10 +1504,12 @@ mod tests { #[cfg(test)] mod removing_expired { use super::*; + use crate::testing::{init_contract_tester, NymPoolContract, NymPoolContractTesterExt}; + use nym_contracts_common_testing::{ChainOpts, ContractOpts, ContractTester, RandExt}; use nym_pool_contract_common::{BasicAllowance, GranteeAddress}; - fn setup_with_expired_grant() -> (TestSetup, GranteeAddress) { - let mut test = TestSetup::init(); + fn setup_with_expired_grant() -> (ContractTester, GranteeAddress) { + let mut test = init_contract_tester(); let env = test.env(); let allowance = Allowance::Basic(BasicAllowance { spend_limit: None, @@ -1543,7 +1549,7 @@ mod tests { #[test] fn requires_grant_to_actually_exist_and_be_expired() -> anyhow::Result<()> { - let mut test = TestSetup::init(); + let mut test = init_contract_tester(); let sender = test.generate_account(); let grantee = test.add_dummy_grant().grantee; let not_grantee = test.generate_account(); diff --git a/contracts/performance/.cargo/config b/contracts/performance/.cargo/config new file mode 100644 index 0000000000..2fb2c1afdb --- /dev/null +++ b/contracts/performance/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema --features=schema-gen" \ No newline at end of file diff --git a/contracts/performance/Cargo.toml b/contracts/performance/Cargo.toml new file mode 100644 index 0000000000..abeef17f40 --- /dev/null +++ b/contracts/performance/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "nym-performance-contract" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "schema" +required-features = ["schema-gen"] + +[lib] +name = "nym_performance_contract" +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-controllers = { workspace = true } +serde = { workspace = true } + +cosmwasm-schema = { workspace = true, optional = true } + +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-performance-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-performance-contract" } +nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } + +[dev-dependencies] +anyhow = { workspace = true } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } +nym-mixnet-contract = { path = "../mixnet", features = ["testable-mixnet-contract"] } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } + +[features] +schema-gen = ["nym-performance-contract-common/schema", "cosmwasm-schema"] + +[lints] +workspace = true \ No newline at end of file diff --git a/contracts/performance/Makefile b/contracts/performance/Makefile new file mode 100644 index 0000000000..086fa71ad3 --- /dev/null +++ b/contracts/performance/Makefile @@ -0,0 +1,5 @@ +wasm: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +generate-schema: + cargo schema diff --git a/contracts/performance/schema/nym-performance-contract.json b/contracts/performance/schema/nym-performance-contract.json new file mode 100644 index 0000000000..2242aefd75 --- /dev/null +++ b/contracts/performance/schema/nym-performance-contract.json @@ -0,0 +1,1240 @@ +{ + "contract_name": "nym-performance-contract", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "authorised_network_monitors", + "mixnet_contract_address" + ], + "properties": { + "authorised_network_monitors": { + "type": "array", + "items": { + "type": "string" + } + }, + "mixnet_contract_address": { + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a particular node for given epoch", + "type": "object", + "required": [ + "submit" + ], + "properties": { + "submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a batch of nodes for given epoch", + "type": "object", + "required": [ + "batch_submit" + ], + "properties": { + "batch_submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to authorise new network monitor for submitting performance data", + "type": "object", + "required": [ + "authorise_network_monitor" + ], + "properties": { + "authorise_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to retire an existing network monitor and forbid it from submitting any future performance data", + "type": "object", + "required": [ + "retire_network_monitor" + ], + "properties": { + "retire_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns performance of particular node for the provided epoch", + "type": "object", + "required": [ + "node_performance" + ], + "properties": { + "node_performance": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns historical performance for particular node", + "type": "object", + "required": [ + "node_performance_paged" + ], + "properties": { + "node_performance_paged": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns all submitted measurements for the particular node", + "type": "object", + "required": [ + "node_measurements" + ], + "properties": { + "node_measurements": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) measurements for particular epoch", + "type": "object", + "required": [ + "epoch_measurements_paged" + ], + "properties": { + "epoch_measurements_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) performance for particular epoch", + "type": "object", + "required": [ + "epoch_performance_paged" + ], + "properties": { + "epoch_performance_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns full (paged) historical performance of the whole network", + "type": "object", + "required": [ + "full_historical_performance_paged" + ], + "properties": { + "full_historical_performance_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about particular network monitor", + "type": "object", + "required": [ + "network_monitor" + ], + "properties": { + "network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all network monitors", + "type": "object", + "required": [ + "network_monitors_paged" + ], + "properties": { + "network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all retired network monitors", + "type": "object", + "required": [ + "retired_network_monitors_paged" + ], + "properties": { + "retired_network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information regarding the latest submitted performance data", + "type": "object", + "required": [ + "last_submitted_measurement" + ], + "properties": { + "last_submitted_measurement": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "epoch_measurements_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochMeasurementsPagedResponse", + "type": "object", + "required": [ + "epoch_id", + "measurements" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/definitions/NodeMeasurement" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeMeasurement": { + "type": "object", + "required": [ + "measurements", + "node_id" + ], + "properties": { + "measurements": { + "$ref": "#/definitions/NodeResults" + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "epoch_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochPerformancePagedResponse", + "type": "object", + "required": [ + "epoch_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "full_historical_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FullHistoricalPerformancePagedResponse", + "type": "object", + "required": [ + "performance" + ], + "properties": { + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalPerformance" + } + }, + "start_next_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HistoricalPerformance": { + "type": "object", + "required": [ + "epoch_id", + "node_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "last_submitted_measurement": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LastSubmission", + "type": "object", + "required": [ + "block_height", + "block_time" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "block_time": { + "$ref": "#/definitions/Timestamp" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/LastSubmittedData" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "LastSubmittedData": { + "type": "object", + "required": [ + "data", + "epoch_id", + "sender" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "sender": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "network_monitor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorResponse", + "type": "object", + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkMonitorInformation" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "network_monitors_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkMonitorInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "node_measurements": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeMeasurementsResponse", + "type": "object", + "properties": { + "measurements": { + "anyOf": [ + { + "$ref": "#/definitions/NodeResults" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "node_performance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformanceResponse", + "type": "object", + "properties": { + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "node_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformancePagedResponse", + "type": "object", + "required": [ + "node_id", + "performance" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/EpochNodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EpochNodePerformance": { + "type": "object", + "required": [ + "epoch" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "retired_network_monitors_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RetiredNetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/RetiredNetworkMonitor" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "RetiredNetworkMonitor": { + "type": "object", + "required": [ + "details", + "retired_at_height", + "retired_by" + ], + "properties": { + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + }, + "retired_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "retired_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } + } + } +} diff --git a/contracts/performance/schema/raw/execute.json b/contracts/performance/schema/raw/execute.json new file mode 100644 index 0000000000..c4b5c8be04 --- /dev/null +++ b/contracts/performance/schema/raw/execute.json @@ -0,0 +1,163 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a particular node for given epoch", + "type": "object", + "required": [ + "submit" + ], + "properties": { + "submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a batch of nodes for given epoch", + "type": "object", + "required": [ + "batch_submit" + ], + "properties": { + "batch_submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to authorise new network monitor for submitting performance data", + "type": "object", + "required": [ + "authorise_network_monitor" + ], + "properties": { + "authorise_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to retire an existing network monitor and forbid it from submitting any future performance data", + "type": "object", + "required": [ + "retire_network_monitor" + ], + "properties": { + "retire_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/instantiate.json b/contracts/performance/schema/raw/instantiate.json new file mode 100644 index 0000000000..4380993e57 --- /dev/null +++ b/contracts/performance/schema/raw/instantiate.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "authorised_network_monitors", + "mixnet_contract_address" + ], + "properties": { + "authorised_network_monitors": { + "type": "array", + "items": { + "type": "string" + } + }, + "mixnet_contract_address": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/migrate.json b/contracts/performance/schema/raw/migrate.json new file mode 100644 index 0000000000..7fbe8c5708 --- /dev/null +++ b/contracts/performance/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/query.json b/contracts/performance/schema/raw/query.json new file mode 100644 index 0000000000..bcca411780 --- /dev/null +++ b/contracts/performance/schema/raw/query.json @@ -0,0 +1,339 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns performance of particular node for the provided epoch", + "type": "object", + "required": [ + "node_performance" + ], + "properties": { + "node_performance": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns historical performance for particular node", + "type": "object", + "required": [ + "node_performance_paged" + ], + "properties": { + "node_performance_paged": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns all submitted measurements for the particular node", + "type": "object", + "required": [ + "node_measurements" + ], + "properties": { + "node_measurements": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) measurements for particular epoch", + "type": "object", + "required": [ + "epoch_measurements_paged" + ], + "properties": { + "epoch_measurements_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) performance for particular epoch", + "type": "object", + "required": [ + "epoch_performance_paged" + ], + "properties": { + "epoch_performance_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns full (paged) historical performance of the whole network", + "type": "object", + "required": [ + "full_historical_performance_paged" + ], + "properties": { + "full_historical_performance_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about particular network monitor", + "type": "object", + "required": [ + "network_monitor" + ], + "properties": { + "network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all network monitors", + "type": "object", + "required": [ + "network_monitors_paged" + ], + "properties": { + "network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all retired network monitors", + "type": "object", + "required": [ + "retired_network_monitors_paged" + ], + "properties": { + "retired_network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information regarding the latest submitted performance data", + "type": "object", + "required": [ + "last_submitted_measurement" + ], + "properties": { + "last_submitted_measurement": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/performance/schema/raw/response_to_admin.json b/contracts/performance/schema/raw/response_to_admin.json new file mode 100644 index 0000000000..c73969ab04 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_admin.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json b/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json new file mode 100644 index 0000000000..4c16b66bb7 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochMeasurementsPagedResponse", + "type": "object", + "required": [ + "epoch_id", + "measurements" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/definitions/NodeMeasurement" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeMeasurement": { + "type": "object", + "required": [ + "measurements", + "node_id" + ], + "properties": { + "measurements": { + "$ref": "#/definitions/NodeResults" + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_epoch_performance_paged.json b/contracts/performance/schema/raw/response_to_epoch_performance_paged.json new file mode 100644 index 0000000000..235c12b64f --- /dev/null +++ b/contracts/performance/schema/raw/response_to_epoch_performance_paged.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochPerformancePagedResponse", + "type": "object", + "required": [ + "epoch_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json b/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json new file mode 100644 index 0000000000..6f0eb6e191 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FullHistoricalPerformancePagedResponse", + "type": "object", + "required": [ + "performance" + ], + "properties": { + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalPerformance" + } + }, + "start_next_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HistoricalPerformance": { + "type": "object", + "required": [ + "epoch_id", + "node_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_last_submitted_measurement.json b/contracts/performance/schema/raw/response_to_last_submitted_measurement.json new file mode 100644 index 0000000000..7c3779d9a6 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_last_submitted_measurement.json @@ -0,0 +1,100 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LastSubmission", + "type": "object", + "required": [ + "block_height", + "block_time" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "block_time": { + "$ref": "#/definitions/Timestamp" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/LastSubmittedData" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "LastSubmittedData": { + "type": "object", + "required": [ + "data", + "epoch_id", + "sender" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "sender": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/performance/schema/raw/response_to_network_monitor.json b/contracts/performance/schema/raw/response_to_network_monitor.json new file mode 100644 index 0000000000..eecd26abe3 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_network_monitor.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorResponse", + "type": "object", + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkMonitorInformation" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/schema/raw/response_to_network_monitors_paged.json b/contracts/performance/schema/raw/response_to_network_monitors_paged.json new file mode 100644 index 0000000000..a811963072 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_network_monitors_paged.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkMonitorInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_measurements.json b/contracts/performance/schema/raw/response_to_node_measurements.json new file mode 100644 index 0000000000..e1add3c02d --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_measurements.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeMeasurementsResponse", + "type": "object", + "properties": { + "measurements": { + "anyOf": [ + { + "$ref": "#/definitions/NodeResults" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_performance.json b/contracts/performance/schema/raw/response_to_node_performance.json new file mode 100644 index 0000000000..d9bae2e00d --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_performance.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformanceResponse", + "type": "object", + "properties": { + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_performance_paged.json b/contracts/performance/schema/raw/response_to_node_performance_paged.json new file mode 100644 index 0000000000..f63c28f477 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_performance_paged.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformancePagedResponse", + "type": "object", + "required": [ + "node_id", + "performance" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/EpochNodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EpochNodePerformance": { + "type": "object", + "required": [ + "epoch" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json b/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json new file mode 100644 index 0000000000..f5310e9438 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RetiredNetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/RetiredNetworkMonitor" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "RetiredNetworkMonitor": { + "type": "object", + "required": [ + "details", + "retired_at_height", + "retired_by" + ], + "properties": { + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + }, + "retired_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "retired_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/src/bin/schema.rs b/contracts/performance/src/bin/schema.rs new file mode 100644 index 0000000000..d34f6f9b74 --- /dev/null +++ b/contracts/performance/src/bin/schema.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::write_api; +use nym_performance_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/performance/src/contract.rs b/contracts/performance/src/contract.rs new file mode 100644 index 0000000000..2f488f3961 --- /dev/null +++ b/contracts/performance/src/contract.rs @@ -0,0 +1,188 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::queries::{ + query_admin, query_epoch_measurements_paged, query_epoch_performance_paged, + query_full_historical_performance_paged, query_last_submission, query_network_monitor_details, + query_network_monitors_paged, query_node_measurements, query_node_performance, + query_node_performance_paged, query_retired_network_monitors_paged, +}; +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use crate::transactions::{ + try_authorise_network_monitor, try_batch_submit_performance_results, + try_remove_epoch_measurements, try_remove_node_measurements, try_retire_network_monitor, + try_submit_performance_results, try_update_contract_admin, +}; +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, +}; +use nym_contracts_common::set_build_information; +use nym_performance_contract_common::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, NymPerformanceContractError, QueryMsg, +}; + +const CONTRACT_NAME: &str = "crate:nym-performance-contract"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[entry_point] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + set_build_information!(deps.storage)?; + + let mixnet_contract_address = deps.api.addr_validate(&msg.mixnet_contract_address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.initialise( + deps, + env, + info.sender, + mixnet_contract_address.clone(), + msg.authorised_network_monitors, + )?; + + Ok(Response::default()) +} + +#[entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin), + ExecuteMsg::Submit { epoch, data } => { + try_submit_performance_results(deps, env, info, epoch, data) + } + ExecuteMsg::BatchSubmit { epoch, data } => { + try_batch_submit_performance_results(deps, env, info, epoch, data) + } + ExecuteMsg::AuthoriseNetworkMonitor { address } => { + try_authorise_network_monitor(deps, env, info, address) + } + ExecuteMsg::RetireNetworkMonitor { address } => { + try_retire_network_monitor(deps, env, info, address) + } + ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => { + try_remove_node_measurements(deps, info, epoch_id, node_id) + } + ExecuteMsg::RemoveEpochMeasurements { epoch_id } => { + try_remove_epoch_measurements(deps, info, epoch_id) + } + } +} + +#[entry_point] +pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?), + QueryMsg::NodePerformance { epoch_id, node_id } => Ok(to_json_binary( + &query_node_performance(deps, epoch_id, node_id)?, + )?), + QueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + } => Ok(to_json_binary(&query_node_performance_paged( + deps, + node_id, + start_after, + limit, + )?)?), + QueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + } => Ok(to_json_binary(&query_epoch_performance_paged( + deps, + epoch_id, + start_after, + limit, + )?)?), + QueryMsg::FullHistoricalPerformancePaged { start_after, limit } => Ok(to_json_binary( + &query_full_historical_performance_paged(deps, start_after, limit)?, + )?), + QueryMsg::NetworkMonitor { address } => Ok(to_json_binary( + &query_network_monitor_details(deps, address)?, + )?), + QueryMsg::NetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary( + &query_network_monitors_paged(deps, start_after, limit)?, + )?), + QueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary( + &query_retired_network_monitors_paged(deps, start_after, limit)?, + )?), + QueryMsg::NodeMeasurements { epoch_id, node_id } => Ok(to_json_binary( + &query_node_measurements(deps, epoch_id, node_id)?, + )?), + QueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + } => Ok(to_json_binary(&query_epoch_measurements_paged( + deps, + epoch_id, + start_after, + limit, + )?)?), + QueryMsg::LastSubmittedMeasurement {} => Ok(to_json_binary(&query_last_submission(deps)?)?), + } +} + +#[entry_point] +pub fn migrate( + deps: DepsMut, + _: Env, + _msg: MigrateMsg, +) -> Result { + set_build_information!(deps.storage)?; + cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Default::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod contract_instantiation { + use super::*; + use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; + use crate::testing::PreInitContract; + use cosmwasm_std::testing::message_info; + + #[test] + fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> { + // we need to mock dependencies in a state where mixnet contract has already been instantiated + // (we query it at init) + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + let mixnet_contract_address = pre_init.mixnet_contract_address.to_string(); + let some_sender = pre_init.addr_make("some_sender"); + let deps = pre_init.deps_mut(); + + instantiate( + deps, + env, + message_info(&some_sender, &[]), + InstantiateMsg { + mixnet_contract_address, + authorised_network_monitors: vec![], + }, + )?; + + let deps = pre_init.deps(); + + NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .assert_admin(deps, &some_sender)?; + + Ok(()) + } + } +} diff --git a/contracts/performance/src/helpers.rs b/contracts/performance/src/helpers.rs new file mode 100644 index 0000000000..51ccb4f4b2 --- /dev/null +++ b/contracts/performance/src/helpers.rs @@ -0,0 +1,118 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdError, StdResult}; +use cw_storage_plus::{Key, Namespace, Path, PrimaryKey}; +use nym_mixnet_contract_common::{Interval, MixNodeBond, NymNodeBond}; +use nym_performance_contract_common::{EpochId, NodeId}; +use serde::de::DeserializeOwned; +use std::ops::Deref; + +pub(crate) trait MixnetContractQuerier { + #[allow(dead_code)] + fn query_mixnet_contract( + &self, + address: impl Into, + msg: &nym_mixnet_contract_common::QueryMsg, + ) -> StdResult; + + fn query_mixnet_contract_storage( + &self, + address: impl Into, + key: impl Into, + ) -> StdResult>>; + + fn query_mixnet_contract_storage_value( + &self, + address: impl Into, + key: impl Into, + ) -> StdResult> { + match self.query_mixnet_contract_storage(address, key)? { + None => Ok(None), + Some(value) => Ok(Some(from_json(&value)?)), + } + } + + fn query_current_mixnet_interval(&self, address: impl Into) -> StdResult { + self.query_mixnet_contract_storage_value(address, b"ci")? + .ok_or(StdError::not_found( + "unable to retrieve interval information from the mixnet contract storage", + )) + } + + fn query_current_absolute_mixnet_epoch_id( + &self, + address: impl Into, + ) -> StdResult { + self.query_current_mixnet_interval(address) + .map(|interval| interval.current_epoch_absolute_id()) + } + + fn check_node_existence(&self, address: impl Into, node_id: NodeId) -> StdResult { + let mixnet_contract_address = address.into(); + + // 1. check if it's a nym-node + if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? { + return Ok(!nym_node.is_unbonding); + } + + // 2. try a legacy mixnode + if let Some(nym_node) = self.query_mixnode_bond(mixnet_contract_address, node_id)? { + return Ok(!nym_node.is_unbonding); + } + Ok(false) + } + + fn query_nymnode_bond( + &self, + address: impl Into, + node_id: NodeId, + ) -> StdResult> { + // construct proper map key + let pk_namespace = "nn"; + let path: Path = Path::new( + Namespace::from_static_str(pk_namespace).as_slice(), + &node_id.key().iter().map(Key::as_ref).collect::>(), + ); + let storage_key = path.deref(); + + self.query_mixnet_contract_storage_value(address, storage_key) + } + + fn query_mixnode_bond( + &self, + address: impl Into, + node_id: NodeId, + ) -> StdResult> { + // construct proper map key + let pk_namespace = "mnn"; + let path: Path = Path::new( + Namespace::from_static_str(pk_namespace).as_slice(), + &node_id.key().iter().map(Key::as_ref).collect::>(), + ); + let storage_key = path.deref(); + + self.query_mixnet_contract_storage_value(address, storage_key) + } +} + +impl MixnetContractQuerier for QuerierWrapper<'_, C> +where + C: CustomQuery, +{ + fn query_mixnet_contract( + &self, + address: impl Into, + msg: &nym_mixnet_contract_common::QueryMsg, + ) -> StdResult { + self.query_wasm_smart(address, msg) + } + + fn query_mixnet_contract_storage( + &self, + address: impl Into, + key: impl Into, + ) -> StdResult>> { + self.query_wasm_raw(address, key) + } +} diff --git a/contracts/performance/src/lib.rs b/contracts/performance/src/lib.rs new file mode 100644 index 0000000000..08a2b9651d --- /dev/null +++ b/contracts/performance/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod contract; +pub mod queued_migrations; +pub mod storage; + +mod helpers; +mod queries; +mod transactions; + +#[cfg(test)] +pub mod testing; diff --git a/contracts/performance/src/queries.rs b/contracts/performance/src/queries.rs new file mode 100644 index 0000000000..5fe2c3ed91 --- /dev/null +++ b/contracts/performance/src/queries.rs @@ -0,0 +1,667 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::storage::{retrieval_limits, NYM_PERFORMANCE_CONTRACT_STORAGE}; +use cosmwasm_std::{Addr, Deps, Order, StdResult}; +use cw_controllers::AdminResponse; +use cw_storage_plus::Bound; +use nym_performance_contract_common::{ + EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, + NetworkMonitorInformation, NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, + NodeMeasurement, NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + NodePerformanceResponse, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse, +}; + +pub fn query_admin(deps: Deps) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .query_admin(deps) + .map_err(Into::into) +} + +pub fn query_node_performance( + deps: Deps, + epoch_id: EpochId, + node_id: NodeId, +) -> Result { + let performance = + NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance(deps.storage, epoch_id, node_id)?; + Ok(NodePerformanceResponse { performance }) +} + +pub fn query_node_measurements( + deps: Deps, + epoch_id: EpochId, + node_id: NodeId, +) -> Result { + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .may_load(deps.storage, (epoch_id, node_id))?; + Ok(NodeMeasurementsResponse { measurements }) +} + +pub fn query_node_performance_paged( + deps: Deps, + node_id: NodeId, + start_after: Option, + limit: Option, +) -> Result { + let current_epoch_id = NYM_PERFORMANCE_CONTRACT_STORAGE.current_mixnet_epoch_id(deps)?; + + let start = match start_after { + None => NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_epoch_id_at_creation + .load(deps.storage)?, + Some(start_after) => start_after + 1, + }; + + let mut performance = Vec::new(); + + if current_epoch_id < start { + return Ok(NodePerformancePagedResponse { + node_id, + performance, + start_next_after: None, + }); + } + + let limit = limit + .unwrap_or(retrieval_limits::NODE_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_PERFORMANCE_MAX_LIMIT) as usize; + + for epoch_id in (start..=current_epoch_id).take(limit) { + performance.push(EpochNodePerformance { + epoch: epoch_id, + performance: NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance( + deps.storage, + epoch_id, + node_id, + )?, + }) + } + + let start_next_after = performance.last().and_then(|last| { + if last.epoch != current_epoch_id { + Some(last.epoch) + } else { + None + } + }); + + Ok(NodePerformancePagedResponse { + node_id, + performance, + start_next_after, + }) +} + +pub fn query_epoch_performance_paged( + deps: Deps, + epoch_id: EpochId, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_EPOCH_PERFORMANCE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let performance = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .prefix(epoch_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|(node_id, results)| NodePerformance { + node_id, + performance: results.median(), + }) + }) + .collect::>>()?; + + let start_next_after = performance.last().map(|last| last.node_id); + + Ok(EpochPerformancePagedResponse { + epoch_id, + performance, + start_next_after, + }) +} + +pub fn query_epoch_measurements_paged( + deps: Deps, + epoch_id: EpochId, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_EPOCH_MEASUREMENTS_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .prefix(epoch_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|(node_id, measurements)| NodeMeasurement { + node_id, + measurements, + }) + }) + .collect::>>()?; + + let start_next_after = measurements.last().map(|last| last.node_id); + + Ok(EpochMeasurementsPagedResponse { + epoch_id, + measurements, + start_next_after, + }) +} + +pub fn query_full_historical_performance_paged( + deps: Deps, + start_after: Option<(EpochId, NodeId)>, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let performance = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|((epoch_id, node_id), results)| HistoricalPerformance { + epoch_id, + node_id, + performance: results.median(), + }) + }) + .collect::>>()?; + + let start_next_after = performance.last().map(|last| (last.epoch_id, last.node_id)); + + Ok(FullHistoricalPerformancePagedResponse { + performance, + start_next_after, + }) +} + +fn get_network_monitor_information( + deps: Deps, + address: &Addr, +) -> Result, NymPerformanceContractError> { + let Some(details) = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .authorised + .may_load(deps.storage, address)? + else { + return Ok(None); + }; + + let current_submission_metadata = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .submission_metadata + .load(deps.storage, address)?; + + Ok(Some(NetworkMonitorInformation { + details, + current_submission_metadata, + })) +} + +pub fn query_network_monitor_details( + deps: Deps, + address: String, +) -> Result { + let address = deps.api.addr_validate(&address)?; + + Ok(NetworkMonitorResponse { + info: get_network_monitor_information(deps, &address)?, + }) +} + +pub fn query_network_monitors_paged( + deps: Deps, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::NETWORK_MONITORS_DEFAULT_LIMIT) + .min(retrieval_limits::NETWORK_MONITORS_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + let start = addr.as_ref().map(Bound::exclusive); + + let info = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .authorised + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.and_then(|(address, details)| { + NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .submission_metadata + .load(deps.storage, &address) + .map(|current_submission_metadata| NetworkMonitorInformation { + details, + current_submission_metadata, + }) + }) + }) + .collect::>>()?; + + let start_next_after = info.last().map(|last| last.details.address.to_string()); + + Ok(NetworkMonitorsPagedResponse { + info, + start_next_after, + }) +} + +pub fn query_retired_network_monitors_paged( + deps: Deps, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::RETIRED_NETWORK_MONITORS_DEFAULT_LIMIT) + .min(retrieval_limits::RETIRED_NETWORK_MONITORS_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + let start = addr.as_ref().map(Bound::exclusive); + + let info = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .retired + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| record.map(|(_, details)| details)) + .collect::>>()?; + + let start_next_after = info.last().map(|last| last.details.address.to_string()); + + Ok(RetiredNetworkMonitorsPagedResponse { + info, + start_next_after, + }) +} + +pub fn query_last_submission(deps: Deps) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE + .last_performance_submission + .load(deps.storage) + .map_err(Into::into) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{ChainOpts, ContractOpts, RandExt}; + use nym_performance_contract_common::LastSubmittedData; + + #[cfg(test)] + mod admin_query { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt}; + use nym_performance_contract_common::ExecuteMsg; + + #[test] + fn returns_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let initial_admin = test.admin_unchecked(); + + // initial + let res = query_admin(test.deps())?; + assert_eq!(res.admin, Some(initial_admin.to_string())); + + let new_admin = test.generate_account(); + + // sanity check + assert_ne!(initial_admin, new_admin); + + // after update + test.execute_msg( + initial_admin.clone(), + &ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + )?; + + let updated_admin = query_admin(test.deps())?; + assert_eq!(updated_admin.admin, Some(new_admin.to_string())); + + Ok(()) + } + } + + #[test] + fn querying_node_performance_paged() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let node_id = test.bond_dummy_nymnode()?; + let nm = test.generate_account(); + test.authorise_network_monitor(&nm)?; + + // epoch 0 + test.insert_raw_performance(&nm, node_id, "0")?; + + // epoch 1 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.1")?; + + // epoch 2 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.2")?; + + // epoch 3 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.3")?; + + // epoch 4 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.4")?; + + // epoch 5 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.5")?; + + let deps = test.deps(); + let res = query_node_performance_paged(deps, node_id, Some(5), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_node_performance_paged(deps, node_id, Some(42), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_node_performance_paged(deps, node_id, Some(4), None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + }] + ); + + let res = query_node_performance_paged(deps, node_id, Some(2), None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![ + EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }, + EpochNodePerformance { + epoch: 4, + performance: Some("0.4".parse()?), + }, + EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + } + ] + ); + + let res = query_node_performance_paged(deps, node_id, None, None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![ + EpochNodePerformance { + epoch: 0, + performance: Some("0".parse()?), + }, + EpochNodePerformance { + epoch: 1, + performance: Some("0.1".parse()?), + }, + EpochNodePerformance { + epoch: 2, + performance: Some("0.2".parse()?), + }, + EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }, + EpochNodePerformance { + epoch: 4, + performance: Some("0.4".parse()?), + }, + EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + } + ] + ); + + let res = query_node_performance_paged(deps, node_id, Some(2), Some(1))?; + assert_eq!(res.start_next_after, Some(3)); + assert_eq!( + res.performance, + vec![EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }] + ); + + Ok(()) + } + + #[test] + fn querying_epoch_performance_paged() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let nm = test.generate_account(); + test.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(test.bond_dummy_nymnode()?); + } + + let epoch_id = 5; + test.set_mixnet_epoch(epoch_id)?; + + test.insert_raw_performance(&nm, nodes[1], "0.1")?; + test.insert_raw_performance(&nm, nodes[2], "0.2")?; + test.insert_raw_performance(&nm, nodes[3], "0.3")?; + // 4 is missing + test.insert_raw_performance(&nm, nodes[5], "0.5")?; + test.insert_raw_performance(&nm, nodes[6], "0.6")?; + + let deps = test.deps(); + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[6]), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(42), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[4]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[3]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }, + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, None, None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[1], + performance: "0.1".parse()?, + }, + NodePerformance { + node_id: nodes[2], + performance: "0.2".parse()?, + }, + NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }, + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), Some(1))?; + assert_eq!(res.start_next_after, Some(nodes[3])); + assert_eq!( + res.performance, + vec![NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }] + ); + + Ok(()) + } + + #[test] + fn last_submission_query() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let env = test.env(); + + let id1 = test.bond_dummy_nymnode()?; + let id2 = test.bond_dummy_nymnode()?; + + // initial + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + } + ); + + let nm1 = test.generate_account(); + let nm2 = test.generate_account(); + test.authorise_network_monitor(&nm1)?; + test.authorise_network_monitor(&nm2)?; + test.set_mixnet_epoch(10)?; + + test.insert_raw_performance(&nm1, id1, "0.2")?; + + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm1.clone(), + epoch_id: 10, + data: NodePerformance { + node_id: id1, + performance: "0.2".parse()? + }, + }), + } + ); + + test.next_block(); + let env = test.env(); + + test.insert_epoch_performance(&nm2, 5, id2, "0.3".parse()?)?; + + // note that even though it's "earlier" data, last submission is still updated accordingly + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm2.clone(), + epoch_id: 5, + data: NodePerformance { + node_id: id2, + performance: "0.3".parse()? + }, + }), + } + ); + + Ok(()) + } +} diff --git a/contracts/performance/src/queued_migrations.rs b/contracts/performance/src/queued_migrations.rs new file mode 100644 index 0000000000..7e1e3cacd6 --- /dev/null +++ b/contracts/performance/src/queued_migrations.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs new file mode 100644 index 0000000000..acba90762b --- /dev/null +++ b/contracts/performance/src/storage.rs @@ -0,0 +1,3091 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::MixnetContractQuerier; +use cosmwasm_std::{Addr, Deps, DepsMut, Env, StdError, Storage}; +use cw_controllers::Admin; +use cw_storage_plus::{Item, Map}; +use nym_contracts_common::Percent; +use nym_performance_contract_common::constants::storage_keys; +use nym_performance_contract_common::{ + BatchSubmissionResult, EpochId, LastSubmission, LastSubmittedData, NetworkMonitorDetails, + NetworkMonitorSubmissionMetadata, NodeId, NodePerformance, NodeResults, + NymPerformanceContractError, RemoveEpochMeasurementsResponse, RetiredNetworkMonitor, +}; + +pub const NYM_PERFORMANCE_CONTRACT_STORAGE: NymPerformanceContractStorage = + NymPerformanceContractStorage::new(); + +pub struct NymPerformanceContractStorage { + pub(crate) contract_admin: Admin, + pub(crate) mixnet_epoch_id_at_creation: Item, + pub(crate) last_performance_submission: Item, + + pub(crate) mixnet_contract_address: Item, + + pub(crate) network_monitors: NetworkMonitorsStorage, + + pub(crate) performance_results: PerformanceResultsStorage, +} + +impl NymPerformanceContractStorage { + #[allow(clippy::new_without_default)] + pub(crate) const fn new() -> Self { + NymPerformanceContractStorage { + contract_admin: Admin::new(storage_keys::CONTRACT_ADMIN), + mixnet_epoch_id_at_creation: Item::new(storage_keys::INITIAL_EPOCH_ID), + last_performance_submission: Item::new(storage_keys::LAST_SUBMISSION), + mixnet_contract_address: Item::new(storage_keys::MIXNET_CONTRACT), + network_monitors: NetworkMonitorsStorage::new(), + performance_results: PerformanceResultsStorage::new(), + } + } + + pub fn current_mixnet_epoch_id( + &self, + deps: Deps, + ) -> Result { + let mixnet_contract_address = self.mixnet_contract_address.load(deps.storage)?; + let current_epoch_id = deps + .querier + .query_current_absolute_mixnet_epoch_id(&mixnet_contract_address)?; + Ok(current_epoch_id) + } + + pub fn node_bonded( + &self, + deps: Deps, + node_id: NodeId, + ) -> Result { + let mixnet_contract_address = self.mixnet_contract_address.load(deps.storage)?; + + let exists = deps + .querier + .check_node_existence(mixnet_contract_address, node_id)?; + Ok(exists) + } + + pub fn initialise( + &self, + mut deps: DepsMut, + env: Env, + admin: Addr, + mixnet_contract_address: Addr, + initial_authorised_network_monitors: Vec, + ) -> Result<(), NymPerformanceContractError> { + // set the mixnet contract address + self.mixnet_contract_address + .save(deps.storage, &mixnet_contract_address)?; + + let initial_epoch_id = self.current_mixnet_epoch_id(deps.as_ref())?; + + // set the last submission to the initial value + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + }, + )?; + + // set the initial epoch id + self.mixnet_epoch_id_at_creation + .save(deps.storage, &initial_epoch_id)?; + + // set the contract admin + self.contract_admin + .set(deps.branch(), Some(admin.clone()))?; + + // initialise the network monitors storage (by setting the current count to 0) + self.network_monitors.initialise(deps.branch())?; + + // add all initial network monitors + for network_monitor in initial_authorised_network_monitors { + let network_monitor = deps.api.addr_validate(&network_monitor)?; + self.authorise_network_monitor(deps.branch(), &env, &admin, network_monitor)?; + } + + Ok(()) + } + + pub fn submit_performance_data( + &self, + deps: DepsMut, + env: Env, + sender: &Addr, + epoch_id: EpochId, + data: NodePerformance, + ) -> Result<(), NymPerformanceContractError> { + // 1. check if the sender is authorised to submit performance data + self.network_monitors + .ensure_authorised(deps.storage, sender)?; + + // 2. check if current submission metadata is consistent with the result we want to submit + self.performance_results.ensure_non_stale_submission( + deps.storage, + sender, + epoch_id, + data.node_id, + )?; + + // 3. check if the node is bonded + if !self.node_bonded(deps.as_ref(), data.node_id)? { + return Err(NymPerformanceContractError::NodeNotBonded { + node_id: data.node_id, + }); + } + + // 4 insert performance data into the storage + self.performance_results + .insert_performance_data(deps.storage, epoch_id, &data)?; + + // 5. update submission metadata based on the last result we submitted + self.performance_results.update_submission_metadata( + deps.storage, + sender, + epoch_id, + data.node_id, + )?; + + // 6. update latest submitted + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: sender.clone(), + epoch_id, + data, + }), + }, + )?; + + Ok(()) + } + + pub fn batch_submit_performance_results( + &self, + deps: DepsMut, + env: Env, + sender: &Addr, + epoch_id: EpochId, + data: Vec, + ) -> Result { + // 1. check if the sender is authorised to submit performance data + self.network_monitors + .ensure_authorised(deps.storage, sender)?; + + let Some(first) = data.first() else { + // no performance data + return Ok(BatchSubmissionResult::default()); + }; + + // 2. check if current submission metadata is consistent with the first result we want to submit + self.performance_results.ensure_non_stale_submission( + deps.storage, + sender, + epoch_id, + first.node_id, + )?; + + let mut accepted_scores = 0; + let mut non_existent_nodes = Vec::new(); + + // 3. submit it + if self.node_bonded(deps.as_ref(), first.node_id)? { + self.performance_results + .insert_performance_data(deps.storage, epoch_id, first)?; + accepted_scores += 1; + } else { + non_existent_nodes.push(first.node_id); + } + + // not point in using peekable iterator, we can just keep track of the previous + // element we've seen + let mut previous = first.node_id; + + for perf in data.iter().skip(1) { + // 4. ensure provided data is sorted (if the check fails in later iteration, + // the whole tx will get reverted so it's fine to just set the storage within the same loop + if perf.node_id <= previous { + return Err(NymPerformanceContractError::UnsortedBatchSubmission); + } + previous = perf.node_id; + + // 5. insert performance data into the storage + if self.node_bonded(deps.as_ref(), perf.node_id)? { + self.performance_results + .insert_performance_data(deps.storage, epoch_id, perf)?; + accepted_scores += 1; + } else { + non_existent_nodes.push(perf.node_id); + } + } + + // SAFETY: we know this vector is not empty + #[allow(clippy::unwrap_used)] + let last = data.last().unwrap(); + + // 5. update submission metadata based on the last result we submitted + self.performance_results.update_submission_metadata( + deps.storage, + sender, + epoch_id, + last.node_id, + )?; + + // 6. update latest submitted + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: sender.clone(), + epoch_id, + data: *last, + }), + }, + )?; + + Ok(BatchSubmissionResult { + accepted_scores, + non_existent_nodes, + }) + } + + #[cfg(test)] + fn is_admin(&self, deps: Deps, addr: &Addr) -> Result { + self.contract_admin.is_admin(deps, addr).map_err(Into::into) + } + + fn ensure_is_admin(&self, deps: Deps, addr: &Addr) -> Result<(), NymPerformanceContractError> { + self.contract_admin + .assert_admin(deps, addr) + .map_err(Into::into) + } + + pub fn authorise_network_monitor( + &self, + mut deps: DepsMut, + env: &Env, + sender: &Addr, + network_monitor: Addr, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + // make sure this address is not already authorised (it'd mess up the total count) + if self + .network_monitors + .authorised + .has(deps.storage, &network_monitor) + { + return Err(NymPerformanceContractError::AlreadyAuthorised { + address: network_monitor, + }); + } + + // insert the new entry and adjust the total count + self.network_monitors + .insert_new(deps.branch(), env, sender, &network_monitor)?; + + // finally, set submission metadata to disallow this NM from submitting data for epochs before it was authorised + let current_epoch_id = self.current_mixnet_epoch_id(deps.as_ref())?; + + self.performance_results.submission_metadata.save( + deps.storage, + &network_monitor, + &NetworkMonitorSubmissionMetadata { + last_submitted_epoch_id: current_epoch_id, + last_submitted_node_id: 0, + }, + )?; + Ok(()) + } + + pub fn retire_network_monitor( + &self, + deps: DepsMut, + env: Env, + sender: &Addr, + network_monitor: Addr, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + self.network_monitors + .retire(deps, &env, sender, &network_monitor) + } + + pub fn try_load_performance( + &self, + storage: &dyn Storage, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result, NymPerformanceContractError> { + Ok(self + .performance_results + .results + .may_load(storage, (epoch_id, node_id))? + .map(|r| r.median())) + } + + pub fn remove_node_measurements( + &self, + deps: DepsMut, + sender: &Addr, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + self.performance_results + .results + .remove(deps.storage, (epoch_id, node_id)); + Ok(()) + } + + pub fn remove_epoch_measurements( + &self, + deps: DepsMut, + sender: &Addr, + epoch_id: EpochId, + ) -> Result { + self.ensure_is_admin(deps.as_ref(), sender)?; + + // 1. purge the entries according to the limit + self.performance_results.results.prefix(epoch_id).clear( + deps.storage, + Some(retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT), + ); + + // 2. see if there's anything left + let additional_entries_to_remove_remaining = !self + .performance_results + .results + .prefix(epoch_id) + .is_empty(deps.storage); + + Ok(RemoveEpochMeasurementsResponse { + additional_entries_to_remove_remaining, + }) + } +} + +pub(crate) struct NetworkMonitorsStorage { + pub(crate) authorised_count: Item, + pub(crate) authorised: Map<&'static Addr, NetworkMonitorDetails>, + pub(crate) retired: Map<&'static Addr, RetiredNetworkMonitor>, +} + +impl NetworkMonitorsStorage { + #[allow(clippy::new_without_default)] + const fn new() -> Self { + NetworkMonitorsStorage { + authorised_count: Item::new(storage_keys::AUTHORISED_COUNT), + authorised: Map::new(storage_keys::AUTHORISED), + retired: Map::new(storage_keys::RETIRED), + } + } + + fn initialise(&self, deps: DepsMut) -> Result<(), NymPerformanceContractError> { + self.authorised_count.save(deps.storage, &0)?; + Ok(()) + } + + fn ensure_authorised( + &self, + storage: &dyn Storage, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + if !self.authorised.has(storage, addr) { + return Err(NymPerformanceContractError::NotAuthorised { + address: addr.clone(), + }); + } + Ok(()) + } + + fn insert_new( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + address: &Addr, + ) -> Result<(), NymPerformanceContractError> { + // if this address has already been retired in the past, restore it + self.retired.remove(deps.storage, address); + + self.authorised_count + .update(deps.storage, |authorised_count| { + Ok::<_, StdError>(authorised_count + 1) + })?; + self.authorised.save( + deps.storage, + address, + &NetworkMonitorDetails { + address: address.clone(), + authorised_by: sender.clone(), + authorised_at_height: env.block.height, + }, + )?; + Ok(()) + } + + fn retire( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + // NOTE: if the NM hasn't been authorised before, the `load` call will fail + // and thus `authorised_count` won't be updated (nor further code executed) + let details = self.authorised.load(deps.storage, addr)?; + self.authorised.remove(deps.storage, addr); + + self.authorised_count + .update(deps.storage, |authorised_count| { + Ok::<_, StdError>(authorised_count - 1) + })?; + + self.retired + .save(deps.storage, addr, &details.retire(env, sender))?; + Ok(()) + } +} + +pub(crate) struct PerformanceResultsStorage { + pub(crate) results: Map<(EpochId, NodeId), NodeResults>, + + // in order to ensure NM does not resubmit results, we keep metadata + // of the latest submitted information + // this requires them to submit everything sorted by node_id + pub(crate) submission_metadata: Map<&'static Addr, NetworkMonitorSubmissionMetadata>, +} + +impl PerformanceResultsStorage { + #[allow(clippy::new_without_default)] + const fn new() -> Self { + PerformanceResultsStorage { + results: Map::new(storage_keys::PERFORMANCE_RESULTS), + submission_metadata: Map::new(storage_keys::SUBMISSION_METADATA), + } + } + + // note: this method assumes authorisation has been checked and invariants validated + // (such as attempting to insert stale data) + fn insert_performance_data( + &self, + storage: &mut dyn Storage, + epoch_id: EpochId, + data: &NodePerformance, + ) -> Result<(), NymPerformanceContractError> { + let performance = data.performance; + + let key = (epoch_id, data.node_id); + let updated = match self.results.may_load(storage, key)? { + None => NodeResults::new(performance), + Some(mut existing) => { + existing.insert_new(performance); + existing + } + }; + + self.results.save(storage, key, &updated)?; + Ok(()) + } + + fn update_submission_metadata( + &self, + storage: &mut dyn Storage, + address: &Addr, + last_submitted_epoch_id: EpochId, + last_submitted_node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + self.submission_metadata.save( + storage, + address, + &NetworkMonitorSubmissionMetadata { + last_submitted_epoch_id, + last_submitted_node_id, + }, + )?; + Ok(()) + } + + fn ensure_non_stale_submission( + &self, + storage: &dyn Storage, + address: &Addr, + epoch_id: EpochId, + new_node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + let last_submission = self.submission_metadata.load(storage, address)?; + + // we can't submit data for past epochs + if last_submission.last_submitted_epoch_id > epoch_id { + return Err(NymPerformanceContractError::StalePerformanceSubmission { + epoch_id, + node_id: new_node_id, + last_epoch_id: last_submission.last_submitted_epoch_id, + last_node_id: last_submission.last_submitted_node_id, + }); + } + + // if we're submitting for the same epoch, the node id has to be greater than the previous one + if last_submission.last_submitted_epoch_id == epoch_id + && last_submission.last_submitted_node_id >= new_node_id + { + return Err(NymPerformanceContractError::StalePerformanceSubmission { + epoch_id, + node_id: new_node_id, + last_epoch_id: last_submission.last_submitted_epoch_id, + last_node_id: last_submission.last_submitted_node_id, + }); + } + // if we're submitting for new epoch, node id doesn't matter + Ok(()) + } +} + +pub mod retrieval_limits { + pub const NODE_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_EPOCH_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT: u32 = 50; + pub const NODE_EPOCH_MEASUREMENTS_MAX_LIMIT: u32 = 100; + + pub const NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NETWORK_MONITORS_DEFAULT_LIMIT: u32 = 50; + pub const NETWORK_MONITORS_MAX_LIMIT: u32 = 100; + + pub const RETIRED_NETWORK_MONITORS_DEFAULT_LIMIT: u32 = 50; + pub const RETIRED_NETWORK_MONITORS_MAX_LIMIT: u32 = 100; + + pub const EPOCH_PERFORMANCE_PURGE_LIMIT: usize = 200; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod performance_contract_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt, PreInitContract}; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + + #[cfg(test)] + mod initialisation { + use super::*; + use nym_contracts_common_testing::{ArbitraryContractStorageWriter, FullReader}; + + fn initialise_storage( + pre_init: &mut PreInitContract, + admin: Option, + ) -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + let env = pre_init.env(); + let admin = admin.unwrap_or(pre_init.addr_make("admin")); + let deps = pre_init.deps_mut(); + + storage.initialise(deps, env, admin, mixnet_contract.clone(), Vec::new())?; + Ok(()) + } + + #[test] + fn sets_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + let admin1 = pre_init.api.addr_make("first-admin"); + let admin2 = pre_init.api.addr_make("second-admin"); + + initialise_storage(&mut pre_init, Some(admin1.clone()))?; + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin1).is_ok()); + + let mut pre_init = PreInitContract::new(); + initialise_storage(&mut pre_init, Some(admin2.clone()))?; + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin2).is_ok()); + + Ok(()) + } + + #[test] + fn sets_provided_mixnet_contract_address() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + + let expected_mixnet_contract_address = pre_init.mixnet_contract_address.clone(); + let deps = pre_init.deps(); + let mixnet_contract = storage.mixnet_contract_address.load(deps.storage)?; + assert_eq!(expected_mixnet_contract_address, mixnet_contract); + Ok(()) + } + + #[test] + fn sets_initial_submission_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + let env = pre_init.env(); + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + + let expected = LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + }; + let data = storage.last_performance_submission.load(deps.storage)?; + assert_eq!(expected, data); + Ok(()) + } + + #[test] + fn retrieves_initial_epoch_id_from_mixnet_contract() -> anyhow::Result<()> { + // base case + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + assert_eq!(0, storage.mixnet_epoch_id_at_creation.load(deps.storage)?); + + // non-0 epoch + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + let address = pre_init.mixnet_contract_address.clone(); + + // advance the epoch few times... + let interval_details = pre_init + .querier() + .query_current_mixnet_interval(&address)? + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch(); + + pre_init.set_contract_storage_value(&address, b"ci", &interval_details)?; + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + assert_eq!(7, storage.mixnet_epoch_id_at_creation.load(deps.storage)?); + + Ok(()) + } + + #[test] + fn authorises_provided_network_monitors() -> anyhow::Result<()> { + // no NM + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + let authorised_count = storage + .network_monitors + .authorised_count + .load(deps.storage)?; + assert_eq!(authorised_count, 0); + + let authorised = storage + .network_monitors + .authorised + .all_values(deps.storage)?; + assert!(authorised.is_empty()); + + let mut pre_init = PreInitContract::new(); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + let env = pre_init.env(); + let admin = pre_init.addr_make("admin"); + let nm1 = pre_init.addr_make("nm1"); + let nm2 = pre_init.addr_make("nm2"); + + let deps = pre_init.deps_mut(); + storage.initialise( + deps, + env.clone(), + admin.clone(), + mixnet_contract.clone(), + vec![nm1.to_string(), nm2.to_string()], + )?; + + let deps = pre_init.deps(); + let authorised_count = storage + .network_monitors + .authorised_count + .load(deps.storage)?; + assert_eq!(authorised_count, 2); + + let authorised = storage + .network_monitors + .authorised + .all_values(deps.storage)?; + + let expected = vec![ + NetworkMonitorDetails { + address: nm1, + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + NetworkMonitorDetails { + address: nm2, + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + ]; + assert_eq!(authorised, expected); + + Ok(()) + } + } + + #[test] + fn getting_current_mixnet_epoch_id() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 0); + tester.advance_mixnet_epoch()?; + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 1); + + tester.set_mixnet_epoch(1000)?; + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 1000); + + Ok(()) + } + + #[cfg(test)] + mod submitting_performance_data { + use super::*; + + #[test] + fn is_only_allowed_by_authorised_network_monitors() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm1 = tester.addr_make("network-monitor-1"); + let nm2 = tester.addr_make("network-monitor-2"); + let unauthorised = tester.addr_make("unauthorised"); + let env = tester.env(); + + tester.authorise_network_monitor(&nm1)?; + + // authorised network monitor can submit the results just fine + let perf = tester.dummy_node_performance(); + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm1, 0, perf) + .is_ok()); + + // unauthorised address is rejected + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: nm2.clone() + } + ); + + // it is fine after explicit authorisation though + tester.authorise_network_monitor(&nm2)?; + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) + .is_ok()); + + // and address that was never authorised still fails + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &unauthorised, 0, perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: unauthorised + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_same_node_again() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + // first submission + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .is_ok()); + + // second submission + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id1, + } + ); + + // another submission works fine + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) + .is_ok()); + + // original one works IF it's for next epoch + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 1, data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 1, + last_node_id: id1, + } + ); + + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_out_of_order() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id2, + } + ); + + // check across epochs + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: id1, + last_epoch_id: 10, + last_node_id: id1, + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_past_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + tester.set_mixnet_epoch(10)?; + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let env = tester.env(); + + // if NM got authorised at epoch 10, it can only submit data for epochs >=10 + let perf = tester.dummy_node_performance(); + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, perf) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, perf) + .is_ok()); + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 11, perf) + .is_ok()); + + Ok(()) + } + + #[test] + fn updates_submission_metadata() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, 0); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[0]); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[3]); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 1, + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[1]); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 12345, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 12345); + assert_eq!(metadata.last_submitted_node_id, nodes[8]); + + Ok(()) + } + + #[test] + fn updates_latest_submitted_information() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 1, + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 12345, + NodePerformance { + node_id: nodes[9], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 12345, + data: NodePerformance { + node_id: nodes[9], + performance: Default::default(), + }, + }), + } + ); + + Ok(()) + } + + #[test] + fn requires_associated_node_to_be_bonded() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let dummy_perf = NodePerformance { + node_id: 12345, + performance: Percent::from_percentage_value(69)?, + }; + + // no node bonded at this point + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + + // bonded nym-node + let node_id = tester.bond_dummy_nymnode()?; + let perf = NodePerformance { + node_id, + performance: Default::default(), + }; + let res = + storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); + assert!(res.is_ok()); + + // unbonded + tester.unbond_nymnode(node_id)?; + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + + // bonded legacy mix-node + let node_id = tester.bond_dummy_legacy_mixnode()?; + let perf = NodePerformance { + node_id, + performance: Default::default(), + }; + let res = + storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); + assert!(res.is_ok()); + + // unbonded + tester.unbond_legacy_mixnode(node_id)?; + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + Ok(()) + } + } + + #[cfg(test)] + mod batch_submitting_performance_data { + use super::*; + + #[test] + fn is_only_allowed_by_authorised_network_monitors() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm1 = tester.addr_make("network-monitor-1"); + let nm2 = tester.addr_make("network-monitor-2"); + let unauthorised = tester.addr_make("unauthorised"); + let env = tester.env(); + + tester.authorise_network_monitor(&nm1)?; + + let perf = tester.dummy_node_performance(); + // authorised network monitor can submit the results just fine + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm1, + 0, + vec![perf] + ) + .is_ok()); + + // unauthorised address is rejected + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf], + ) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: nm2.clone() + } + ); + + // it is fine after explicit authorisation though + tester.authorise_network_monitor(&nm2)?; + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf] + ) + .is_ok()); + + // and address that was never authorised still fails + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &unauthorised, + 0, + vec![perf], + ) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: unauthorised + } + ); + Ok(()) + } + + #[test] + fn requires_sorted_list_of_performances() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let id3 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + let more_data = NodePerformance { + node_id: id3, + performance: Percent::hundred(), + }; + + let duplicates = vec![data, data]; + let another_dups = vec![another_data, another_data]; + let unsorted = vec![another_data, data]; + let semi_sorted = vec![data, more_data, another_data]; + let sorted = vec![data, another_data, more_data]; + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + duplicates, + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + another_dups, + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + unsorted, + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + semi_sorted, + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + sorted + ) + .is_ok()); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_same_node_again() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + // first submission + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data] + ) + .is_ok()); + + // second submission + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id1, + } + ); + + // another submission works fine + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) + .is_ok()); + + // original one works IF it's for next epoch + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![data] + ) + .is_ok()); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 1, + last_node_id: id1, + } + ); + + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_out_of_order() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) + .is_ok()); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id2, + } + ); + + // check across epochs + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![data] + ) + .is_ok()); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 9, + vec![data], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: id1, + last_epoch_id: 10, + last_node_id: id1, + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_past_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + tester.set_mixnet_epoch(10)?; + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let perf = tester.dummy_node_performance(); + + // if NM got authorised at epoch 10, it can only submit data for epochs >=10 + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![perf], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 9, + vec![perf], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![perf] + ) + .is_ok()); + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 11, + vec![perf] + ) + .is_ok()); + + Ok(()) + } + + #[test] + fn updates_submission_metadata() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, 0); + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + // single submission + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[0]); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[1]); + + // multiple submissions + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + ], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[4]); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 2, + vec![ + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + ], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 2); + assert_eq!(metadata.last_submitted_node_id, nodes[8]); + + Ok(()) + } + + #[test] + fn updates_latest_submitted_information() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + // single submission + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + }), + } + ); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + }), + } + ); + + // multiple submissions + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + ], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + }), + } + ); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 2, + vec![ + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[7], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + ], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 2, + data: NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + }), + } + ); + + Ok(()) + } + + #[test] + fn informs_if_associated_node_is_not_bonded() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + // bond and unbond some nodes to advance the id counter + for _ in 0..10 { + let node_id = tester.bond_dummy_nymnode()?; + tester.unbond_nymnode(node_id)?; + } + + let nym_node1 = tester.bond_dummy_nymnode()?; + let nym_node_between = tester.bond_dummy_nymnode()?; + tester.unbond_nymnode(nym_node_between)?; + let nym_node2 = tester.bond_dummy_nymnode()?; + + let mix_node1 = tester.bond_dummy_legacy_mixnode()?; + let mixnode_between = tester.bond_dummy_legacy_mixnode()?; + tester.unbond_legacy_mixnode(mixnode_between)?; + let mix_node2 = tester.bond_dummy_legacy_mixnode()?; + + let env = tester.env(); + + // single id - nothing bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![NodePerformance { + node_id: 999999, + performance: Default::default(), + }], + )?; + assert_eq!(res.accepted_scores, 0); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // one bonded nym-node, one not bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: 999999, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 1); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // not-bonded, bonded, not-bonded, bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 2, + vec![ + NodePerformance { + node_id: 2, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node_between, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![2, nym_node_between]); + + // MIXNODES + + // one bonded mixnode, one not bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 3, + vec![ + NodePerformance { + node_id: mix_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: 999999, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 1); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // not-bonded, bonded, not-bonded, bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 4, + vec![ + NodePerformance { + node_id: 2, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: mixnode_between, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![2, mixnode_between]); + + // nym-node, not bonded, mixnode + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 5, + vec![ + NodePerformance { + node_id: 3, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node_between, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![3, nym_node_between]); + + Ok(()) + } + } + + #[test] + fn checking_for_admin() -> anyhow::Result<()> { + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + let admin = pre_init.api.addr_make("admin"); + let non_admin = pre_init.api.addr_make("non-admin"); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + + let storage = NymPerformanceContractStorage::new(); + + let deps = pre_init.deps_mut(); + storage.initialise(deps, env, admin.clone(), mixnet_contract, Vec::new())?; + + let deps = pre_init.deps(); + assert!(storage.is_admin(deps, &admin)?); + assert!(!storage.is_admin(deps, &non_admin)?); + + Ok(()) + } + + #[test] + fn ensuring_admin_privileges() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + + let admin = pre_init.api.addr_make("admin"); + let non_admin = pre_init.api.addr_make("non-admin"); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + + let deps = pre_init.deps_mut(); + storage.initialise(deps, env, admin.clone(), mixnet_contract, Vec::new())?; + + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin).is_ok()); + assert!(storage.ensure_is_admin(deps, &non_admin).is_err()); + + Ok(()) + } + + #[cfg(test)] + mod authorising_network_monitor { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::AdminExt; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, ¬_admin, nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, nm) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + let another_nm = tester.addr_make("another-network-monitor"); + + // old one no longer works + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, another_nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .authorise_network_monitor(tester.deps_mut(), &env, &new_admin, another_nm) + .is_ok()); + Ok(()) + } + + #[test] + fn network_monitor_must_not_already_be_authorised() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone()) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::AlreadyAuthorised { address: nm } + ); + + Ok(()) + } + + #[test] + fn for_valid_network_monitor_storage_is_updated() -> anyhow::Result<()> { + // note: detailed invariants are checked in network_monitors_storage + // here we just want to ensure **something** happens (i.e. `insert_new` is called) + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 0); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 1); + + Ok(()) + } + + #[test] + fn initial_metadata_uses_current_mixnet_epoch() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm1.clone())?; + assert_eq!( + 0, + storage + .performance_results + .submission_metadata + .load(&tester, &nm1)? + .last_submitted_epoch_id + ); + + tester.advance_mixnet_epoch()?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm2.clone())?; + assert_eq!( + 1, + storage + .performance_results + .submission_metadata + .load(&tester, &nm2)? + .last_submitted_epoch_id + ); + + tester.set_mixnet_epoch(1000)?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm3.clone())?; + assert_eq!( + 1000, + storage + .performance_results + .submission_metadata + .load(&tester, &nm3)? + .last_submitted_epoch_id + ); + + Ok(()) + } + } + + #[cfg(test)] + mod retiring_network_monitor { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::AdminExt; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let another_nm = tester.addr_make("another-network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + storage.authorise_network_monitor( + tester.deps_mut(), + &env, + &admin, + another_nm.clone(), + )?; + + let res = storage + .retire_network_monitor(tester.deps_mut(), env.clone(), ¬_admin, nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .retire_network_monitor(tester.deps_mut(), env.clone(), &admin, nm) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .retire_network_monitor( + tester.deps_mut(), + env.clone(), + &admin, + another_nm.clone(), + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .retire_network_monitor(tester.deps_mut(), env, &new_admin, another_nm) + .is_ok()); + + Ok(()) + } + + #[test] + fn for_valid_network_monitor_storage_is_updated() -> anyhow::Result<()> { + // note: detailed invariants are checked in network_monitors_storage + // here we just want to ensure **something** happens (i.e. `retire` is called) + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 1); + + storage.retire_network_monitor(tester.deps_mut(), env, &admin, nm)?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 0); + + Ok(()) + } + } + + #[test] + fn loading_performance_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let admin = tester.admin_unchecked(); + let mut nms = Vec::new(); + + // pre-authorise some network monitors + for i in 0..6 { + let env = tester.env(); + let nm = tester.addr_make(&format!("network-monitor{i}")); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + nms.push(nm); + } + + // no results + let node_id = tester.bond_dummy_nymnode()?; + assert_eq!(storage.try_load_performance(&tester, 0, node_id)?, None); + + // + // always returns median value with 2decimal places precision + // + + // single result + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.42")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.42" + ); + + // two results (median doesn't require changing decimal places) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.50")?; + tester.insert_raw_performance(&nms[1], node_id, "0.40")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.45" + ); + + // two results (median requires changing decimal places) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.58")?; + tester.insert_raw_performance(&nms[1], node_id, "0.45")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.52" + ); + + // three results (median is the middle value rather than the average) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.12")?; + tester.insert_raw_performance(&nms[1], node_id, "0.34")?; + tester.insert_raw_performance(&nms[2], node_id, "0.56")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.34" + ); + + // five results (notice how they're not inserted sorted) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.9")?; + tester.insert_raw_performance(&nms[1], node_id, "0.9")?; + tester.insert_raw_performance(&nms[2], node_id, "0.1")?; + tester.insert_raw_performance(&nms[4], node_id, "0.1")?; + tester.insert_raw_performance(&nms[5], node_id, "0.7")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.7" + ); + + // six results (same as above, but average of middle values) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.9")?; + tester.insert_raw_performance(&nms[1], node_id, "0.9")?; + tester.insert_raw_performance(&nms[2], node_id, "0.1")?; + tester.insert_raw_performance(&nms[3], node_id, "0.1")?; + tester.insert_raw_performance(&nms[4], node_id, "0.2")?; + tester.insert_raw_performance(&nms[5], node_id, "0.3")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.25" + ); + + Ok(()) + } + + #[cfg(test)] + mod removing_node_measurements { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::FullReader; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let epoch_id = 0; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + let res = storage + .remove_node_measurements(tester.deps_mut(), ¬_admin, epoch_id, id1) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id2) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_node_measurements(tester.deps_mut(), &new_admin, epoch_id, id2) + .is_ok()); + + Ok(()) + } + + #[test] + fn is_noop_if_entry_didnt_exist() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let epoch_id = 0; + let node_id = 0; + + let before = storage.performance_results.results.all_values(&tester)?; + assert!(before.is_empty()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, node_id)?; + + let after = storage.performance_results.results.all_values(&tester)?; + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn removes_the_underlying_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + let epoch_id = 0; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm1.clone())?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm2.clone())?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm3.clone())?; + + // single measurement + tester.insert_raw_performance(&nm1, id1, "0.42")?; + + let before = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id1))?; + assert!(before.is_some()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1)?; + + let after = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id1))?; + assert!(after.is_none()); + + // multiple measurements + tester.insert_raw_performance(&nm1, id2, "0.42")?; + tester.insert_raw_performance(&nm2, id2, "0.69")?; + tester.insert_raw_performance(&nm3, id2, "1")?; + + let before = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id2))?; + assert!(before.is_some()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id2)?; + + let after = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id2))?; + assert!(after.is_none()); + + Ok(()) + } + } + + #[cfg(test)] + mod removing_epoch_measurements { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::FullReader; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + // epoch 0 + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + // epoch 1 + tester.advance_mixnet_epoch()?; + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + let res = storage + .remove_epoch_measurements(tester.deps_mut(), ¬_admin, 0) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_epoch_measurements(tester.deps_mut(), &admin, 0) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .remove_epoch_measurements(tester.deps_mut(), &admin, 1) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_epoch_measurements(tester.deps_mut(), &new_admin, 1) + .is_ok()); + + Ok(()) + } + + #[test] + fn is_noop_for_empty_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let epoch_id = 0; + + let before = storage.performance_results.results.all_values(&tester)?; + assert!(before.is_empty()); + + storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + + let after = storage.performance_results.results.all_values(&tester)?; + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn removes_the_underlying_data_below_limit() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + + let env = tester.env(); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + // just few entries + let epoch_id = 0; + for _ in 0..10 { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let before = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + assert_eq!(before.len(), 10); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + // EXACT limit + let epoch_id = 1; + tester.advance_mixnet_epoch()?; + for _ in 0..retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn indicates_need_for_further_calls_above_limit() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + + let env = tester.env(); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + // just few entries + let epoch_id = 0; + for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let before = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + assert_eq!( + before.len(), + 2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 + ); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert_eq!( + after.len(), + retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 + ); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert_eq!(after.len(), 50); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + Ok(()) + } + } + } + + #[cfg(test)] + mod network_monitors_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + + #[test] + fn inserting_new_entry() -> anyhow::Result<()> { + let main_storage = NymPerformanceContractStorage::new(); + + let storage = NetworkMonitorsStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm1) + .is_ok()); + + // total authorised count is incremented + assert_eq!(storage.authorised_count.load(&tester)?, 1); + + // its current data is saved + assert_eq!( + storage.authorised.load(&tester, &nm1)?, + NetworkMonitorDetails { + address: nm1.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + } + ); + + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm2) + .is_ok()); + + assert_eq!(storage.authorised_count.load(&tester)?, 2); + assert_eq!( + storage.authorised.load(&tester, &nm2)?, + NetworkMonitorDetails { + address: nm2.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + } + ); + + main_storage.retire_network_monitor( + tester.deps_mut(), + env.clone(), + &admin, + nm1.clone(), + )?; + assert!(storage.retired.may_load(&tester, &nm1)?.is_some()); + + // if it was previously retired, that information is purged + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm1) + .is_ok()); + + assert!(storage.retired.may_load(&tester, &nm1)?.is_none()); + + Ok(()) + } + + #[test] + fn retiring_existing_monitor() -> anyhow::Result<()> { + let storage = NetworkMonitorsStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + + tester.authorise_network_monitor(&nm1)?; + tester.authorise_network_monitor(&nm2)?; + + // fails on unauthorised NMs + assert!(storage + .retire(tester.deps_mut(), &env, &admin, &nm3) + .is_err()); + + assert_eq!(storage.authorised_count.load(&tester)?, 2); + + storage.retire(tester.deps_mut(), &env, &admin, &nm1)?; + + // total authorised count is decremented + assert_eq!(storage.authorised_count.load(&tester)?, 1); + + // data is removed + assert!(storage.authorised.may_load(&tester, &nm1)?.is_none()); + assert_eq!( + storage.retired.load(&tester, &nm1)?, + RetiredNetworkMonitor { + details: NetworkMonitorDetails { + address: nm1.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + retired_by: admin.clone(), + retired_at_height: env.block.height, + } + ); + + storage.retire(tester.deps_mut(), &env, &admin, &nm2)?; + + assert_eq!(storage.authorised_count.load(&tester)?, 0); + assert!(storage.authorised.may_load(&tester, &nm2)?.is_none()); + assert_eq!( + storage.retired.load(&tester, &nm2)?, + RetiredNetworkMonitor { + details: NetworkMonitorDetails { + address: nm2.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + retired_by: admin.clone(), + retired_at_height: env.block.height, + } + ); + + Ok(()) + } + } + + #[cfg(test)] + mod performance_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::ContractOpts; + use std::str::FromStr; + + #[test] + fn inserting_new_entry() -> anyhow::Result<()> { + // essentially make sure there are no silly bugs that epoch_id and node_id got accidentally mixed up + // when constructing map key... + let storage = PerformanceResultsStorage::new(); + let mut tester = init_contract_tester(); + + let node_id1 = 123; + let node_id2 = 456; + + let data1 = NodePerformance { + node_id: node_id1, + performance: Percent::from_str("0.23")?, + }; + + let data2 = NodePerformance { + node_id: node_id1, + performance: Percent::hundred(), + }; + + let data3 = NodePerformance { + node_id: node_id2, + performance: Percent::from_str("0.23643634")?, + }; + + let data4 = NodePerformance { + node_id: node_id2, + performance: Percent::hundred(), + }; + + assert!(storage.results.may_load(&tester, (1, node_id1))?.is_none()); + assert!(storage.results.may_load(&tester, (1, node_id2))?.is_none()); + + storage.insert_performance_data(&mut tester, 1, &data1)?; + assert_eq!( + tester.read_raw_scores(1, node_id1)?.inner(), + &[data1.performance] + ); + storage.insert_performance_data(&mut tester, 1, &data2)?; + assert_eq!( + tester.read_raw_scores(1, node_id1)?.inner(), + &[data1.performance, data2.performance] + ); + + storage.insert_performance_data(&mut tester, 1, &data3)?; + assert_eq!( + tester.read_raw_scores(1, node_id2)?.inner(), + &[data3.performance.round_to_two_decimal_places()] + ); + storage.insert_performance_data(&mut tester, 1, &data4)?; + assert_eq!( + tester.read_raw_scores(1, node_id2)?.inner(), + &[ + data3.performance.round_to_two_decimal_places(), + data4.performance + ] + ); + + storage.insert_performance_data(&mut tester, 2, &data2)?; + storage.insert_performance_data(&mut tester, 2, &data2)?; + assert_eq!( + tester.read_raw_scores(2, node_id1)?.inner(), + &[data2.performance, data2.performance] + ); + + storage.insert_performance_data(&mut tester, 2, &data4)?; + storage.insert_performance_data(&mut tester, 2, &data4)?; + assert_eq!( + tester.read_raw_scores(2, node_id2)?.inner(), + &[data4.performance, data4.performance] + ); + + Ok(()) + } + + #[test] + fn checking_for_submission_staleness() -> anyhow::Result<()> { + let storage = PerformanceResultsStorage::new(); + let mut tester = init_contract_tester(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let id3 = tester.bond_dummy_nymnode()?; + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + tester.insert_epoch_performance(&nm, 2, id2, Percent::hundred())?; + + // illegal to submit anything < than last used epoch + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 0, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1, id3) + .is_err()); + + // for the current epoch, node id has to be greater than what has already been submitted + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id1) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id3) + .is_ok()); + + // and anything for future epochs is fine (as long as it's the first entry) + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 3, id1) + .is_ok()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 3, id2) + .is_ok()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1111, id3) + .is_ok()); + + Ok(()) + } + } +} diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs new file mode 100644 index 0000000000..4f8e7cf4bd --- /dev/null +++ b/contracts/performance/src/testing/mod.rs @@ -0,0 +1,608 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::{execute, instantiate, migrate, query}; +use crate::helpers::MixnetContractQuerier; +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use cosmwasm_std::testing::{message_info, mock_env, MockApi}; +use cosmwasm_std::{ + coin, coins, Addr, Binary, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, + StdError, StdResult, +}; +use cw_storage_plus::PrimaryKey; +use mixnet_contract::testable_mixnet_contract::MixnetContract; +use nym_contracts_common::signing::{ContractMessageContent, MessageSignature}; +use nym_contracts_common::Percent; +use nym_contracts_common_testing::{ + addr, AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, + ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractStorageWrapper, ContractTester, + ContractTesterBuilder, DenomExt, PermissionedFn, QueryFn, RandExt, TestableNymContract, + TEST_DENOM, +}; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role}; +use nym_mixnet_contract_common::{ + CurrentIntervalResponse, EpochId, Interval, MixNode, MixNodeBond, MixnodeDetailsResponse, + NodeCostParams, NodeRewarding, NymNode, NymNodeBondingPayload, RoleAssignment, + SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_performance_contract_common::constants::storage_keys; +use nym_performance_contract_common::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformance, NodeResults, + NymPerformanceContractError, QueryMsg, +}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +pub struct PerformanceContract; + +impl TestableNymContract for PerformanceContract { + const NAME: &'static str = "performance-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = NymPerformanceContractError; + + fn instantiate() -> ContractFn { + instantiate + } + + fn execute() -> ContractFn { + execute + } + + fn query() -> QueryFn { + query + } + + fn migrate() -> PermissionedFn { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + InstantiateMsg { + mixnet_contract_address: addr("mixnet-contract").to_string(), + authorised_network_monitors: vec![], + } + } + + fn init() -> ContractTester + where + Self: Sized, + { + let builder = ContractTesterBuilder::new().instantiate::(None); + + // we just instantiated it + let mixnet_address = builder + .well_known_contracts + .get(MixnetContract::NAME) + .unwrap() + .clone(); + + builder + .instantiate::(Some(InstantiateMsg { + mixnet_contract_address: mixnet_address.to_string(), + authorised_network_monitors: vec![], + })) + .build() + } +} + +pub fn init_contract_tester() -> ContractTester { + PerformanceContract::init() + .with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN) +} + +// we need to be able to test instantiation, but for that we require +// deps in a state that already includes instantiated mixnet contract +pub(crate) struct PreInitContract { + tester_builder: ContractTesterBuilder, + pub(crate) mixnet_contract_address: Addr, + pub(crate) api: MockApi, + storage: ContractStorageWrapper, + placeholder_address: Addr, +} + +#[allow(dead_code)] +impl PreInitContract { + pub(crate) fn new() -> PreInitContract { + let tester_builder = + ContractTesterBuilder::::new().instantiate::(None); + + let mixnet_contract = tester_builder + .well_known_contracts + .get(&MixnetContract::NAME) + .unwrap(); + + let api = tester_builder.api(); + let placeholder_address = api.addr_make("to-be-performance-contract"); + + let storage = tester_builder.contract_storage_wrapper(&placeholder_address); + + PreInitContract { + mixnet_contract_address: mixnet_contract.clone(), + tester_builder, + api, + storage, + placeholder_address, + } + } + + pub(crate) fn deps(&self) -> Deps { + Deps { + storage: &self.storage, + api: &self.api, + querier: self.tester_builder.querier(), + } + } + + pub(crate) fn deps_mut(&mut self) -> DepsMut { + DepsMut { + storage: &mut self.storage, + api: &self.api, + querier: self.tester_builder.querier(), + } + } + + pub(crate) fn querier(&self) -> QuerierWrapper { + self.tester_builder.querier() + } + + pub(crate) fn env(&self) -> Env { + Env { + contract: ContractInfo { + address: self.placeholder_address.clone(), + }, + ..mock_env() + } + } + + pub(crate) fn addr_make(&self, input: &str) -> Addr { + self.api.addr_make(input) + } + + pub(crate) fn write_to_mixnet_contract_storage( + &mut self, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) -> StdResult<()> { + let address = NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage)?; + + self.set_contract_storage(address, key, value); + Ok(()) + } + + pub(crate) fn write_to_mixnet_contract_storage_value( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + let address = NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage)?; + + self.set_contract_storage_value(address, key, value) + } +} + +impl ArbitraryContractStorageWriter for PreInitContract { + fn set_contract_storage( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + self.storage + .as_inner_storage_mut() + .set_contract_storage(address, key, value); + } +} + +#[allow(dead_code)] +pub(crate) trait PerformanceContractTesterExt: + ContractOpts< + ExecuteMsg = ExecuteMsg, + QueryMsg = QueryMsg, + ContractError = NymPerformanceContractError, + > + ChainOpts + + AdminExt + + DenomExt + + RandExt + + BankExt + + ArbitraryContractStorageReader + + ArbitraryContractStorageWriter +{ + fn mixnet_contract_address(&self) -> StdResult { + NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage) + } + + fn execute_mixnet_contract( + &mut self, + sender: MessageInfo, + msg: &nym_mixnet_contract_common::ExecuteMsg, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + self.execute_arbitrary_contract(address, sender, msg) + .map_err(|err| { + StdError::generic_err(format!("mixnet contract execution failure: {err}")) + })?; + Ok(()) + } + + fn read_from_mixnet_contract_storage( + &self, + key: impl AsRef<[u8]>, + ) -> StdResult { + let address = self.mixnet_contract_address()?; + + self.must_read_value_from_contract_storage(address, key) + } + + fn write_to_mixnet_contract_storage( + &mut self, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + ::set_contract_storage(self, address, key, value); + Ok(()) + } + + fn write_to_mixnet_contract_storage_value( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + self.set_contract_storage_value(address, key, value) + } + + fn current_mixnet_epoch(&self) -> StdResult { + let address = self.mixnet_contract_address()?; + + Ok(self + .deps() + .querier + .query_current_mixnet_interval(address.clone())? + .current_epoch_absolute_id()) + } + + fn advance_mixnet_epoch(&mut self) -> StdResult<()> { + let interval_details: CurrentIntervalResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetCurrentIntervalDetails {}, + )?; + let until_end = interval_details.time_until_current_epoch_end().as_secs(); + let timestamp = self.env().block.time.plus_seconds(until_end + 1); + self.set_block_time(timestamp); + self.next_block(); + + // this was hardcoded in mixnet init + let mixnet_rewarder = self.addr_make("rewarder"); + let rewarder = message_info(&mixnet_rewarder, &[]); + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::BeginEpochTransition {}, + )?; + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::ReconcileEpochEvents { limit: None }, + )?; + + for role in [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ] { + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role, + nodes: vec![], + }, + }, + )?; + } + Ok(()) + } + + fn set_mixnet_epoch(&mut self, epoch_id: EpochId) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + let interval = self + .deps() + .querier + .query_current_mixnet_interval(address.clone())?; + + let mut to_update = if interval.current_epoch_absolute_id() <= epoch_id { + interval + } else { + Interval::init_interval( + interval.epochs_in_interval(), + interval.epoch_length(), + &mock_env(), + ) + }; + + let current = to_update.current_epoch_absolute_id(); + let diff = epoch_id - current; + for _ in 0..diff { + to_update = to_update.advance_epoch(); + } + self.set_contract_storage_value(&address, b"ci", &to_update) + } + + fn authorise_network_monitor( + &mut self, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + let admin = self.admin_unchecked(); + self.execute_raw( + admin, + ExecuteMsg::AuthoriseNetworkMonitor { + address: addr.to_string(), + }, + )?; + Ok(()) + } + + fn dummy_node_performance(&mut self) -> NodePerformance { + let node_id = self.bond_dummy_nymnode().unwrap(); + NodePerformance { + node_id, + performance: Percent::from_percentage_value(69).unwrap(), + } + } + + fn retire_network_monitor(&mut self, addr: &Addr) -> Result<(), NymPerformanceContractError> { + let admin = self.admin_unchecked(); + self.execute_raw( + admin, + ExecuteMsg::RetireNetworkMonitor { + address: addr.to_string(), + }, + )?; + Ok(()) + } + + fn insert_epoch_performance( + &mut self, + addr: &Addr, + epoch_id: EpochId, + node_id: NodeId, + performance: Percent, + ) -> Result<(), NymPerformanceContractError> { + let env = self.env(); + NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( + self.deps_mut(), + env, + addr, + epoch_id, + NodePerformance { + node_id, + performance, + }, + ) + } + + fn insert_performance( + &mut self, + addr: &Addr, + node_id: NodeId, + performance: Percent, + ) -> Result<(), NymPerformanceContractError> { + let epoch_id = self.current_mixnet_epoch()?; + + self.insert_epoch_performance(addr, epoch_id, node_id, performance) + } + + // makes testing easier + fn insert_raw_performance( + &mut self, + addr: &Addr, + node_id: NodeId, + raw: &str, + ) -> Result<(), NymPerformanceContractError> { + self.insert_performance( + addr, + node_id, + Percent::from_str(raw).map_err(|err| { + NymPerformanceContractError::StdErr(StdError::parse_err("Percent", err.to_string())) + })?, + ) + } + + fn read_raw_scores( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result { + let scores = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .load(self.deps().storage, (epoch_id, node_id))?; + Ok(scores) + } + + fn bond_dummy_nymnode(&mut self) -> Result { + let node_owner = self.generate_account_with_balance(); + let pledge = coins(100_000000, TEST_DENOM); + let keypair = ed25519::KeyPair::new(self.raw_rng()); + let identity_key = keypair.public_key().to_base58_string(); + + let node = NymNode { + host: "1.2.3.4".to_string(), + custom_http_port: None, + identity_key, + }; + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM), + }; + // initial signing nonce is 0 for a new address + let signing_nonce = 0; + + let payload = NymNodeBondingPayload::new(node.clone(), cost_params.clone()); + let content = ContractMessageContent::new(node_owner.clone(), pledge.clone(), payload); + let msg = SignableNymNodeBondingMsg::new(signing_nonce, content); + + let owner_signature = keypair.private_key().sign(msg.to_plaintext()?); + let owner_signature = MessageSignature::from(owner_signature.to_bytes().as_ref()); + + self.execute_mixnet_contract( + message_info(&node_owner, &pledge), + &nym_mixnet_contract_common::ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + }, + )?; + + let bond: NodeOwnershipResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetOwnedNymNode { + address: node_owner.to_string(), + }, + )?; + + Ok(bond.details.unwrap().bond_information.node_id) + } + + fn unbond_nymnode(&mut self, node_id: NodeId) -> Result<(), NymPerformanceContractError> { + let bond: NodeDetailsResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetNymNodeDetails { node_id }, + )?; + + let node_owner = bond.details.unwrap().bond_information.owner; + + self.execute_mixnet_contract( + message_info(&node_owner, &[]), + &nym_mixnet_contract_common::ExecuteMsg::UnbondNymNode {}, + )?; + + self.advance_mixnet_epoch()?; + Ok(()) + } + + fn bond_dummy_legacy_mixnode(&mut self) -> Result { + #[derive(Deserialize, Serialize)] + pub(crate) struct UniqueRef { + // note, we collapse the pk - combining everything under the namespace - even if it is composite + pk: Binary, + value: T, + } + + // there's no proper Execute flow for this anymore, so we have to "hack" the storage a bit, + // ensuring all invariants still hold + let owner = self.generate_account_with_balance(); + + let mixnode = MixNode { + host: "1.2.3.4".to_string(), + mix_port: 123, + verloc_port: 123, + http_api_port: 123, + sphinx_key: "aaaa".to_string(), + identity_key: "bbbbb".to_string(), + version: "ccc".to_string(), + }; + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM), + }; + + // adjust node counter + let node_id_counter: u32 = self.read_from_mixnet_contract_storage("nic")?; + let node_id = node_id_counter + 1; + self.write_to_mixnet_contract_storage_value("nic", &node_id)?; + + let current_epoch = self.current_mixnet_epoch()?; + let pledge = coin(100_000000, TEST_DENOM); + let mixnode_rewarding = + NodeRewarding::initialise_new(cost_params, &pledge, current_epoch).unwrap(); + let env = self.env(); + let mixnode_bond = MixNodeBond { + mix_id: node_id, + owner, + original_pledge: pledge, + mix_node: mixnode, + proxy: None, + bonding_height: env.block.height, + is_unbonding: false, + }; + + // save to the main mixnode storage + self.set_contract_map_value( + self.mixnet_contract_address()?, + "mnn", + node_id, + &mixnode_bond, + )?; + // update indices + let pk = node_id.joined_key(); + let unique_ref = UniqueRef { + pk: pk.into(), + value: mixnode_bond.clone(), + }; + + // owner index + let idx = mixnode_bond.owner.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mno", idx, &unique_ref)?; + + // identity key index + let idx = mixnode_bond.mix_node.identity_key.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mni", idx, &unique_ref)?; + + // sphinx key index + let idx = mixnode_bond.mix_node.sphinx_key.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mns", idx, &unique_ref)?; + + // update rewarding data + self.set_contract_map_value( + self.mixnet_contract_address()?, + "mnr", + node_id, + &mixnode_rewarding, + )?; + + Ok(node_id) + } + + fn unbond_legacy_mixnode( + &mut self, + node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + let bond: MixnodeDetailsResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetMixnodeDetails { mix_id: node_id }, + )?; + + let node_owner = bond.mixnode_details.unwrap().bond_information.owner; + + self.execute_mixnet_contract( + message_info(&node_owner, &[]), + &nym_mixnet_contract_common::ExecuteMsg::UnbondMixnode {}, + )?; + + self.advance_mixnet_epoch()?; + Ok(()) + } +} + +impl PerformanceContractTesterExt for ContractTester {} diff --git a/contracts/performance/src/transactions.rs b/contracts/performance/src/transactions.rs new file mode 100644 index 0000000000..86a2d99772 --- /dev/null +++ b/contracts/performance/src/transactions.rs @@ -0,0 +1,314 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use cosmwasm_std::{to_json_binary, DepsMut, Env, Event, MessageInfo, Response}; +use nym_performance_contract_common::{ + EpochId, NodeId, NodePerformance, NymPerformanceContractError, +}; + +pub fn try_update_contract_admin( + deps: DepsMut<'_>, + info: MessageInfo, + new_admin: String, +) -> Result { + let new_admin = deps.api.addr_validate(&new_admin)?; + + let res = NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .execute_update_admin(deps, info, Some(new_admin))?; + + Ok(res) +} + +pub fn try_submit_performance_results( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + epoch_id: EpochId, + data: NodePerformance, +) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( + deps, + env, + &info.sender, + epoch_id, + data, + )?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_batch_submit_performance_results( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + epoch_id: EpochId, + data: Vec, +) -> Result { + let res = NYM_PERFORMANCE_CONTRACT_STORAGE.batch_submit_performance_results( + deps, + env, + &info.sender, + epoch_id, + data, + )?; + + let response = Response::new().set_data(to_json_binary(&res)?).add_event( + Event::new("batch_performance_submission") + .add_attribute("accepted_scores", res.accepted_scores.to_string()) + .add_attribute( + "non_existent_nodes", + format!("{:?}", res.non_existent_nodes), + ), + ); + Ok(response) +} + +pub fn try_authorise_network_monitor( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + address: String, +) -> Result { + let address = deps.api.addr_validate(&address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.authorise_network_monitor( + deps, + &env, + &info.sender, + address, + )?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_retire_network_monitor( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + address: String, +) -> Result { + let address = deps.api.addr_validate(&address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.retire_network_monitor(deps, env, &info.sender, address)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_remove_node_measurements( + deps: DepsMut<'_>, + info: MessageInfo, + epoch_id: EpochId, + node_id: NodeId, +) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE.remove_node_measurements( + deps, + &info.sender, + epoch_id, + node_id, + )?; + + Ok(Response::new()) +} + +pub fn try_remove_epoch_measurements( + deps: DepsMut<'_>, + info: MessageInfo, + epoch_id: EpochId, +) -> Result { + let res = + NYM_PERFORMANCE_CONTRACT_STORAGE.remove_epoch_measurements(deps, &info.sender, epoch_id)?; + + Ok(Response::new().set_data(to_json_binary(&res)?)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::retrieval_limits; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use cosmwasm_std::from_json; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + use nym_performance_contract_common::RemoveEpochMeasurementsResponse; + + #[cfg(test)] + mod updating_contract_admin { + use super::*; + use crate::testing::init_contract_tester; + use cw_controllers::AdminError; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + use nym_performance_contract_common::ExecuteMsg; + + #[test] + fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let random_acc = test.generate_account(); + let new_admin = test.generate_account(); + let res = test + .execute_raw( + random_acc, + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::Admin(AdminError::NotAdmin {}) + ); + + let actual_admin = test.admin_unchecked(); + let res = test.execute_raw( + actual_admin.clone(), + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + ); + assert!(res.is_ok()); + + let updated_admin = test.admin_unchecked(); + assert_eq!(new_admin, updated_admin); + + Ok(()) + } + + #[test] + fn requires_providing_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_account = "definitely-not-valid-account"; + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::UpdateAdmin { + admin: bad_account.to_string(), + }, + ); + + assert!(res.is_err()); + + let empty_account = ""; + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::UpdateAdmin { + admin: empty_account.to_string(), + }, + ); + + assert!(res.is_err()); + + Ok(()) + } + } + + #[cfg(test)] + mod authorising_network_monitor { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + + #[test] + fn requires_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_address = "foomp".to_string(); + let good_address = test.generate_account(); + + let env = test.env(); + let admin = test.admin_msg(); + + assert!(try_authorise_network_monitor( + test.deps_mut(), + env.clone(), + admin.clone(), + bad_address + ) + .is_err()); + assert!(try_authorise_network_monitor( + test.deps_mut(), + env, + admin, + good_address.to_string() + ) + .is_ok()); + + Ok(()) + } + } + + #[cfg(test)] + mod retiring_network_monitor { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + + #[test] + fn requires_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_address = "foomp".to_string(); + let good_address = test.generate_account(); + test.authorise_network_monitor(&good_address)?; + + let env = test.env(); + let admin = test.admin_msg(); + + assert!(try_retire_network_monitor( + test.deps_mut(), + env.clone(), + admin.clone(), + bad_address + ) + .is_err()); + assert!(try_retire_network_monitor( + test.deps_mut(), + env, + admin, + good_address.to_string() + ) + .is_ok()); + + Ok(()) + } + } + + // panics in tests are fine... + #[allow(clippy::panic)] + #[test] + fn removing_epoch_measurements_returns_binary_data() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + tester.advance_mixnet_epoch()?; + for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let admin = tester.admin_msg(); + let res = try_remove_epoch_measurements(tester.deps_mut(), admin.clone(), 0)?; + + let Some(data) = res.data else { + panic!("missing binary response"); + }; + let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?; + assert!(!deserialised.additional_entries_to_remove_remaining); + + let res = try_remove_epoch_measurements(tester.deps_mut(), admin, 1)?; + + let Some(data) = res.data else { + panic!("missing binary response"); + }; + let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?; + assert!(deserialised.additional_entries_to_remove_remaining); + + Ok(()) + } +} diff --git a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json index 060eb96030..8d1b16afe2 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json @@ -5,7 +5,7 @@ }, "mixmining_reserve": { "denom": "unym", - "amount": "187227500568584" + "amount": "185782781887190" }, "vesting_tokens": { "denom": "unym", @@ -13,6 +13,6 @@ }, "circulating_supply": { "denom": "unym", - "amount": "812772499431416" + "amount": "814217218112810" } } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md index 679f542c4e..0b3ee43ca0 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md @@ -1 +1 @@ -812_772_499 +814_217_218 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md index cb66fed878..788d05cd23 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md @@ -1 +1 @@ -5_200 +5_160 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md index 39bbb86bce..fe3aa06e04 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md @@ -1 +1 @@ -0.64% +0.69% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md index e36dbca0e2..961a5f20a4 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md @@ -1 +1 @@ -44.811 +41.235 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md index f6f11060bd..40e1984ae2 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md @@ -1 +1 @@ -1_037_131 +1_038_975 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md index 5df8d8b43b..a5045462e5 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md @@ -1 +1 @@ -248_911_577 +249_354_023 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md index 5df8d8b43b..a5045462e5 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md @@ -1 +1 @@ -248_911_577 +249_354_023 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md index e9ed67ebdc..f723dc5285 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md @@ -1,7 +1,7 @@ | **Item** | **Description** | **Amount in NYM** | |:-------------------|:------------------------------------------------------|--------------------:| | Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 | -| Mixmining Reserve | Tokens releasing for operators rewards | 187_227_500 | +| Mixmining Reserve | Tokens releasing for operators rewards | 185_782_781 | | Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 | -| Circulating Supply | Amount of unlocked tokens | 812_772_499 | -| Stake Saturation | Optimal size of node self-bond + delegation | 1_037_131 | +| Circulating Supply | Amount of unlocked tokens | 814_217_218 | +| Stake Saturation | Optimal size of node self-bond + delegation | 1_038_975 | diff --git a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json index d811dfdbce..107af311de 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json @@ -1,10 +1,10 @@ { "interval": { - "reward_pool": "187227500568584.066152911712126276", - "staking_supply": "248911577950871.129740670788161327", + "reward_pool": "185782781887190.195531020579422623", + "staking_supply": "249354023047048.00261862494755182", "staking_supply_scale_factor": "0.30625", - "epoch_reward_budget": "5200763904.682890726469769781", - "stake_saturation_point": "1037131574795.296373919461617338", + "epoch_reward_budget": "5160632830.199727653639460539", + "stake_saturation_point": "1038975096029.366677577603948132", "sybil_resistance": "0.3", "active_set_work_factor": "10", "interval_pool_emission": "0.02" diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index ac97e28533..9a18ad5a74 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Friday, June 6th 2025, 09:33:10 UTC +Friday, July 4th 2025, 14:45:02 UTC diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index d2a2454361..d72f01bccd 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -47,6 +47,132 @@ This page displays a full list of all the changes during our release cycle from +## `v2025.12-dolcelatte` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.12-dolcelatte) +- [`nym-node`](nodes/nym-node.mdx) version `1.14.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-07-09T12:50:32.123212812Z +Build Version: 1.14.0 +Commit SHA: 089c47cce70ef8ea5ecfe3d00b52e510d006e120 +Commit Date: 2025-07-07T15:44:15.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- **[Nym Governator](https://governator.nym.com) is out!** Make sure to check out this new operators governance tool and vote on: + - [**NIP-1:** Proposing NIP as process to propose and log changes to the Nym network](https://governator.nym.com/proposal/prop-9719838a-e86d-4227-8141-dfd5c651ed92) + - [**NIP-2:** Change stake saturation and active set to improve network decentralisation and efficiency](https://governator.nym.com/proposal/prop-8dfa4a28-55b1-45a5-968f-f2897c2670df) + +### Features + +- [Nympool contract](https://github.com/nymtech/nym/pull/5464): This is the first iteration of the nympool contract. It's currently **not yet** integrated with the mixnet contract. + +- [Noise XKpsk3 integration (2025 version)](https://github.com/nymtech/nym/pull/5692) + +- [Key rotation](https://github.com/nymtech/nym/pull/5777) + +- [HTTP Client Retries, Fallbacks, and Redirects](https://github.com/nymtech/nym/pull/5789) + +- [Close sqlite pool before moving or reopening databases](https://github.com/nymtech/nym/pull/5796): Our sqlite db recovery and backup strategies don't work on Windows because the OS prevents moving the already open file. This PR attempts to address this and in principle it attempts to close all connections to the database and ensure that sqlx relinquishes the access to db file(s). Short summary of what this PR introduces: + + 1. Close sqlx connection pools in order to close/release the db file + 2. Poll until there are no more db file descriptors left open. + +- [Replace chrono with time in NS API](https://github.com/nymtech/nym/pull/5811) + +- [Use the same client bandwidth for top up](https://github.com/nymtech/nym/pull/5813) + +- [ Node status dvpn directory](https://github.com/nymtech/nym/pull/5829) + +- [Removing test-net faucet](https://github.com/nymtech/nym/pull/5840) + +- [Amended the buy section](https://github.com/nymtech/nym/pull/5841): Change the wallet to include the buy options for NYM + +- [Remove bity dir](https://github.com/nymtech/nym/pull/5844) + +- [Remove not used old `mock-api`](https://github.com/nymtech/nym/pull/5845) + +- [Remove qa env](https://github.com/nymtech/nym/pull/5847) + +- [Remove/old env references](https://github.com/nymtech/nym/pull/5848) + +- [Updated browser extension piece removal](https://github.com/nymtech/nym/pull/5849): Keep all the `internal-dev wasm` pieces as future examples + - everything previously was going to be removed + - shows functioning `wasm` interaction with the `js` + +### Bugfix + +- [Fixed typo in API endpoint parameter](https://github.com/nymtech/nym/pull/5449) + +- [Adjust heuristic for wireguard peer activity](https://github.com/nymtech/nym/pull/5818) + +- [Url scheme warning log](https://github.com/nymtech/nym/pull/5819): Fix conditions for logging about url scheme. Warns only if the provided urls are not HTTP or HTTPS, as intended. + +- [Bug Fix for Wallet build](https://github.com/nymtech/nym/pull/5820): Revert url used for `connection-tester` to default `url::Url`. + +- [Fix swapped total and circulating supplies](https://github.com/nymtech/nym/pull/5822) + +- [Fixed client route for obtaining v2 list of gateways](https://github.com/nymtech/nym/pull/5859) + +- [Allow gateways to permit authentication from v4 clients](https://github.com/nymtech/nym/pull/5862) + +- [Backwards compat](https://github.com/nymtech/nym/pull/5865) + +## `v2025.11-cheddar` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.11-cheddar) +- [`nym-node`](nodes/nym-node.mdx) version `1.13.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-06-10T09:41:31.291089877Z +Build Version: 1.13.0 +Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51 +Commit Date: 2025-06-10T11:39:20.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` +### Operators Updates & Tools + +- **Nym Improvement Proposals**: NIPs are being introduced, allowing us to propose changes to the Nym network and ecosystem, and decentralise governance +- **NIP1** will outline the governance process: Its draft will be published soon for discussion +- **NIP2** will be about reducing the stake saturation point variable, which would make reward distribution across the network more equitable +- **Join the Operator AMA next Tuesday** for an in-depth overview of what this all means (feat Nym Chief Scientist Claudia Diaz) +- Keep an eye on our channels for information about how to participate in the upcoming voting (no need for forum registration, we are building something new!) + +### Features + +- [Track wireguard credential retries](https://github.com/nymtech/nym/pull/5783) + +- [Swap a decode into a `fromrow` to please future `postgres` feature](https://github.com/nymtech/nym/pull/5785): This PR change a sqlx derive from `Decode` to `FromRow` because a future PR will bring the `postgres` feature and it doesn't like that `Decode` there + +- [Fix contains ticketbook function that always returned true](https://github.com/nymtech/nym/pull/5787) + +- [QoL: `RequestPath` trait for `http-api-client`](https://github.com/nymtech/nym/pull/5788): Those `PathSegments` in `ApiClient` weren't very user-friendly. Instead we've defined a `RequestPath` trait that allows you to also pass a good old `&str` or `String` and internally it does exactly the same sanitization as before. `PathSegments` are also fully supported to not break any existing compatibility. + +- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800): + - Types for `nym-vpn-client`: It introduces `VpnClientStatsReport` which will be used by nym vpn clients to report statistics. Those types are in the monorepo because they're used by the `num-statistics-API` + - Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code). + +- [Resolve 1.87 clippy warnings](https://github.com/nymtech/nym/pull/5802) + +- [Adjusted wallet storybook mocks to fix the build](https://github.com/nymtech/nym/pull/5804) + +- [Set cached storage counters to 0](https://github.com/nymtech/nym/pull/5812) + +- [No autoremoval of peers](https://github.com/nymtech/nym/pull/5831) + ## `v2025.10-brie` - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.10-brie) @@ -68,7 +194,7 @@ cargo Profile: release ### Operators Updates & Tools - [**Reward calculator**](tokenomics/mixnet-rewards#rewards-logic--calculation): A quick app to calculate rewards for any node included in the Mixnet active set -- **Backup your nodes: Stark Industries Solutions and its subsidy PQ.Hosting were added to EU's sanction list**. Everyone running a node on PQ or Stark in Europe should get additional service free of charge and back up their node there. +- **Backup your nodes: Stark Industries Solutions and its subsidy PQ.Hosting were added to EU's sanction list**. Everyone running a node on PQ or Stark in Europe should get additional service free of charge and back up their node there. - **We recommend operators using Stark Industries Solutions and PQ.Hosting to start looking for alternatives.** Here are some useful pages: - [Backup a node](nodes/maintenance#backup-a-node) - [Backup proxy conf](nodes/maintenance#backup-proxy-configuration) @@ -121,10 +247,10 @@ cargo Profile: release - [Downgrade deranged crate to `0.4.0`](https://github.com/nymtech/nym/pull/5746): Downgrade the crate `deranged` from `0.4.1` to `0.4.0`, as `0.4.1` was yanked and is flagged by `cargo audit` - + - [Upgrade prometheus crate to fix security warning](https://github.com/nymtech/nym/pull/5747): Upgrade the `prometheus` crate to bump the version of the `protobuf` crate, which is flagged by `cargo audit` as having a security issue: - + ```bash Crate: protobuf Version: 2.28.0 @@ -139,7 +265,7 @@ cargo Profile: release ``` - [Update `pretty_env_logger` to latest to not depend on unmaintained crate atty](https://github.com/nymtech/nym/pull/5748): The crate `atty` is flagged to be unmaintained and also having some security issues. -- [Remove `pretty_env_logger` and switch remaining crates to use tracing](https://github.com/nymtech/nym/pull/5749) +- [Remove `pretty_env_logger` and switch remaining crates to use tracing](https://github.com/nymtech/nym/pull/5749) ## `v2025.9-appenzeller` @@ -167,7 +293,7 @@ cargo Profile: release ### Features - [Add contains ticketbook data db query](https://github.com/nymtech/nym/pull/5670) -- [Add `/account/{address}`](https://github.com/nymtech/nym/pull/5673): Meant to replace `https://explorer.nymtech.net/api/v1/tmp/unstable/account/{address}` for Explorer v2 +- [Add `/account/{address}`](https://github.com/nymtech/nym/pull/5673): Meant to replace `https://explorer.nymtech.net/api/v1/tmp/unstable/account/{address}` for Explorer v2 ### Bugfix @@ -207,15 +333,15 @@ cargo Profile: release - [Adding fresh nym-api tests and workflow](https://github.com/nymtech/nym/pull/5659) -- [Replay protection](https://github.com/nymtech/nym/pull/5682): Introduces replay detection into a `nym-node`. Currently it uses a bloomfilter that's reset every 25h. In the future this will be controlled by the key rotation. The bloomfilter is also periodically flushed to disk in order to be able to recover from a crash/shutdown. +- [Replay protection](https://github.com/nymtech/nym/pull/5682): Introduces replay detection into a `nym-node`. Currently it uses a bloomfilter that's reset every 25h. In the future this will be controlled by the key rotation. The bloomfilter is also periodically flushed to disk in order to be able to recover from a crash/shutdown. -- [Tauri V2 - Wallet Migration](https://github.com/nymtech/nym/pull/5687): +- [Tauri V2 - Wallet Migration](https://github.com/nymtech/nym/pull/5687): - The core of the lifting was done via the migrate command - A lot of API's changed - Improved styling - Pipelines are fixed for all platforms - -- [Make mix hops optional for Mixnet Client](https://github.com/nymtech/nym/pull/5696): As is the route selection for packets, reply SURBs, and acknowledgements are selected deep withing the `MixnetClient` machinery. This makes custom route selection (i.e. for authenticating / registering wiregaurd mode vpn clients) is not supported. This PR adds configuration toggle that allows `MixnetClient` to be built where packets are sent direct through entry to Exit Gateway nodes -- no mix hops. + +- [Make mix hops optional for Mixnet Client](https://github.com/nymtech/nym/pull/5696): As is the route selection for packets, reply SURBs, and acknowledgements are selected deep withing the `MixnetClient` machinery. This makes custom route selection (i.e. for authenticating / registering wiregaurd mode vpn clients) is not supported. This PR adds configuration toggle that allows `MixnetClient` to be built where packets are sent direct through entry to Exit Gateway nodes -- no mix hops. - [Bump the `nym-vpn deb` metapackage to `1.0`](https://github.com/nymtech/nym/pull/5697) @@ -227,7 +353,7 @@ cargo Profile: release - [Peer handle should die more gracefully](https://github.com/nymtech/nym/pull/5704) -- [Update `hickory` DNS `0.24.4` to `0.25`](https://github.com/nymtech/nym/pull/5709): Update the dependency on `hickory` DNS to the latest minor version +- [Update `hickory` DNS `0.24.4` to `0.25`](https://github.com/nymtech/nym/pull/5709): Update the dependency on `hickory` DNS to the latest minor version - [Remove inactive peers](https://github.com/nymtech/nym/pull/5721) @@ -254,7 +380,7 @@ cargo Profile: release ### Operators Updates & Tools -- **[Nym release binaries](https://github.com/nymtech/nym/releases) no longer work on distributions Debian bullseye/sid (11) / Ubuntu 20.04 LTS and older! Please upgrade your sever to Debian bookworm (Debian 12) / Ubuntu 22.04 (and newer)!** Alternatively compile the binaries from source. +- **[Nym release binaries](https://github.com/nymtech/nym/releases) no longer work on distributions Debian bullseye/sid (11) / Ubuntu 20.04 LTS and older! Please upgrade your sever to Debian bookworm (Debian 12) / Ubuntu 22.04 (and newer)!** Alternatively compile the binaries from source. - The Nym Squad League Winter Season has concluded. Changes are coming to NSL, including the new NSL council, monthly payments for contributions, revamped reports, and more. Details available in the [Winter Season report](https://forum.nym.com/t/nym-squad-league-winter-season-report/1265). @@ -270,7 +396,7 @@ cargo Profile: release - [Mix throughput tester](https://github.com/nymtech/nym/pull/5661): A utility to measure mixing throughput of a node. Especially since there are some design choices with non-obvious performance penalties. For example 1 bloomfilter per connection vs 1 global bloomfilter with additional locking for replay protection. -- [Update log crate](https://github.com/nymtech/nym/pull/5667): Update `log crate` to latest, fix `format_in_format_args`, fix `to_string_in_format_args` +- [Update log crate](https://github.com/nymtech/nym/pull/5667): Update `log crate` to latest, fix `format_in_format_args`, fix `to_string_in_format_args` - [Bump the patch-updates group across 1 directory with 7 updates](https://github.com/nymtech/nym/pull/5668): @@ -286,8 +412,8 @@ cargo Profile: release - [Improve explorer caching](https://github.com/nymtech/nym/pull/5669): Fix: - `tanstack` caching - - graphs data bug - - "auto" gas fee for redeem rewards + - graphs data bug + - "auto" gas fee for redeem rewards - [Update node versions in CI](https://github.com/nymtech/nym/pull/5677) @@ -299,7 +425,7 @@ cargo Profile: release - [`clippy` for `1.86`](https://github.com/nymtech/nym/pull/5685) -- [Expand `/v3/nym-nodes` with geodata](https://github.com/nymtech/nym/pull/5686): Includes node description and geodata. +- [Expand `/v3/nym-nodes` with geodata](https://github.com/nymtech/nym/pull/5686): Includes node description and geodata. ### Bugfix @@ -335,11 +461,11 @@ cargo Profile: release - [**Nym Explorer v2.1.0**](https://nym.com/explorer) is out as a beta release -- Since last release [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration). Make sure to apply it on the servers hosting `nym-node` in `exit-gateway` mode. +- Since last release [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration). Make sure to apply it on the servers hosting `nym-node` in `exit-gateway` mode. #### Community Tools -Nym operators are coming with diverse backgrounds. While for some running a `nym-node` is an introduction to sys-administration, others are well seasoned builders and hackers. It's amazing to see the effort people put in order to lift each other by day-to-day mutual problem solving support in [Matrix channel](https://matrix.to/#/#operators:nymtech.chat). More and more people take it further and build tools to improve network monitoring and node administration. +Nym operators are coming with diverse backgrounds. While for some running a `nym-node` is an introduction to sys-administration, others are well seasoned builders and hackers. It's amazing to see the effort people put in order to lift each other by day-to-day mutual problem solving support in [Matrix channel](https://matrix.to/#/#operators:nymtech.chat). More and more people take it further and build tools to improve network monitoring and node administration. While we are planning to make a page aggregating these tools together, here are some tools released lately by the technical community: @@ -363,7 +489,7 @@ While we are planning to make a page aggregating these tools together, here are - [Bump `nanoid` from `3.3.7` to `3.3.8` in `/documentation/docs`](https://github.com/nymtech/nym/pull/5335): Bumps [`nanoid`](https://github.com/ai/nanoid) -- [Bump `store2` from `2.14.3` to `2.14.4`](https://github.com/nymtech/nym/pull/5391): Bumps [`store2`](https://github.com/nbubna/store) +- [Bump `store2` from `2.14.3` to `2.14.4`](https://github.com/nymtech/nym/pull/5391): Bumps [`store2`](https://github.com/nbubna/store) - [Bump `@octokit/plugin-paginate-rest` and `@actions/github` in `/.github/actions/nym-hash-releases/src`](https://github.com/nymtech/nym/pull/5488): Bumps [`@octokit/plugin-paginate-rest`](https://github.com/octokit/plugin-paginate-rest.js) @@ -388,16 +514,16 @@ While we are planning to make a page aggregating these tools together, here are - `/issued-ticketbooks-on-count/:issuance_date` - `/issued-ticketbooks-challenge-commitment` - `/issued-ticketbooks-data` - + - removed: - `/issued-ticketbooks-challenge` }> Testing Steps Performed: -New issue ticketbook related endpoints work - tickets are issued and consumed as before. +New issue ticketbook related endpoints work - tickets are issued and consumed as before. Redeeming tickets is happening as normal on the gateway side. - + Notes (if any): Chuckles will not require the rewarder binary to be updated; as there will be additional changes to rewarding, this will be extensively tested in the future. @@ -432,15 +558,15 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Bump `tempfile` from `3.18.0` to `3.19.0`](https://github.com/nymtech/nym/pull/5631): Bumps [`tempfile`](https://github.com/Stebalien/tempfile) - [Rework IPR codec to extract out timer and implement AsyncWrite](https://github.com/nymtech/nym/pull/5632): The goal is to be able to handle back pressure in the vpn client. For that we need to extract out the timer from the codec since we want to move control over that to the mixnet processor. Once the timer is removed we can reformulate the mixnet sender as a `Sink` wrapped in a `FramedWrite` - + - Extract out timer from IPR codec - Implement AsyncWrite on mixnet client sender - Add functions to wait for lanes to clear on `LaneQueueLenghts` - + - [Remove `explorer-api` from the main workspace](https://github.com/nymtech/nym/pull/5635) - [Bump `dtolnay/rust-toolchain` from `1.90.0` to `1.100.0`](https://github.com/nymtech/nym/pull/5638): Bumps [`dtolnay/rust-toolchain`](https://github.com/dtolnay/rust-toolchain) - + - [Bump `zip` from `2.2.2` to `2.4.1`](https://github.com/nymtech/nym/pull/5639): Bumps [`zip`](https://github.com/zip-rs/zip2) - [Add `max_retransmissions` flag on each message](https://github.com/nymtech/nym/pull/5642): Add an optional `max_retransmissions` field on `InputMessage`, to be able to selectively disable or turn down the number of `retransmissions` done for specific packets @@ -456,7 +582,7 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Update wallet to include Interval Operator Cost and Profit Margin](https://github.com/nymtech/nym/pull/5652): Created a new modal for Interval Operator Cost and Profit Margin changes - Uses existing update node operating cost functionality for old mixnodes (If it works it works) - Prompts a warning that a user "Can send the request as many times as they want but it will only update the last message when the interval changes" - + - [Wallet-revamp to be in line with new nym-theming](https://github.com/nymtech/nym/pull/5653): Updating colour palette to match the [nym.com](https://nym.com) sites: - Used the same font too - Updated icons @@ -465,7 +591,7 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Revert using `AsyncWrite` sink in IPR](https://github.com/nymtech/nym/pull/5656): Revert the use of the `AsyncWrite` sink in the IPR until it's fully ready. -- [Remove Google public DNS](https://github.com/nymtech/nym/pull/5660): We had a lot of complaints about Google, so we're removing this in VPN client, Nym should follow to avoid issues with firewall. +- [Remove Google public DNS](https://github.com/nymtech/nym/pull/5660): We had a lot of complaints about Google, so we're removing this in VPN client, Nym should follow to avoid issues with firewall. ### Bugfix @@ -496,14 +622,14 @@ cargo Profile: release 2. Security Fixes in Dependencies: Address known security vulnerabilities, making updating highly recommended 3. Authorisation & Timestamp Changes: Modifies how timestamps are validated in Auth v2 - this is going to be what the clients use. For example: the clients will start to use v2 variants of everything (prepping the network for the breaking change to come on April 1st which will force clients to only use v2). - + 4. Wireguard exit policy [scripts](https://github.com/nymtech/nym/tree/develop/scripts/wireguard-exit-policy) ### Operators Updates & Tools - [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration) Operators can use a new guide with our scripts to setup wireguard exit policy using IP tables rules. -- [Setup commands for `nym-node` simplified](nodes/nym-node/setup#setup--run): `config.toml` defaults to correct binding addresses and ports, no need to specify unless operators want to change those values. +- [Setup commands for `nym-node` simplified](nodes/nym-node/setup#setup--run): `config.toml` defaults to correct binding addresses and ports, no need to specify unless operators want to change those values. - [Virtualising dedicated server with KVM now includes IPv6 setup](nodes/preliminary-steps/vps-setup/advanced#installing-kvm-on-a-server-with-ubuntu-2204): Thanks to [LunarDAO](https://lunardao.net) squad we caught a missing IPv6 configuration in KVM installation guide. Operators can now run a bare metal or dedicated server and virtualise it into VMs each with a fully functional node, having all specs fully under control. @@ -572,17 +698,17 @@ cargo Profile: release - Add `bond_info` & `self_described` to DB `nym_nodes` - Update mixnode & gateway bond status on data refresh - Add `active` column to DB `nym_nodes` - - Use only active & bonded nodes in scraping/testrun tasks + - Use only active & bonded nodes in scraping/testrun tasks - [Rust SDK SURB example: change hardcoded file to tempdir](https://github.com/nymtech/nym/pull/5576): Change hardcoded filepath for client to tempdir -- [Server Side internal DoT/DoH opt out](https://github.com/nymtech/nym/pull/5577): Allow server side (nodes and nym-api) to use nym-http-api-client without DoH/DoT resolver. +- [Server Side internal DoT/DoH opt out](https://github.com/nymtech/nym/pull/5577): Allow server side (nodes and nym-api) to use nym-http-api-client without DoH/DoT resolver. - [Delete double memo field in send modal](https://github.com/nymtech/nym/pull/5578) - [Bump ring from `0.17.3` to `0.17.13` in /nym-wallet](https://github.com/nymtech/nym/pull/5582): Bumps [ring](https://github.com/briansmith/ring) -- [Bump the patch-updates group with 8 updates](https://github.com/nymtech/nym/pull/5585): +- [Bump the patch-updates group with 8 updates](https://github.com/nymtech/nym/pull/5585): | Package | From | To | | --- | --- | --- | @@ -649,7 +775,7 @@ cargo Profile: release ## `v2025.4-dorina-patched` -Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch. +Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch. - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.4-dorina-patched) - [`nym-node`](nodes/nym-node.mdx) version `1.6.2` diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index 8c2e00052d..760b8e5efa 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -20,10 +20,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ```sh nym-node Binary Name: nym-node -Build Timestamp: 2025-05-27T10:13:18.511075453Z -Build Version: 1.12.0 -Commit SHA: 1c6db86259d08d80e8bcfbc4fcc71ccb147fcfd0 -Commit Date: 2025-05-27T12:11:13.000000000+02:00 +Build Timestamp: 2025-07-09T12:50:32.123212812Z +Build Version: 1.14.0 +Commit SHA: 089c47cce70ef8ea5ecfe3d00b52e510d006e120 +Commit Date: 2025-07-07T15:44:15.000000000+02:00 Commit Branch: HEAD rustc Version: 1.86.0 rustc Channel: stable diff --git a/envs/canary.env b/envs/canary.env index 6bdd85494e..f494ad3270 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -18,7 +18,6 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07 MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364 COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 -EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/ NYXD=https://rpc.canary-validator.performance.nymte.ch NYM_API=https://canary-api.performance.nymte.ch/api/ NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket diff --git a/envs/mainnet-local-api.env b/envs/mainnet-local-api.env index d6683c937d..f7d1fec6d5 100644 --- a/envs/mainnet-local-api.env +++ b/envs/mainnet-local-api.env @@ -23,5 +23,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYXD="https://rpc.nymtech.net" NYM_API="http://127.0.0.1:8000" NYXD_WS="wss://rpc.nymtech.net/websocket" -EXPLORER_API="https://explorer.nymtech.net/api/" NYM_VPN_API="https://nymvpn.com/api" diff --git a/envs/mainnet.env b/envs/mainnet.env index ad1e2cd88e..444307f836 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -24,5 +24,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS=https://mainnet-stats.nymte.ch:8090 NYXD=https://rpc.nymtech.net NYM_API=https://validator.nymtech.net/api/ NYXD_WS=wss://rpc.nymtech.net/websocket -EXPLORER_API=https://explorer.nymtech.net/api/ NYM_VPN_API=https://nymvpn.com/api/ diff --git a/envs/sandbox.env b/envs/sandbox.env index 68e51fec59..e90257f015 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -19,7 +19,6 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865 ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9 STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -EXPLORER_API=https://sandbox-explorer.nymtech.net/api/ NYXD=https://rpc.sandbox.nymtech.net NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket NYM_API=https://sandbox-nym-api1.nymtech.net/api/ diff --git a/explorer-nextjs/.eslintrc.json b/explorer-nextjs/.eslintrc.json deleted file mode 100644 index 957cd1545e..0000000000 --- a/explorer-nextjs/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next/core-web-vitals"] -} diff --git a/explorer-nextjs/.gitignore b/explorer-nextjs/.gitignore deleted file mode 100644 index fd3dbb571a..0000000000 --- a/explorer-nextjs/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/explorer-nextjs/README.md b/explorer-nextjs/README.md deleted file mode 100644 index c4033664f8..0000000000 --- a/explorer-nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/explorer-nextjs/app/App.tsx b/explorer-nextjs/app/App.tsx deleted file mode 100644 index 6763797c84..0000000000 --- a/explorer-nextjs/app/App.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { Navbar } from './components/Nav/Navbar' -import { Providers } from './providers' - -const App = ({ children }: { children: React.ReactNode }) => ( - - {children} - -) - -export { App } diff --git a/explorer-nextjs/app/account/[id]/page.tsx b/explorer-nextjs/app/account/[id]/page.tsx deleted file mode 100644 index c0c482476c..0000000000 --- a/explorer-nextjs/app/account/[id]/page.tsx +++ /dev/null @@ -1,407 +0,0 @@ -'use client' - -import * as React from 'react' -import {Alert, AlertTitle, Box, Button, Chip, CircularProgress, Grid, Tooltip, Typography} from '@mui/material' -import { useParams } from 'next/navigation' -import { useMainContext } from '@/app/context/main' -import { Title } from '@/app/components/Title' -import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from "material-react-table"; -import { useMemo } from "react"; -import { humanReadableCurrencyToString } from "@/app/utils/currency"; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; -import { PieChart } from '@mui/x-charts/PieChart'; -import { useTheme } from "@mui/material/styles"; -import { useIsMobile } from "@/app/hooks"; -import { StyledLink } from "@/app/components"; - -const AccumulatedRewards = ({account}: { account?: any}) => { - const columns = useMemo< - MRT_ColumnDef[] - >(() => { - return [ - { - id: 'accumulated-rewards-data', - header: 'Accumulated Rewards Data', - columns: [ - { - id: 'node_id', - accessorKey: 'node_id', - header: 'Node ID', - size: 150, - Cell: ({ row }) => ( - {row.original.node_id} - ), - }, - { - id: 'node_still_fully_bonded', - accessorKey: 'node_still_fully_bonded', - header: 'Node still bonded?', - width: 150, - Cell: ({ row }) => ( - <>{row.original.node_still_fully_bonded ? : - theme.palette.warning.main }}> - - Unbonded - } - ) - }, - { - id: 'amount_staked', - accessorKey: 'amount_staked', - header: 'Amount', - width: 150, - Cell: ({ row }) => ( - <>{humanReadableCurrencyToString(row.original.amount_staked)} - ) - }, - { - id: 'rewards', - accessorKey: 'rewards', - header: 'Rewards', - width: 150, - Cell: ({ row }) => ( - {humanReadableCurrencyToString(row.original.rewards)} - ) - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: account?.accumulated_rewards || [], - enableFullScreenToggle: false, - }) - - return (); -} - -const DelegationHistory = ({account}: { account?: any}) => { - const columns = useMemo< - MRT_ColumnDef[] - >(() => { - return [ - { - id: 'delegation-history-data', - header: 'Delegation History', - columns: [ - { - id: 'node_id', - accessorKey: 'node_id', - header: 'Node ID', - size: 150, - }, - { - id: 'delegated', - accessorKey: 'delegated', - header: 'Amount', - width: 150, - Cell: ({ row }) => ( - <>{humanReadableCurrencyToString(row.original.delegated)} - ) - }, - { - id: 'height', - accessorKey: 'height', - header: 'Delegated at height', - width: 150, - Cell: ({ row }) => ( - <>{row.original.height} - ) - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: account?.delegations || [], - enableFullScreenToggle: false, - }) - - return (); -} - - -/** - * Shows account details - */ -const PageAccountWithState = ({ account }: { - account?: any; -}) => { - const theme = useTheme(); - const isMobile = useIsMobile(); - - const pieChartData = React.useMemo(() => { - if(!account) { - return []; - } - - const parts = []; - - const nymBalance = Number.parseFloat(account.balances.find((b: any) => b.denom === "unym")?.amount || "0") / 1e6; - - if(nymBalance > 0) { - parts.push({label: "Spendable", value: nymBalance, color: theme.palette.primary.main}); - } - - if(account.vesting_account) { - if (`${account.vesting_account.locked?.amount}` !== "0") { - const value = Number.parseFloat(account.vesting_account.locked.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Vesting locked", - value, - color: 'red' - }); - } - } - if (`${account.vesting_account.spendable?.amount}` !== "0") { - const value = Number.parseFloat(account.vesting_account.spendable.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Vesting spendable", - value, - color: theme.palette.primary.light - }); - } - } - } - - if (account.claimable_rewards &&`${account.claimable_rewards.amount}` !== "0") { - const value = Number.parseFloat(account.claimable_rewards.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Claimable delegation rewards", - value, - color: theme.palette.success.light - }); - } - } - if (account.operator_rewards && `${account.operator_rewards.amount}` !== "0") { - const value = Number.parseFloat(account.operator_rewards.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Claimable operator rewards", - value, - color: theme.palette.success.dark - }); - } - } - if (account.total_delegations && `${account.total_delegations.amount}` !== "0") { - const value = Number.parseFloat(account.total_delegations.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Total delegations", - value, - color: '#888' - }); - } - } - - return parts; - }, [account]); - - return ( - - - - </Box> - - <Box mt={4} sx={{ maxWidth: "600px" }}> - <PieChart - series={[ - { - data: pieChartData, - innerRadius: 40, - outerRadius: 80, - cy: isMobile ? 200 : undefined, - }, - ]} - height={300} - slotProps={isMobile ? { - legend: { position: { vertical: "top", horizontal: "right" } } - } : undefined} - /> - </Box> - - <Box mt={4}> - <TableContainer component={Paper} sx={{ maxWidth: "400px" }}> - <Table> - <TableBody> - <TableRow sx={{ color: theme => theme.palette.primary.main }}> - <TableCell component="th" scope="row" sx={{ color: "inherit" }}> - <strong>Spendable Balance</strong> - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {account.balances.map((b: any) => (<strong key={`balance-${b.denom}`}>{humanReadableCurrencyToString(b)}<br/></strong>))} - </TableCell> - </TableRow> - <TableRow> - <TableCell component="th" scope="row"> - Total delegations - </TableCell> - <TableCell align="right"> - {humanReadableCurrencyToString(account.total_delegations)} - </TableCell> - </TableRow> - {account.claimable_rewards && <TableRow sx={{ color: theme => theme.palette.success.light }}> - <TableCell component="th" scope="row" sx={{ color: "inherit" }}> - Claimable delegation rewards - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.claimable_rewards)} - </TableCell> - </TableRow>} - {account.operator_rewards && `${account.operator_rewards.amount}` !== "0" && <TableRow sx={{ color: theme => theme.palette.success.light }}> - <TableCell component="th" scope="row" sx={{ color: "inherit" }}> - Claimable operator rewards - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.operator_rewards)} - </TableCell> - </TableRow>} - {account.vesting_account && ( - <> - <TableRow> - <TableCell component="th" scope="row" colSpan={2}> - Vesting account - </TableCell> - </TableRow> - {`${account.vesting_account.locked.amount}` !== "0" && - <TableRow> - <TableCell component="th" scope="row" sx={{ pl: 4 }}> - Locked - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.vesting_account.locked)} - </TableCell> - </TableRow> - } - {`${account.vesting_account.vested.amount}` !== "0" && - <TableRow> - <TableCell component="th" scope="row" sx={{ pl: 4 }}> - Vested - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.vesting_account.vested)} - </TableCell> - </TableRow> - } - {`${account.vesting_account.vesting.amount}` !== "0" && - <TableRow> - <TableCell component="th" scope="row" sx={{ pl: 4 }}> - Vesting - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.vesting_account.vesting)} - </TableCell> - </TableRow> - } - {`${account.vesting_account.spendable.amount}` !== "0" && - <TableRow> - <TableCell component="th" scope="row" sx={{ pl: 4 }}> - Spendable - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.vesting_account.spendable)} - </TableCell> - </TableRow> - } - </> - )} - <TableRow> - <TableCell component="th" scope="row" sx={{ color: "inherit" }}> - <h3>Total value</h3> - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - <h3>{humanReadableCurrencyToString(account.total_value)}</h3> - </TableCell> - </TableRow> - </TableBody> - </Table> - </TableContainer> - </Box> - <Box mt={4}> - <AccumulatedRewards account={account}/> - </Box> - <Box mt={4}> - <DelegationHistory account={account}/> - </Box> - </Box> - ) -} - -/** - * Guard component to handle loading and not found states - */ -const PageAccountDetailGuard = ({ account } : { account: string }) => { - const [accountDetails, setAccountDetails] = React.useState<any>(); - const [isLoading, setLoading] = React.useState<boolean>(true); - const [error, setError] = React.useState<string>(); - const { fetchAccountById } = useMainContext() - const { id } = useParams() - - React.useEffect(() => { - setLoading(true); - (async () => { - if(typeof(id) === "string") { - try { - const res = await fetchAccountById(account); - setAccountDetails(res); - } catch(e: any) { - setError(e.message); - } - finally { - setLoading(false); - } - } - })(); - }, [id]) - - if (isLoading) { - return <CircularProgress /> - } - - // loaded, but not found - if (error) { - return ( - <Alert severity="warning"> - <AlertTitle>Account not found</AlertTitle> - Sorry, we could not find the account <code>{id || ''}</code> - </Alert> - ) - } - - return <PageAccountWithState account={accountDetails} /> -} - -/** - * Wrapper component that adds the account details based on the `id` in the address URL - */ -const PageAccountDetail = () => { - const { id } = useParams() - - if (!id || typeof id !== 'string') { - return ( - <Alert severity="error">Oh no! Could not find that account</Alert> - ) - } - - return ( - <PageAccountDetailGuard account={id} /> - ) -} - -export default PageAccountDetail diff --git a/explorer-nextjs/app/api/constants.ts b/explorer-nextjs/app/api/constants.ts deleted file mode 100644 index 515655c184..0000000000 --- a/explorer-nextjs/app/api/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -// master APIs -export const API_BASE_URL = process.env.NEXT_PUBLIC_EXPLORER_API_URL || 'https://explorer.nymtech.net/api/v1'; -export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https://validator.nymtech.net'; - -export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net'; - -export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net'; -export const BLOCK_EXPLORER_BASE_URL = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru'; - -// specific API routes -export const OVERVIEW_API = `${API_BASE_URL}/overview`; -export const MIXNODE_PING = `${API_BASE_URL}/ping`; -export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; -export const MIXNODE_API = `${API_BASE_URL}/mix-node`; -export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`; -export const BLOCK_API = `${NYX_RPC_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 SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`; -export const TEMP_UNSTABLE_NYM_NODES = `${API_BASE_URL}/tmp/unstable/nym-nodes`; -export const TEMP_UNSTABLE_ACCOUNT = `${API_BASE_URL}/tmp/unstable/account`; -export const NYM_API_NODE_UPTIME = `${NYM_API_BASE_URL}/api/v1/nym-nodes/uptime-history`; -export const NYM_API_NODE_PERFORMANCE = `${NYM_API_BASE_URL}/api/v1/nym-nodes/performance-history`; - -export const LEGACY_MIXNODES_API = `${API_BASE_URL}/tmp/unstable/legacy-mixnode-bonds`; -export const LEGACY_GATEWAYS_API = `${API_BASE_URL}/tmp/unstable/legacy-gateway-bonds`; - -// errors -export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us."; - -export const NYM_WEBSITE = 'https://nymtech.net'; - -export const EXPLORER_FOR_ACCOUNTS = ''; // set to empty to use this Nym Explorer and NOT an external one - -export const NYM_MIXNET_CONTRACT = - process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr'; -export const COSMOS_KIT_USE_CHAIN = process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || 'sandbox'; -export const WALLET_CONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || ''; diff --git a/explorer-nextjs/app/api/index.ts b/explorer-nextjs/app/api/index.ts deleted file mode 100644 index ae190de9b9..0000000000 --- a/explorer-nextjs/app/api/index.ts +++ /dev/null @@ -1,217 +0,0 @@ -import keyBy from 'lodash/keyBy'; -import { - API_BASE_URL, - BLOCK_API, - COUNTRY_DATA_API, - UPTIME_STORY_API_GATEWAY, - MIXNODE_API, - MIXNODE_PING, - MIXNODES_API, - OVERVIEW_API, - UPTIME_STORY_API, - VALIDATORS_API, - SERVICE_PROVIDERS, - TEMP_UNSTABLE_NYM_NODES, - NYM_API_NODE_UPTIME, - NYM_API_NODE_PERFORMANCE, - TEMP_UNSTABLE_ACCOUNT, - LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API, -} from './constants'; - -import { - CountryDataResponse, - DelegationsResponse, - UniqDelegationsResponse, - GatewayReportResponse, - UptimeStoryResponse, - MixNodeDescriptionResponse, - MixNodeResponse, - MixNodeResponseItem, - MixnodeStatus, - MixNodeEconomicDynamicsStatsResponse, - StatsResponse, - StatusResponse, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - GatewayBondAnnotated, - GatewayBond, - DirectoryServiceProvider, - LocatedGateway, -} from '../typeDefs/explorer-api'; - -function getFromCache(key: string) { - const ts = Number(localStorage.getItem('ts')); - const hasExpired = Date.now() - ts > 5000; - const curr = localStorage.getItem(key); - if (curr && !hasExpired) { - return JSON.parse(curr); - } - return undefined; -} - -function storeInCache(key: string, data: any) { - localStorage.setItem(key, data); - localStorage.setItem('ts', Date.now().toString()); -} - -export class Api { - static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => { - const cache = getFromCache('overview-summary'); - if (cache) { - return cache; - } - const res = await fetch(`${OVERVIEW_API}/summary`); - const json: SummaryOverviewResponse = await res.json(); - - if (json.nymnodes?.roles) { - json.mixnodes.count += json.nymnodes.roles.mixnode; - json.gateways.count += json.nymnodes.roles.entry; - json.gateways.count += Math.max(json.nymnodes.roles.exit_ipr, json.nymnodes.roles.exit_nr); - } - - storeInCache('overview-summary', JSON.stringify(json)); - return json; - }; - - static fetchMixnodes = async (): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache('mixnodes'); - if (cachedMixnodes) { - return cachedMixnodes; - } - - const res = await fetch(LEGACY_MIXNODES_API); - const json = await res.json(); - storeInCache('mixnodes', JSON.stringify(json)); - return json; - }; - - static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache(`mixnodes-${status}`); - if (cachedMixnodes) { - return cachedMixnodes; - } - const res = await fetch(`${MIXNODES_API}/active-set/${status}`); - const json = await res.json(); - storeInCache(`mixnodes-${status}`, JSON.stringify(json)); - return json; - }; - - static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => { - const response = await fetch(`${MIXNODE_API}/${id}`); - - // when the mixnode is not found, returned undefined - if (response.status === 404) { - return undefined; - } - - return response.json(); - }; - - static fetchGateways = async (): Promise<LocatedGateway[]> => { - // const res = await fetch(GATEWAYS_API); - // const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); - // const res2 = await fetch(GATEWAYS_EXPLORER_API); - // const locatedGateways: LocatedGateway[] = await res2.json(); - // const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); - // return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ - // ...gateway_bond, - // node_performance, - // location: locatedGatewaysByOwner[gateway_bond.owner]?.location, - // })); - - const res = await fetch(LEGACY_GATEWAYS_API); - const locatedGateways: LocatedGateway[] = await res.json(); - return locatedGateways; - }; - - static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json(); - - static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json(); - - static fetchValidators = async (): Promise<ValidatorsResponse> => { - const res = await fetch(VALIDATORS_API); - const json = await res.json(); - return json.result; - }; - - static fetchBlock = async (): Promise<number> => { - const res = await fetch(BLOCK_API); - const json = await res.json(); - const { height } = json.result.block.header; - return height; - }; - - static fetchCountryData = async (): Promise<CountryDataResponse> => { - const result: CountryDataResponse = {}; - const res = await fetch(COUNTRY_DATA_API); - const json = await res.json(); - Object.keys(json).forEach((ISO3) => { - result[ISO3] = { ISO3, nodes: json[ISO3] }; - }); - return result; - }; - - static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations`)).json(); - - static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json(); - - static fetchStatsById = async (id: string): Promise<StatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/stats`)).json(); - - static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> => - (await fetch(`${MIXNODE_API}/${id}/description`)).json(); - - static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json(); - - static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json(); - - static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API}/${id}/history`)).json(); - - static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => { - const res = await fetch(SERVICE_PROVIDERS); - const json = await res.json(); - return json; - }; - - static fetchNodes = async () => { - const res = await fetch(TEMP_UNSTABLE_NYM_NODES); - const json = await res.json(); - return json; - } - - static fetchNodeById = async (id: number) => { - const res = await fetch(`${TEMP_UNSTABLE_NYM_NODES}/${id}`); - const json = await res.json(); - return json; - } - - static fetchNymNodeUptimeHistoryById = async (id: number | string) => { - const res = await fetch(`${NYM_API_NODE_UPTIME}/${id}`) - const json = await res.json(); - return json; - } - - static fetchNymNodePerformanceById = async (id: number | string) => { - const res = await fetch(`${NYM_API_NODE_PERFORMANCE}/${id}`) - const json = await res.json(); - return json; - } - - static fetchAccountById = async (id: string) => { - const res = await fetch(`${TEMP_UNSTABLE_ACCOUNT}/${id}`); - const json = await res.json(); - return json; - } -} - -export const getEnvironment = (): Environment => { - const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env; - return matchEnv('sandbox') || matchEnv('qa') || 'mainnet'; -}; diff --git a/explorer-nextjs/app/assets/world-110m.json b/explorer-nextjs/app/assets/world-110m.json deleted file mode 100644 index 5c999c82ca..0000000000 --- a/explorer-nextjs/app/assets/world-110m.json +++ /dev/null @@ -1,39718 +0,0 @@ -{ - "type": "Topology", - "arcs": [ - [ - [ - 16814, - 15074 - ], - [ - 71, - -45 - ], - [ - 53, - 16 - ], - [ - 15, - 54 - ], - [ - 55, - 18 - ], - [ - 39, - 37 - ], - [ - 14, - 95 - ], - [ - 59, - 24 - ], - [ - 11, - 42 - ], - [ - 32, - -32 - ], - [ - 21, - -3 - ] - ], - [ - [ - 17184, - 15280 - ], - [ - 39, - -1 - ], - [ - 53, - -26 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 21, - -14 - ], - [ - 51, - 38 - ], - [ - 23, - -23 - ], - [ - 23, - 55 - ], - [ - 41, - -2 - ], - [ - 11, - 17 - ], - [ - 7, - 49 - ], - [ - 30, - 41 - ], - [ - 38, - -27 - ], - [ - -8, - -37 - ], - [ - 22, - -5 - ], - [ - -7, - -101 - ], - [ - 28, - -39 - ], - [ - 24, - 25 - ], - [ - 31, - 12 - ], - [ - 43, - 53 - ], - [ - 48, - -8 - ], - [ - 72, - -1 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - 13, - -34 - ] - ], - [ - [ - 17787, - 15252 - ], - [ - -41, - -13 - ], - [ - -35, - -23 - ], - [ - -80, - -14 - ], - [ - -75, - -25 - ], - [ - -41, - -52 - ], - [ - 17, - -51 - ], - [ - 8, - -60 - ], - [ - -35, - -50 - ], - [ - 3, - -46 - ], - [ - -19, - -43 - ], - [ - -67, - 4 - ], - [ - 28, - -80 - ], - [ - -45, - -30 - ], - [ - -29, - -73 - ], - [ - 4, - -72 - ], - [ - -28, - -33 - ], - [ - -26, - 11 - ], - [ - -53, - -16 - ], - [ - -7, - -33 - ], - [ - -52, - 0 - ], - [ - -39, - -68 - ], - [ - -3, - -102 - ], - [ - -90, - -50 - ], - [ - -49, - 10 - ], - [ - -14, - -26 - ], - [ - -42, - 15 - ], - [ - -69, - -17 - ], - [ - -117, - 61 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 63, - 109 - ], - [ - -6, - 77 - ], - [ - -52, - 20 - ], - [ - -6, - 76 - ], - [ - -23, - 96 - ], - [ - 30, - 66 - ], - [ - -30, - 17 - ], - [ - 19, - 88 - ], - [ - 28, - 149 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - -128, - -49 - ], - [ - -169, - 17 - ], - [ - -48, - 58 - ], - [ - -283, - -6 - ], - [ - -11, - -8 - ], - [ - -41, - 54 - ], - [ - -45, - 4 - ], - [ - -42, - -21 - ], - [ - -34, - -22 - ] - ], - [ - [ - 13365, - 8722 - ], - [ - -6, - 75 - ], - [ - 10, - 105 - ], - [ - 24, - 110 - ], - [ - 3, - 52 - ], - [ - 23, - 108 - ], - [ - 16, - 49 - ], - [ - 41, - 79 - ], - [ - 22, - 53 - ], - [ - 7, - 89 - ], - [ - -3, - 68 - ], - [ - -21, - 43 - ], - [ - -19, - 72 - ], - [ - -17, - 72 - ], - [ - 4, - 25 - ], - [ - 21, - 48 - ], - [ - -21, - 116 - ], - [ - -14, - 80 - ], - [ - -35, - 76 - ], - [ - 6, - 23 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - 29, - 16 - ], - [ - 20, - -2 - ], - [ - 25, - 15 - ], - [ - 206, - -2 - ], - [ - 17, - -89 - ], - [ - 20, - -72 - ], - [ - 16, - -39 - ], - [ - 27, - -63 - ], - [ - 46, - 10 - ], - [ - 23, - 17 - ], - [ - 38, - -17 - ], - [ - 11, - 30 - ], - [ - 17, - 70 - ], - [ - 43, - 4 - ], - [ - 4, - 21 - ], - [ - 36, - 1 - ], - [ - -6, - -44 - ], - [ - 84, - 2 - ], - [ - 1, - -76 - ], - [ - 15, - -46 - ], - [ - -11, - -73 - ], - [ - 5, - -73 - ], - [ - 24, - -45 - ], - [ - -4, - -143 - ], - [ - 17, - 11 - ], - [ - 30, - -3 - ], - [ - 44, - 18 - ], - [ - 31, - -7 - ] - ], - [ - [ - 14214, - 9486 - ], - [ - 8, - -37 - ], - [ - -8, - -58 - ], - [ - 12, - -56 - ], - [ - -10, - -45 - ], - [ - 6, - -42 - ], - [ - -146, - 2 - ], - [ - -3, - -382 - ], - [ - 47, - -98 - ], - [ - 46, - -75 - ] - ], - [ - [ - 13397, - 10103 - ], - [ - -19, - 90 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - 28, - 52 - ], - [ - 21, - 20 - ], - [ - 26, - -41 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - -25, - -26 - ], - [ - -11, - -30 - ], - [ - -3, - -53 - ], - [ - -17, - -12 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - -2, - -31 - ], - [ - -22, - -18 - ], - [ - -4, - -39 - ], - [ - -33, - -58 - ] - ], - [ - [ - 13952, - 15551 - ], - [ - -12, - 8 - ], - [ - -1, - 27 - ], - [ - -39, - 40 - ], - [ - -6, - 57 - ], - [ - 6, - 82 - ], - [ - 10, - 37 - ], - [ - -12, - 19 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -5, - 38 - ], - [ - 30, - 59 - ], - [ - 5, - -22 - ], - [ - 19, - 11 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 14, - -33 - ], - [ - 17, - -12 - ], - [ - 5, - -43 - ] - ], - [ - [ - 13983, - 15819 - ], - [ - -9, - -41 - ], - [ - 10, - -52 - ], - [ - 29, - -29 - ] - ], - [ - [ - 16143, - 13706 - ], - [ - 12, - 6 - ], - [ - 3, - -33 - ], - [ - 55, - 19 - ], - [ - 57, - -3 - ], - [ - 42, - -4 - ], - [ - 48, - 81 - ], - [ - 52, - 77 - ], - [ - 44, - 74 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 13, - -41 - ] - ], - [ - [ - 16469, - 13882 - ], - [ - 10, - -95 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - -36, - 0 - ], - [ - -5, - -78 - ], - [ - 12, - -17 - ], - [ - -32, - -24 - ], - [ - 0, - -49 - ], - [ - -20, - -49 - ], - [ - -2, - -49 - ] - ], - [ - [ - 16396, - 13521 - ], - [ - -14, - -25 - ], - [ - -210, - 61 - ], - [ - -26, - 121 - ], - [ - -3, - 28 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -42, - 4 - ], - [ - -75, - 0 - ], - [ - 0, - 267 - ] - ], - [ - [ - 7763, - 4482 - ], - [ - 27, - -55 - ], - [ - 35, - -90 - ], - [ - 90, - -72 - ], - [ - 98, - -30 - ], - [ - -31, - -60 - ], - [ - -67, - -6 - ], - [ - -35, - 42 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -64, - 19 - ], - [ - -169, - 16 - ], - [ - -28, - 70 - ], - [ - 1, - 90 - ], - [ - -47, - -8 - ], - [ - -24, - 43 - ], - [ - -6, - 128 - ], - [ - 53, - 52 - ], - [ - 22, - 76 - ], - [ - -8, - 61 - ], - [ - 37, - 102 - ], - [ - 26, - 159 - ], - [ - -8, - 71 - ], - [ - 31, - 22 - ], - [ - -8, - 46 - ], - [ - -32, - 24 - ], - [ - 23, - 50 - ], - [ - -32, - 46 - ], - [ - -16, - 138 - ], - [ - 28, - 24 - ], - [ - -12, - 147 - ], - [ - 17, - 122 - ], - [ - 18, - 107 - ], - [ - 42, - 44 - ], - [ - -21, - 117 - ], - [ - 0, - 110 - ], - [ - 52, - 79 - ], - [ - -1, - 100 - ], - [ - 40, - 117 - ], - [ - 0, - 110 - ], - [ - -18, - 22 - ], - [ - -32, - 207 - ], - [ - 43, - 124 - ], - [ - -7, - 116 - ], - [ - 25, - 109 - ], - [ - 46, - 113 - ], - [ - 49, - 74 - ], - [ - -21, - 47 - ], - [ - 14, - 39 - ], - [ - -2, - 200 - ], - [ - 76, - 59 - ], - [ - 24, - 125 - ], - [ - -8, - 30 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - 58, - 108 - ], - [ - 91, - -29 - ], - [ - 41, - -87 - ], - [ - 27, - 97 - ], - [ - 80, - -5 - ], - [ - 11, - -26 - ] - ], - [ - [ - 8178, - 8128 - ], - [ - 128, - -196 - ], - [ - 57, - -18 - ], - [ - 85, - -89 - ], - [ - 72, - -47 - ], - [ - 10, - -52 - ], - [ - -69, - -183 - ], - [ - 71, - -32 - ], - [ - 78, - -19 - ], - [ - 55, - 20 - ], - [ - 63, - 91 - ], - [ - 12, - 106 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 34, - 23 - ], - [ - 35, - -69 - ], - [ - -1, - -96 - ], - [ - -59, - -66 - ], - [ - -47, - -49 - ], - [ - -78, - -116 - ], - [ - -93, - -164 - ] - ], - [ - [ - 8531, - 7172 - ], - [ - -18, - -96 - ], - [ - -19, - -123 - ], - [ - 1, - -120 - ], - [ - -15, - -26 - ], - [ - -5, - -78 - ] - ], - [ - [ - 8475, - 6729 - ], - [ - -5, - -63 - ], - [ - 88, - -102 - ], - [ - -9, - -83 - ], - [ - 43, - -52 - ], - [ - -3, - -59 - ], - [ - -67, - -154 - ], - [ - -103, - -64 - ], - [ - -140, - -25 - ], - [ - -77, - 12 - ], - [ - 15, - -71 - ], - [ - -14, - -90 - ], - [ - 12, - -61 - ], - [ - -41, - -42 - ], - [ - -72, - -17 - ], - [ - -67, - 44 - ], - [ - -27, - -31 - ], - [ - 10, - -119 - ], - [ - 47, - -37 - ], - [ - 38, - 38 - ], - [ - 21, - -62 - ], - [ - -64, - -37 - ], - [ - -56, - -75 - ], - [ - -10, - -121 - ], - [ - -17, - -64 - ], - [ - -66, - 0 - ], - [ - -54, - -62 - ], - [ - -20, - -90 - ], - [ - 68, - -87 - ], - [ - 67, - -25 - ], - [ - -24, - -107 - ], - [ - -83, - -68 - ], - [ - -45, - -141 - ], - [ - -63, - -47 - ], - [ - -29, - -56 - ], - [ - 22, - -125 - ], - [ - 47, - -69 - ], - [ - -30, - 6 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - 96, - 19 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 15, - -32 - ], - [ - 26, - -21 - ], - [ - -14, - -30 - ], - [ - 38, - -41 - ], - [ - -20, - -38 - ], - [ - 29, - -33 - ], - [ - 32, - -19 - ], - [ - 1, - -84 - ] - ], - [ - [ - 15789, - 15448 - ], - [ - -25, - -3 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -28, - 69 - ], - [ - 0, - 19 - ], - [ - -31, - 0 - ], - [ - -20, - 32 - ], - [ - -15, - -3 - ] - ], - [ - [ - 15670, - 15562 - ], - [ - -27, - 35 - ], - [ - -52, - 29 - ], - [ - 6, - 59 - ], - [ - -11, - 42 - ] - ], - [ - [ - 8395, - 1195 - ], - [ - -21, - -61 - ], - [ - -20, - -54 - ], - [ - -146, - 16 - ], - [ - -156, - -7 - ], - [ - -87, - 40 - ], - [ - 0, - 5 - ], - [ - -38, - 35 - ], - [ - 157, - -5 - ], - [ - 150, - -11 - ], - [ - 52, - 49 - ], - [ - 36, - 42 - ], - [ - 73, - -49 - ] - ], - [ - [ - 1449, - 1260 - ], - [ - -133, - -16 - ], - [ - -92, - 42 - ], - [ - -41, - 42 - ], - [ - -3, - 7 - ], - [ - -45, - 33 - ], - [ - 43, - 45 - ], - [ - 129, - -19 - ], - [ - 70, - -38 - ], - [ - 53, - -42 - ], - [ - 19, - -54 - ] - ], - [ - [ - 9400, - 1434 - ], - [ - 86, - -52 - ], - [ - 30, - -73 - ], - [ - 8, - -51 - ], - [ - 3, - -61 - ], - [ - -108, - -38 - ], - [ - -113, - -31 - ], - [ - -131, - -28 - ], - [ - -147, - -23 - ], - [ - -165, - 7 - ], - [ - -91, - 40 - ], - [ - 12, - 49 - ], - [ - 149, - 33 - ], - [ - 60, - 40 - ], - [ - 44, - 52 - ], - [ - 31, - 44 - ], - [ - 42, - 43 - ], - [ - 45, - 49 - ], - [ - 36, - 0 - ], - [ - 104, - 26 - ], - [ - 105, - -26 - ] - ], - [ - [ - 4098, - 1979 - ], - [ - 90, - -18 - ], - [ - 83, - 21 - ], - [ - -39, - -43 - ], - [ - -66, - -30 - ], - [ - -97, - 9 - ], - [ - -69, - 43 - ], - [ - 15, - 40 - ], - [ - 83, - -22 - ] - ], - [ - [ - 3795, - 1982 - ], - [ - 106, - -47 - ], - [ - -41, - 4 - ], - [ - -90, - 12 - ], - [ - -95, - 33 - ], - [ - 50, - 26 - ], - [ - 70, - -28 - ] - ], - [ - [ - 5648, - 2167 - ], - [ - 76, - -16 - ], - [ - 77, - 14 - ], - [ - 41, - -68 - ], - [ - -55, - 9 - ], - [ - -85, - -4 - ], - [ - -86, - 4 - ], - [ - -94, - -7 - ], - [ - -71, - 24 - ], - [ - -37, - 49 - ], - [ - 44, - 21 - ], - [ - 89, - -16 - ], - [ - 101, - -10 - ] - ], - [ - [ - 7776, - 2285 - ], - [ - 8, - -54 - ], - [ - -12, - -47 - ], - [ - -19, - -45 - ], - [ - -82, - -16 - ], - [ - -78, - -24 - ], - [ - -92, - 2 - ], - [ - 35, - 47 - ], - [ - -82, - -16 - ], - [ - -78, - -17 - ], - [ - -53, - 36 - ], - [ - -5, - 49 - ], - [ - 77, - 47 - ], - [ - 48, - 14 - ], - [ - 80, - -5 - ], - [ - 21, - 62 - ], - [ - 4, - 44 - ], - [ - -2, - 97 - ], - [ - 40, - 56 - ], - [ - 64, - 19 - ], - [ - 37, - -45 - ], - [ - 17, - -44 - ], - [ - 30, - -55 - ], - [ - 23, - -51 - ], - [ - 19, - -54 - ] - ], - [ - [ - 8462, - 3101 - ], - [ - -30, - -26 - ], - [ - -52, - 19 - ], - [ - -58, - -12 - ], - [ - -47, - -28 - ], - [ - -51, - -31 - ], - [ - -34, - -35 - ], - [ - -10, - -47 - ], - [ - 4, - -45 - ], - [ - 33, - -40 - ], - [ - -48, - -28 - ], - [ - -65, - -9 - ], - [ - -38, - -40 - ], - [ - -41, - -38 - ], - [ - -44, - -51 - ], - [ - -11, - -45 - ], - [ - 25, - -50 - ], - [ - 37, - -37 - ], - [ - 57, - -28 - ], - [ - 53, - -38 - ], - [ - 29, - -47 - ], - [ - 15, - -45 - ], - [ - 20, - -47 - ], - [ - 33, - -40 - ], - [ - 21, - -44 - ], - [ - 9, - -111 - ], - [ - 21, - -44 - ], - [ - 5, - -47 - ], - [ - 22, - -47 - ], - [ - -10, - -64 - ], - [ - -38, - -49 - ], - [ - -41, - -40 - ], - [ - -93, - -17 - ], - [ - -31, - -42 - ], - [ - -42, - -40 - ], - [ - -106, - -45 - ], - [ - -92, - -18 - ], - [ - -88, - -26 - ], - [ - -94, - -26 - ], - [ - -56, - -50 - ], - [ - -112, - -4 - ], - [ - -123, - 4 - ], - [ - -110, - -9 - ], - [ - -118, - 0 - ], - [ - 22, - -47 - ], - [ - 107, - -21 - ], - [ - 77, - -33 - ], - [ - 44, - -42 - ], - [ - -78, - -38 - ], - [ - -120, - 12 - ], - [ - -100, - -31 - ], - [ - -4, - -49 - ], - [ - -2, - -47 - ], - [ - 82, - -40 - ], - [ - 15, - -45 - ], - [ - 88, - -44 - ], - [ - 148, - -19 - ], - [ - 125, - -33 - ], - [ - 100, - -38 - ], - [ - 127, - -37 - ], - [ - 173, - -19 - ], - [ - 171, - -33 - ], - [ - 119, - -35 - ], - [ - 130, - -40 - ], - [ - 68, - -57 - ], - [ - 34, - -44 - ], - [ - 85, - 42 - ], - [ - 114, - 35 - ], - [ - 122, - 38 - ], - [ - 144, - 30 - ], - [ - 125, - 33 - ], - [ - 173, - 3 - ], - [ - 171, - -17 - ], - [ - 140, - -28 - ], - [ - 45, - 52 - ], - [ - 97, - 35 - ], - [ - 177, - 2 - ], - [ - 137, - 26 - ], - [ - 131, - 26 - ], - [ - 145, - 16 - ], - [ - 154, - 22 - ], - [ - 108, - 30 - ], - [ - -49, - 42 - ], - [ - -30, - 43 - ], - [ - 0, - 44 - ], - [ - -135, - -4 - ], - [ - -143, - -19 - ], - [ - -137, - 0 - ], - [ - -19, - 45 - ], - [ - 10, - 89 - ], - [ - 31, - 26 - ], - [ - 100, - 28 - ], - [ - 117, - 28 - ], - [ - 85, - 35 - ], - [ - 84, - 36 - ], - [ - 63, - 47 - ], - [ - 96, - 21 - ], - [ - 94, - 16 - ], - [ - 48, - 10 - ], - [ - 108, - 4 - ], - [ - 102, - 17 - ], - [ - 86, - 23 - ], - [ - 85, - 29 - ], - [ - 76, - 28 - ], - [ - 97, - 37 - ], - [ - 61, - 40 - ], - [ - 66, - 36 - ], - [ - 20, - 47 - ], - [ - -73, - 28 - ], - [ - 24, - 49 - ], - [ - 47, - 38 - ], - [ - 72, - 23 - ], - [ - 77, - 29 - ], - [ - 71, - 37 - ], - [ - 54, - 47 - ], - [ - 34, - 57 - ], - [ - 51, - 33 - ], - [ - 83, - -7 - ], - [ - 34, - -40 - ], - [ - 83, - -5 - ], - [ - 3, - 45 - ], - [ - 36, - 47 - ], - [ - 75, - -12 - ], - [ - 18, - -45 - ], - [ - 83, - -7 - ], - [ - 90, - 21 - ], - [ - 87, - 14 - ], - [ - 80, - -7 - ], - [ - 30, - -49 - ], - [ - 76, - 40 - ], - [ - 71, - 21 - ], - [ - 79, - 16 - ], - [ - 78, - 17 - ], - [ - 71, - 28 - ], - [ - 78, - 19 - ], - [ - 60, - 26 - ], - [ - 42, - 42 - ], - [ - 52, - -30 - ], - [ - 72, - 16 - ], - [ - 51, - -56 - ], - [ - 40, - -43 - ], - [ - 79, - 24 - ], - [ - 31, - 47 - ], - [ - 71, - 33 - ], - [ - 92, - -7 - ], - [ - 27, - -45 - ], - [ - 57, - 45 - ], - [ - 75, - 14 - ], - [ - 82, - 4 - ], - [ - 74, - -2 - ], - [ - 78, - -14 - ], - [ - 75, - -7 - ], - [ - 33, - -40 - ], - [ - 45, - -35 - ], - [ - 76, - 21 - ], - [ - 82, - 5 - ], - [ - 79, - 0 - ], - [ - 78, - 2 - ], - [ - 70, - 16 - ], - [ - 74, - 15 - ], - [ - 61, - 32 - ], - [ - 65, - 22 - ], - [ - 71, - 11 - ], - [ - 54, - 33 - ], - [ - 38, - 66 - ], - [ - 40, - 40 - ], - [ - 72, - -19 - ], - [ - 27, - -42 - ], - [ - 60, - -28 - ], - [ - 73, - 9 - ], - [ - 49, - -42 - ], - [ - 52, - -31 - ], - [ - 71, - 28 - ], - [ - 24, - 52 - ], - [ - 63, - 21 - ], - [ - 72, - 40 - ], - [ - 69, - 17 - ], - [ - 82, - 23 - ], - [ - 54, - 26 - ], - [ - 58, - 28 - ], - [ - 54, - 26 - ], - [ - 66, - -14 - ], - [ - 63, - 42 - ], - [ - 45, - 33 - ], - [ - 65, - -2 - ], - [ - 57, - 28 - ], - [ - 14, - 42 - ], - [ - 59, - 33 - ], - [ - 57, - 24 - ], - [ - 70, - 19 - ], - [ - 64, - 9 - ], - [ - 61, - -7 - ], - [ - 66, - -12 - ], - [ - 56, - -33 - ], - [ - 7, - -51 - ], - [ - 61, - -40 - ], - [ - 42, - -33 - ], - [ - 84, - -14 - ], - [ - 46, - -33 - ], - [ - 58, - -33 - ], - [ - 66, - -7 - ], - [ - 56, - 23 - ], - [ - 60, - 50 - ], - [ - 66, - -26 - ], - [ - 68, - -14 - ], - [ - 66, - -14 - ], - [ - 68, - -10 - ], - [ - 70, - 0 - ], - [ - 57, - -124 - ], - [ - -3, - -31 - ], - [ - -8, - -54 - ], - [ - -67, - -31 - ], - [ - -54, - -44 - ], - [ - 9, - -47 - ], - [ - 78, - 2 - ], - [ - -10, - -47 - ], - [ - -35, - -45 - ], - [ - -33, - -49 - ], - [ - 53, - -38 - ], - [ - 81, - -11 - ], - [ - 81, - 21 - ], - [ - 38, - 47 - ], - [ - 23, - 45 - ], - [ - 38, - 37 - ], - [ - 44, - 35 - ], - [ - 18, - 43 - ], - [ - 36, - 58 - ], - [ - 44, - 12 - ], - [ - 79, - 5 - ], - [ - 70, - 14 - ], - [ - 71, - 19 - ], - [ - 34, - 47 - ], - [ - 21, - 45 - ], - [ - 47, - 44 - ], - [ - 69, - 31 - ], - [ - 58, - 23 - ], - [ - 39, - 40 - ], - [ - 39, - 21 - ], - [ - 51, - 19 - ], - [ - 69, - -12 - ], - [ - 63, - 12 - ], - [ - 68, - 14 - ], - [ - 77, - -7 - ], - [ - 50, - 33 - ], - [ - 36, - 80 - ], - [ - 26, - -33 - ], - [ - 33, - -56 - ], - [ - 58, - -24 - ], - [ - 67, - -9 - ], - [ - 67, - 14 - ], - [ - 71, - -9 - ], - [ - 66, - -3 - ], - [ - 43, - 12 - ], - [ - 59, - -7 - ], - [ - 53, - -26 - ], - [ - 63, - 16 - ], - [ - 75, - 0 - ], - [ - 64, - 17 - ], - [ - 73, - -17 - ], - [ - 46, - 40 - ], - [ - 36, - 40 - ], - [ - 47, - 33 - ], - [ - 88, - 90 - ], - [ - 45, - -17 - ], - [ - 53, - -33 - ], - [ - 46, - -42 - ], - [ - 89, - -73 - ], - [ - 69, - -2 - ], - [ - 64, - 0 - ], - [ - 75, - 14 - ], - [ - 75, - 16 - ], - [ - 57, - 33 - ], - [ - 48, - 35 - ], - [ - 78, - 5 - ], - [ - 52, - 26 - ], - [ - 54, - -23 - ], - [ - 36, - -38 - ], - [ - 49, - -38 - ], - [ - 76, - 5 - ], - [ - 48, - -31 - ], - [ - 83, - -30 - ], - [ - 88, - -12 - ], - [ - 72, - 10 - ], - [ - 55, - 37 - ], - [ - 46, - 38 - ], - [ - 63, - 9 - ], - [ - 63, - -16 - ], - [ - 72, - -12 - ], - [ - 66, - 19 - ], - [ - 63, - 0 - ], - [ - 61, - -12 - ], - [ - 64, - -12 - ], - [ - 63, - 21 - ], - [ - 75, - 19 - ], - [ - 71, - 5 - ], - [ - 79, - 0 - ], - [ - 64, - 12 - ], - [ - 63, - 9 - ], - [ - 19, - 59 - ], - [ - 3, - 49 - ], - [ - 44, - -33 - ], - [ - 12, - -54 - ], - [ - 23, - -49 - ], - [ - 29, - -40 - ], - [ - 59, - -21 - ], - [ - 79, - 7 - ], - [ - 91, - 2 - ], - [ - 63, - 7 - ], - [ - 92, - 0 - ], - [ - 65, - 3 - ], - [ - 92, - -5 - ], - [ - 77, - -10 - ], - [ - 50, - -37 - ], - [ - -14, - -45 - ], - [ - 45, - -35 - ], - [ - 75, - -28 - ], - [ - 78, - -31 - ], - [ - 90, - -21 - ], - [ - 94, - -19 - ], - [ - 71, - -19 - ], - [ - 79, - -2 - ], - [ - 45, - 40 - ], - [ - 62, - -33 - ], - [ - 53, - -38 - ], - [ - 62, - -28 - ], - [ - 84, - -12 - ], - [ - 81, - -14 - ], - [ - 34, - -47 - ], - [ - 79, - -28 - ], - [ - 53, - -42 - ], - [ - 78, - -19 - ], - [ - 81, - 2 - ], - [ - 75, - -7 - ], - [ - 83, - 3 - ], - [ - 83, - -10 - ], - [ - 78, - -16 - ], - [ - 73, - -28 - ], - [ - 72, - -24 - ], - [ - 49, - -35 - ], - [ - -8, - -47 - ], - [ - -37, - -42 - ], - [ - -31, - -55 - ], - [ - -25, - -42 - ], - [ - -33, - -49 - ], - [ - -91, - -19 - ], - [ - -41, - -42 - ], - [ - -90, - -26 - ], - [ - -32, - -47 - ], - [ - -47, - -45 - ], - [ - -51, - -38 - ], - [ - -29, - -49 - ], - [ - -17, - -45 - ], - [ - -7, - -54 - ], - [ - 1, - -44 - ], - [ - 40, - -47 - ], - [ - 15, - -45 - ], - [ - 32, - -42 - ], - [ - 130, - -17 - ], - [ - 27, - -51 - ], - [ - -125, - -19 - ], - [ - -107, - -26 - ], - [ - -132, - -5 - ], - [ - -59, - -68 - ], - [ - -12, - -56 - ], - [ - -30, - -45 - ], - [ - -37, - -45 - ], - [ - 93, - -40 - ], - [ - 35, - -49 - ], - [ - 60, - -45 - ], - [ - 85, - -40 - ], - [ - 97, - -37 - ], - [ - 105, - -38 - ], - [ - 160, - -38 - ], - [ - 35, - -58 - ], - [ - 201, - -26 - ], - [ - 14, - -9 - ], - [ - 52, - -36 - ], - [ - 192, - 31 - ], - [ - 160, - -38 - ], - [ - 120, - -29 - ], - [ - 0, - -634 - ], - [ - -25095, - 0 - ], - [ - 0, - 634 - ], - [ - 4, - -1 - ], - [ - 62, - 70 - ], - [ - 125, - -38 - ], - [ - 8, - 5 - ], - [ - 74, - 38 - ], - [ - 10, - -1 - ], - [ - 8, - -1 - ], - [ - 101, - -50 - ], - [ - 88, - 50 - ], - [ - 16, - 6 - ], - [ - 204, - 22 - ], - [ - 67, - -28 - ], - [ - 33, - -15 - ], - [ - 105, - -40 - ], - [ - 198, - -30 - ], - [ - 157, - -38 - ], - [ - 269, - -28 - ], - [ - 200, - 33 - ], - [ - 297, - -24 - ], - [ - 168, - -37 - ], - [ - 184, - 35 - ], - [ - 194, - 33 - ], - [ - 15, - 56 - ], - [ - -275, - 5 - ], - [ - -225, - 28 - ], - [ - -59, - 47 - ], - [ - -187, - 26 - ], - [ - 13, - 54 - ], - [ - 25, - 50 - ], - [ - 26, - 44 - ], - [ - -13, - 50 - ], - [ - -116, - 33 - ], - [ - -54, - 42 - ], - [ - -107, - 37 - ], - [ - 169, - -7 - ], - [ - 161, - 19 - ], - [ - 101, - -40 - ], - [ - 124, - 36 - ], - [ - 115, - 44 - ], - [ - 56, - 40 - ], - [ - -25, - 50 - ], - [ - -90, - 32 - ], - [ - -102, - 36 - ], - [ - -143, - 7 - ], - [ - -126, - 16 - ], - [ - -135, - 12 - ], - [ - -45, - 45 - ], - [ - -90, - 37 - ], - [ - -55, - 43 - ], - [ - -22, - 136 - ], - [ - 34, - -12 - ], - [ - 63, - -37 - ], - [ - 115, - 11 - ], - [ - 110, - 17 - ], - [ - 58, - -52 - ], - [ - 110, - 12 - ], - [ - 93, - 26 - ], - [ - 87, - 33 - ], - [ - 80, - 39 - ], - [ - 105, - 12 - ], - [ - -3, - 45 - ], - [ - -24, - 45 - ], - [ - 20, - 42 - ], - [ - 90, - 21 - ], - [ - 41, - -40 - ], - [ - 107, - 24 - ], - [ - 80, - 30 - ], - [ - 100, - 3 - ], - [ - 94, - 11 - ], - [ - 94, - 28 - ], - [ - 75, - 26 - ], - [ - 85, - 26 - ], - [ - 55, - -7 - ], - [ - 47, - -9 - ], - [ - 104, - 16 - ], - [ - 93, - -21 - ], - [ - 95, - 2 - ], - [ - 92, - 17 - ], - [ - 94, - -12 - ], - [ - 104, - -12 - ], - [ - 97, - 5 - ], - [ - 101, - -2 - ], - [ - 104, - -3 - ], - [ - 95, - 5 - ], - [ - 71, - 35 - ], - [ - 85, - 19 - ], - [ - 87, - -26 - ], - [ - 84, - 21 - ], - [ - 75, - 43 - ], - [ - 45, - -38 - ], - [ - 24, - -42 - ], - [ - 45, - -40 - ], - [ - 73, - 35 - ], - [ - 83, - -45 - ], - [ - 94, - -14 - ], - [ - 81, - -33 - ], - [ - 98, - 7 - ], - [ - 89, - 22 - ], - [ - 105, - -5 - ], - [ - 94, - -17 - ], - [ - 96, - -21 - ], - [ - 37, - 52 - ], - [ - -46, - 40 - ], - [ - -34, - 42 - ], - [ - -90, - 10 - ], - [ - -39, - 44 - ], - [ - -15, - 45 - ], - [ - -25, - 89 - ], - [ - 53, - -16 - ], - [ - 92, - -7 - ], - [ - 90, - 7 - ], - [ - 82, - -19 - ], - [ - 71, - -35 - ], - [ - 30, - -42 - ], - [ - 94, - -8 - ], - [ - 90, - 17 - ], - [ - 96, - 23 - ], - [ - 86, - 15 - ], - [ - 71, - -29 - ], - [ - 93, - 10 - ], - [ - 60, - 91 - ], - [ - 56, - -54 - ], - [ - 80, - -21 - ], - [ - 88, - 12 - ], - [ - 57, - -47 - ], - [ - 91, - -5 - ], - [ - 85, - -14 - ], - [ - 83, - -26 - ], - [ - 55, - 45 - ], - [ - 27, - 42 - ], - [ - 70, - -47 - ], - [ - 95, - 12 - ], - [ - 71, - -26 - ], - [ - 48, - -40 - ], - [ - 93, - 12 - ], - [ - 72, - 26 - ], - [ - 71, - 30 - ], - [ - 85, - 17 - ], - [ - 98, - 14 - ], - [ - 89, - 16 - ], - [ - 68, - 26 - ], - [ - 41, - 38 - ], - [ - 17, - 52 - ], - [ - -8, - 49 - ], - [ - -22, - 47 - ], - [ - -25, - 47 - ], - [ - -22, - 47 - ], - [ - -18, - 42 - ], - [ - -4, - 47 - ], - [ - 7, - 47 - ], - [ - 33, - 45 - ], - [ - 27, - 49 - ], - [ - 11, - 47 - ], - [ - -13, - 52 - ], - [ - -9, - 47 - ], - [ - 35, - 54 - ], - [ - 38, - 35 - ], - [ - 45, - 45 - ], - [ - 48, - 38 - ], - [ - 56, - 35 - ], - [ - 27, - 52 - ], - [ - 38, - 33 - ], - [ - 44, - 30 - ], - [ - 67, - 7 - ], - [ - 43, - 38 - ], - [ - 50, - 23 - ], - [ - 57, - 14 - ], - [ - 50, - 31 - ], - [ - 40, - 38 - ], - [ - 55, - 14 - ], - [ - 41, - -31 - ], - [ - -26, - -40 - ], - [ - -71, - -35 - ] - ], - [ - [ - 17353, - 4964 - ], - [ - 45, - -38 - ], - [ - 66, - -15 - ], - [ - 2, - -23 - ], - [ - -19, - -54 - ], - [ - -107, - -8 - ], - [ - -2, - 64 - ], - [ - 10, - 49 - ], - [ - 5, - 25 - ] - ], - [ - [ - 22683, - 5903 - ], - [ - 67, - -41 - ], - [ - 38, - 16 - ], - [ - 55, - 23 - ], - [ - 41, - -8 - ], - [ - 5, - -142 - ], - [ - -23, - -41 - ], - [ - -8, - -97 - ], - [ - -24, - 33 - ], - [ - -48, - -84 - ], - [ - -15, - 7 - ], - [ - -43, - 4 - ], - [ - -43, - 102 - ], - [ - -9, - 79 - ], - [ - -40, - 105 - ], - [ - 1, - 55 - ], - [ - 46, - -11 - ] - ], - [ - [ - 22555, - 9146 - ], - [ - 25, - -94 - ], - [ - 45, - 45 - ], - [ - 23, - -51 - ], - [ - 33, - -47 - ], - [ - -7, - -53 - ], - [ - 15, - -103 - ], - [ - 11, - -59 - ], - [ - 17, - -15 - ], - [ - 19, - -103 - ], - [ - -7, - -62 - ], - [ - 23, - -81 - ], - [ - 75, - -63 - ], - [ - 50, - -57 - ], - [ - 46, - -52 - ], - [ - -9, - -29 - ], - [ - 40, - -75 - ], - [ - 27, - -130 - ], - [ - 28, - 26 - ], - [ - 28, - -52 - ], - [ - 17, - 19 - ], - [ - 12, - -128 - ], - [ - 50, - -73 - ], - [ - 32, - -46 - ], - [ - 55, - -97 - ], - [ - 19, - -97 - ], - [ - 2, - -68 - ], - [ - -5, - -74 - ], - [ - 34, - -102 - ], - [ - -4, - -106 - ], - [ - -12, - -56 - ], - [ - -19, - -107 - ], - [ - 1, - -69 - ], - [ - -14, - -86 - ], - [ - -30, - -109 - ], - [ - -52, - -59 - ], - [ - -26, - -93 - ], - [ - -23, - -59 - ], - [ - -20, - -104 - ], - [ - -27, - -59 - ], - [ - -18, - -90 - ], - [ - -9, - -83 - ], - [ - 4, - -38 - ], - [ - -40, - -41 - ], - [ - -78, - -5 - ], - [ - -65, - -49 - ], - [ - -32, - -46 - ], - [ - -42, - -52 - ], - [ - -58, - 53 - ], - [ - -42, - 21 - ], - [ - 10, - 63 - ], - [ - -38, - -23 - ], - [ - -61, - -87 - ], - [ - -60, - 33 - ], - [ - -39, - 19 - ], - [ - -40, - 8 - ], - [ - -68, - 35 - ], - [ - -45, - 74 - ], - [ - -13, - 91 - ], - [ - -16, - 61 - ], - [ - -34, - 48 - ], - [ - -67, - 15 - ], - [ - 23, - 58 - ], - [ - -17, - 89 - ], - [ - -34, - -83 - ], - [ - -62, - -22 - ], - [ - 36, - 66 - ], - [ - 11, - 70 - ], - [ - 27, - 58 - ], - [ - -6, - 89 - ], - [ - -57, - -102 - ], - [ - -43, - -41 - ], - [ - -27, - -96 - ], - [ - -54, - 50 - ], - [ - 2, - 63 - ], - [ - -44, - 87 - ], - [ - -37, - 45 - ], - [ - 14, - 28 - ], - [ - -90, - 73 - ], - [ - -49, - 3 - ], - [ - -67, - 59 - ], - [ - -125, - -12 - ], - [ - -90, - -43 - ], - [ - -79, - -40 - ], - [ - -67, - 8 - ], - [ - -74, - -61 - ], - [ - -60, - -28 - ], - [ - -14, - -63 - ], - [ - -25, - -49 - ], - [ - -60, - -2 - ], - [ - -43, - -11 - ], - [ - -62, - 22 - ], - [ - -50, - -13 - ], - [ - -48, - -6 - ], - [ - -41, - -64 - ], - [ - -21, - 6 - ], - [ - -35, - -34 - ], - [ - -33, - -38 - ], - [ - -51, - 4 - ], - [ - -47, - 0 - ], - [ - -74, - 77 - ], - [ - -37, - 23 - ], - [ - 1, - 68 - ], - [ - 35, - 17 - ], - [ - 12, - 27 - ], - [ - -3, - 43 - ], - [ - 9, - 84 - ], - [ - -8, - 71 - ], - [ - -37, - 121 - ], - [ - -11, - 68 - ], - [ - 3, - 69 - ], - [ - -28, - 78 - ], - [ - -2, - 35 - ], - [ - -31, - 48 - ], - [ - -8, - 94 - ], - [ - -40, - 95 - ], - [ - -10, - 51 - ], - [ - 31, - -52 - ], - [ - -24, - 111 - ], - [ - 35, - -34 - ], - [ - 20, - -47 - ], - [ - -1, - 62 - ], - [ - -34, - 94 - ], - [ - -7, - 38 - ], - [ - -16, - 36 - ], - [ - 8, - 69 - ], - [ - 14, - 30 - ], - [ - 9, - 60 - ], - [ - -7, - 70 - ], - [ - 29, - 86 - ], - [ - 5, - -91 - ], - [ - 29, - 82 - ], - [ - 57, - 40 - ], - [ - 34, - 52 - ], - [ - 53, - 44 - ], - [ - 32, - 9 - ], - [ - 19, - -15 - ], - [ - 55, - 45 - ], - [ - 42, - 13 - ], - [ - 11, - 27 - ], - [ - 18, - 10 - ], - [ - 39, - -2 - ], - [ - 73, - 35 - ], - [ - 38, - 53 - ], - [ - 18, - 64 - ], - [ - 41, - 61 - ], - [ - 3, - 48 - ], - [ - 2, - 65 - ], - [ - 49, - 102 - ], - [ - 29, - -103 - ], - [ - 30, - 23 - ], - [ - -25, - 57 - ], - [ - 22, - 58 - ], - [ - 30, - -26 - ], - [ - 9, - 92 - ], - [ - 38, - 59 - ], - [ - 17, - 47 - ], - [ - 35, - 20 - ], - [ - 1, - 34 - ], - [ - 30, - -14 - ], - [ - 2, - 30 - ], - [ - 30, - 17 - ], - [ - 34, - 16 - ], - [ - 52, - -55 - ], - [ - 38, - -71 - ], - [ - 44, - 0 - ], - [ - 44, - -12 - ], - [ - -15, - 66 - ], - [ - 34, - 96 - ], - [ - 31, - 32 - ], - [ - -11, - 30 - ], - [ - 31, - 68 - ], - [ - 42, - 43 - ], - [ - 36, - -15 - ], - [ - 58, - 23 - ], - [ - -1, - 61 - ], - [ - -51, - 40 - ], - [ - 37, - 17 - ], - [ - 46, - -30 - ], - [ - 37, - -49 - ], - [ - 59, - -31 - ], - [ - 20, - 13 - ], - [ - 43, - -37 - ], - [ - 41, - 34 - ], - [ - 26, - -10 - ], - [ - 16, - 23 - ], - [ - 32, - -60 - ], - [ - -18, - -64 - ], - [ - -27, - -48 - ], - [ - -24, - -4 - ], - [ - 8, - -48 - ], - [ - -20, - -60 - ], - [ - -25, - -59 - ], - [ - 5, - -34 - ], - [ - 55, - -66 - ], - [ - 54, - -39 - ], - [ - 36, - -41 - ], - [ - 50, - -71 - ], - [ - 20, - 0 - ], - [ - 37, - -31 - ], - [ - 10, - -37 - ], - [ - 67, - -41 - ], - [ - 46, - 41 - ], - [ - 13, - 65 - ], - [ - 14, - 53 - ], - [ - 9, - 66 - ], - [ - 21, - 95 - ], - [ - -9, - 58 - ], - [ - 5, - 35 - ], - [ - -8, - 69 - ], - [ - 9, - 90 - ], - [ - 13, - 25 - ], - [ - -11, - 40 - ], - [ - 17, - 63 - ], - [ - 13, - 66 - ], - [ - 2, - 34 - ], - [ - 26, - 45 - ], - [ - 20, - -58 - ], - [ - 5, - -76 - ], - [ - 17, - -14 - ], - [ - 3, - -51 - ], - [ - 25, - -61 - ], - [ - 5, - -67 - ], - [ - -2, - -44 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - -5, - -50 - ], - [ - -39, - 0 - ], - [ - 13, - -26 - ], - [ - -23, - -77 - ] - ], - [ - [ - 13677, - 16418 - ], - [ - -13, - -20 - ], - [ - -61, - -3 - ], - [ - -35, - -27 - ], - [ - -58, - 9 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -100, - 31 - ], - [ - -15, - 42 - ], - [ - -69, - -21 - ], - [ - -8, - -23 - ], - [ - -43, - 17 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -35, - 3 - ], - [ - -32, - 22 - ], - [ - 11, - 29 - ], - [ - -3, - 22 - ] - ], - [ - [ - 13216, - 16499 - ], - [ - 21, - 6 - ], - [ - 36, - -33 - ], - [ - 10, - 32 - ], - [ - 61, - -5 - ], - [ - 50, - 21 - ], - [ - 33, - -4 - ], - [ - 22, - -24 - ], - [ - 7, - 20 - ], - [ - -10, - 78 - ], - [ - 25, - 16 - ], - [ - 24, - 55 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - 52, - -39 - ], - [ - 39, - 49 - ], - [ - 25, - 9 - ], - [ - 54, - -36 - ], - [ - 33, - 6 - ], - [ - 32, - -23 - ] - ], - [ - [ - 13730, - 16627 - ], - [ - -6, - -15 - ], - [ - 7, - -41 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 18, - 19 - ], - [ - 51, - -34 - ], - [ - 38, - -7 - ], - [ - 10, - 14 - ], - [ - -35, - 65 - ], - [ - 18, - 16 - ] - ], - [ - [ - 15782, - 15819 - ], - [ - 20, - -4 - ], - [ - 48, - -73 - ], - [ - 31, - -8 - ], - [ - 12, - 31 - ], - [ - 41, - 48 - ] - ], - [ - [ - 15934, - 15813 - ], - [ - 37, - -63 - ], - [ - 35, - -85 - ], - [ - 33, - -6 - ], - [ - 21, - -32 - ], - [ - -57, - -10 - ], - [ - -12, - -93 - ], - [ - -12, - -42 - ], - [ - -26, - -28 - ], - [ - 2, - -60 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - -17, - -6 - ], - [ - -44, - 63 - ], - [ - 24, - 60 - ], - [ - -20, - 35 - ], - [ - -26, - -9 - ], - [ - -83, - -89 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -48, - 16 - ], - [ - -35, - 55 - ], - [ - -11, - 46 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - -5, - 145 - ], - [ - -17, - 55 - ] - ], - [ - [ - 14571, - 10457 - ], - [ - 42, - -10 - ], - [ - 21, - 68 - ], - [ - 37, - -7 - ] - ], - [ - [ - 14671, - 10508 - ], - [ - 5, - -48 - ], - [ - 15, - -27 - ], - [ - 0, - -39 - ], - [ - -17, - -25 - ], - [ - -27, - -62 - ], - [ - -25, - -44 - ], - [ - -29, - -6 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -8, - -81 - ] - ], - [ - [ - 12969, - 16811 - ], - [ - -18, - -5 - ], - [ - -8, - -67 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - -61, - 55 - ], - [ - -36, - -9 - ], - [ - -48, - 56 - ], - [ - -33, - 48 - ], - [ - -32, - 2 - ], - [ - -10, - 42 - ] - ], - [ - [ - 12723, - 16933 - ], - [ - 56, - 24 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 51, - -9 - ], - [ - 64, - 25 - ], - [ - 44, - -53 - ], - [ - 39, - -28 - ] - ], - [ - [ - 12735, - 11548 - ], - [ - -57, - -14 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -18, - 83 - ], - [ - 4, - 275 - ], - [ - -15, - 25 - ], - [ - -2, - 59 - ], - [ - -24, - 42 - ], - [ - -22, - 35 - ], - [ - 9, - 64 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - 24, - 13 - ], - [ - 14, - 53 - ], - [ - 34, - 11 - ], - [ - 16, - 36 - ] - ], - [ - [ - 12698, - 12230 - ], - [ - 23, - 35 - ], - [ - 25, - 0 - ], - [ - 53, - -69 - ] - ], - [ - [ - 12799, - 12196 - ], - [ - -2, - -40 - ], - [ - 15, - -71 - ], - [ - -14, - -48 - ], - [ - 8, - -33 - ], - [ - -34, - -74 - ], - [ - -21, - -37 - ], - [ - -14, - -75 - ], - [ - 2, - -77 - ], - [ - -4, - -193 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - -61, - 2 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -32, - 10 - ], - [ - -23, - -20 - ], - [ - -30, - 9 - ], - [ - -121, - -6 - ], - [ - -2, - -68 - ], - [ - 9, - -90 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - -47, - 31 - ], - [ - -33, - -5 - ], - [ - -24, - -30 - ], - [ - -32, - 26 - ], - [ - -12, - 39 - ], - [ - -31, - 26 - ] - ], - [ - [ - 12171, - 12041 - ], - [ - -5, - 70 - ], - [ - 19, - 51 - ], - [ - -1, - 40 - ], - [ - 55, - 100 - ], - [ - 10, - 82 - ], - [ - 19, - 29 - ], - [ - 34, - -16 - ], - [ - 29, - 25 - ], - [ - 10, - 31 - ], - [ - 54, - 53 - ], - [ - 13, - 38 - ], - [ - 65, - 50 - ], - [ - 39, - 17 - ], - [ - 17, - -23 - ], - [ - 45, - 0 - ] - ], - [ - [ - 12574, - 12588 - ], - [ - -6, - -58 - ], - [ - 9, - -55 - ], - [ - 40, - -78 - ], - [ - 2, - -58 - ], - [ - 80, - -27 - ], - [ - -1, - -82 - ] - ], - [ - [ - 19008, - 13441 - ], - [ - -2, - -86 - ], - [ - -24, - 19 - ], - [ - 4, - -97 - ] - ], - [ - [ - 18986, - 13277 - ], - [ - -20, - 63 - ], - [ - -4, - 61 - ], - [ - -13, - 57 - ], - [ - -29, - 70 - ], - [ - -64, - 5 - ], - [ - 6, - -49 - ], - [ - -22, - -67 - ], - [ - -29, - 24 - ], - [ - -11, - -22 - ], - [ - -19, - 13 - ], - [ - -27, - 11 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -11, - 99 - ], - [ - -24, - 90 - ], - [ - 12, - 72 - ], - [ - -43, - 33 - ], - [ - 15, - 43 - ], - [ - 44, - 45 - ], - [ - -51, - 64 - ], - [ - 25, - 81 - ], - [ - 55, - -52 - ], - [ - 34, - -6 - ], - [ - 6, - -83 - ], - [ - 66, - -17 - ], - [ - 65, - 2 - ], - [ - 40, - -20 - ], - [ - -32, - -102 - ], - [ - -31, - -7 - ], - [ - -22, - -68 - ], - [ - 38, - -62 - ], - [ - 12, - 76 - ], - [ - 19, - 1 - ], - [ - 37, - -191 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - 20, - -49 - ], - [ - 27, - 8 - ], - [ - 54, - -18 - ], - [ - 102, - -7 - ], - [ - 34, - 31 - ], - [ - 83, - 28 - ], - [ - 50, - -44 - ], - [ - 41, - -12 - ] - ], - [ - [ - 14538, - 16041 - ], - [ - -36, - -50 - ], - [ - -25, - -86 - ], - [ - 22, - -68 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - -60, - 16 - ], - [ - -71, - -38 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - -1, - -60 - ], - [ - -63, - -11 - ], - [ - -49, - 42 - ], - [ - -56, - -33 - ], - [ - -52, - 3 - ] - ], - [ - [ - 14147, - 15756 - ], - [ - -4, - 80 - ], - [ - -35, - 38 - ] - ], - [ - [ - 14108, - 15874 - ], - [ - 11, - 17 - ], - [ - -7, - 15 - ], - [ - 11, - 38 - ], - [ - 27, - 37 - ], - [ - -34, - 52 - ], - [ - -6, - 44 - ], - [ - 17, - 27 - ] - ], - [ - [ - 7143, - 13648 - ], - [ - -17, - -6 - ], - [ - -18, - 69 - ], - [ - -26, - 35 - ], - [ - 15, - 76 - ], - [ - 21, - -5 - ], - [ - 24, - -100 - ], - [ - 1, - -69 - ] - ], - [ - [ - 7123, - 13986 - ], - [ - -76, - -19 - ], - [ - -5, - 44 - ], - [ - 33, - 10 - ], - [ - 46, - -4 - ], - [ - 2, - -31 - ] - ], - [ - [ - 7180, - 13987 - ], - [ - -12, - -85 - ], - [ - -13, - 15 - ], - [ - 1, - 63 - ], - [ - -31, - 47 - ], - [ - 0, - 14 - ], - [ - 55, - -54 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - -13, - -11 - ], - [ - -23, - -28 - ], - [ - -10, - -66 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -61, - 45 - ], - [ - -27, - 50 - ], - [ - -26, - 27 - ], - [ - -32, - 45 - ], - [ - -15, - 37 - ], - [ - -35, - 56 - ], - [ - 15, - 50 - ], - [ - 25, - -28 - ], - [ - 15, - 25 - ], - [ - 33, - 3 - ], - [ - 60, - -20 - ], - [ - 48, - 2 - ], - [ - 31, - -27 - ] - ], - [ - [ - 13872, - 16179 - ], - [ - 26, - 0 - ], - [ - -18, - -52 - ], - [ - 34, - -47 - ], - [ - -10, - -56 - ], - [ - -17, - -5 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - 67, - -1 - ], - [ - 76, - 45 - ], - [ - 16, - 68 - ], - [ - 57, - 39 - ], - [ - -7, - 53 - ] - ], - [ - [ - 14394, - 17469 - ], - [ - 43, - 20 - ], - [ - 75, - 47 - ] - ], - [ - [ - 14512, - 17536 - ], - [ - 73, - -30 - ], - [ - 10, - -30 - ], - [ - 37, - 14 - ], - [ - 68, - -28 - ], - [ - 6, - -57 - ], - [ - -14, - -32 - ], - [ - 43, - -79 - ], - [ - 29, - -22 - ], - [ - -5, - -21 - ], - [ - 47, - -21 - ], - [ - 21, - -32 - ], - [ - -28, - -27 - ], - [ - -56, - 5 - ], - [ - -13, - -12 - ], - [ - 16, - -39 - ], - [ - 17, - -77 - ] - ], - [ - [ - 14763, - 17048 - ], - [ - -60, - -7 - ], - [ - -21, - -27 - ], - [ - -5, - -60 - ], - [ - -27, - 12 - ], - [ - -63, - -6 - ], - [ - -18, - 28 - ], - [ - -27, - -21 - ], - [ - -26, - 17 - ], - [ - -55, - 3 - ], - [ - -78, - 28 - ], - [ - -70, - 10 - ], - [ - -54, - -3 - ], - [ - -38, - -32 - ], - [ - -33, - -5 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - -2, - 53 - ], - [ - -21, - 56 - ], - [ - 42, - 24 - ], - [ - 0, - 48 - ], - [ - -19, - 46 - ], - [ - -3, - 53 - ] - ], - [ - [ - 6333, - 12934 - ], - [ - 0, - 17 - ], - [ - 8, - 6 - ], - [ - 13, - -14 - ], - [ - 25, - 72 - ], - [ - 13, - 2 - ] - ], - [ - [ - 6392, - 13017 - ], - [ - 1, - -18 - ], - [ - 13, - -1 - ], - [ - -1, - -32 - ], - [ - -12, - -52 - ], - [ - 6, - -19 - ], - [ - -7, - -43 - ], - [ - 4, - -11 - ], - [ - -8, - -61 - ], - [ - -13, - -31 - ], - [ - -13, - -4 - ], - [ - -14, - -42 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - -21, - 0 - ], - [ - 6, - 136 - ], - [ - 0, - 95 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - -51, - -17 - ], - [ - -27, - 166 - ], - [ - -37, - 134 - ], - [ - 22, - 116 - ], - [ - -37, - 51 - ], - [ - -9, - 87 - ], - [ - -35, - 81 - ] - ], - [ - [ - 7696, - 8688 - ], - [ - 44, - 130 - ], - [ - -30, - 100 - ], - [ - 16, - 41 - ], - [ - -12, - 44 - ], - [ - 27, - 60 - ], - [ - 2, - 102 - ], - [ - 3, - 85 - ], - [ - 15, - 40 - ], - [ - -60, - 193 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - 52, - -10 - ], - [ - 35, - 3 - ], - [ - 16, - 36 - ], - [ - 61, - 49 - ], - [ - 37, - 45 - ], - [ - 91, - 20 - ], - [ - -8, - -90 - ], - [ - 9, - -46 - ], - [ - -6, - -80 - ], - [ - 76, - -108 - ], - [ - 78, - -20 - ], - [ - 28, - -44 - ], - [ - 47, - -24 - ], - [ - 29, - -35 - ], - [ - 43, - 1 - ], - [ - 41, - -35 - ], - [ - 3, - -70 - ], - [ - 14, - -35 - ], - [ - 0, - -52 - ], - [ - -20, - -2 - ], - [ - 27, - -139 - ], - [ - 134, - -5 - ], - [ - -11, - -70 - ], - [ - 8, - -47 - ], - [ - 38, - -34 - ], - [ - 16, - -74 - ], - [ - -12, - -95 - ], - [ - -19, - -52 - ], - [ - 7, - -69 - ], - [ - -22, - -24 - ] - ], - [ - [ - 8493, - 8377 - ], - [ - -1, - 37 - ], - [ - -65, - 61 - ], - [ - -65, - 2 - ], - [ - -122, - -35 - ], - [ - -33, - -106 - ], - [ - -2, - -64 - ], - [ - -27, - -144 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 13, - 70 - ], - [ - 10, - 70 - ], - [ - 0, - 66 - ], - [ - -25, - 22 - ], - [ - -26, - -19 - ], - [ - -26, - 5 - ], - [ - -9, - 46 - ], - [ - -6, - 110 - ], - [ - -13, - 36 - ], - [ - -47, - 33 - ], - [ - -29, - -24 - ], - [ - -73, - 23 - ], - [ - 4, - 163 - ], - [ - -20, - 67 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - -40, - -20 - ], - [ - -31, - 13 - ], - [ - 4, - 183 - ], - [ - -57, - -71 - ], - [ - -61, - 3 - ], - [ - -27, - 64 - ], - [ - -46, - 7 - ], - [ - 15, - 52 - ], - [ - -39, - 73 - ], - [ - -29, - 108 - ], - [ - 18, - 22 - ], - [ - 0, - 50 - ], - [ - 42, - 35 - ], - [ - -7, - 65 - ], - [ - 18, - 41 - ], - [ - 5, - 56 - ], - [ - 80, - 82 - ], - [ - 57, - 23 - ], - [ - 10, - 18 - ], - [ - 62, - -5 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - 32, - 328 - ], - [ - 1, - 53 - ], - [ - -11, - 68 - ], - [ - -31, - 44 - ], - [ - 1, - 87 - ], - [ - 39, - 20 - ], - [ - 14, - -13 - ], - [ - 2, - 46 - ], - [ - -40, - 13 - ], - [ - -1, - 75 - ], - [ - 135, - -3 - ], - [ - 24, - 42 - ], - [ - 19, - -38 - ], - [ - 14, - -71 - ], - [ - 13, - 15 - ] - ], - [ - [ - 7886, - 10948 - ], - [ - 38, - -64 - ], - [ - 54, - 8 - ], - [ - 14, - 37 - ], - [ - 52, - 28 - ], - [ - 28, - 19 - ], - [ - 8, - 51 - ], - [ - 50, - 34 - ], - [ - -4, - 25 - ], - [ - -59, - 11 - ], - [ - -9, - 75 - ], - [ - 2, - 81 - ], - [ - -31, - 31 - ], - [ - 13, - 11 - ], - [ - 52, - -15 - ], - [ - 55, - -30 - ], - [ - 21, - 28 - ], - [ - 50, - 19 - ], - [ - 78, - 44 - ], - [ - 25, - 46 - ], - [ - -9, - 34 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - 36, - 5 - ], - [ - 16, - -27 - ], - [ - -9, - -53 - ], - [ - 24, - -18 - ], - [ - 16, - -56 - ], - [ - -19, - -42 - ], - [ - -11, - -102 - ], - [ - 18, - -61 - ], - [ - 5, - -55 - ], - [ - 43, - -57 - ], - [ - 34, - -6 - ], - [ - 7, - 24 - ], - [ - 23, - 5 - ], - [ - 31, - 21 - ], - [ - 23, - 32 - ], - [ - 38, - -10 - ], - [ - 17, - 4 - ] - ], - [ - [ - 8606, - 11025 - ], - [ - 38, - -10 - ], - [ - 6, - 25 - ], - [ - -11, - 24 - ], - [ - 7, - 34 - ], - [ - 28, - -10 - ], - [ - 33, - 12 - ], - [ - 40, - -25 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 30, - -25 - ], - [ - 22, - 32 - ], - [ - 15, - -5 - ], - [ - 10, - -33 - ], - [ - 33, - 8 - ], - [ - 27, - 46 - ], - [ - 21, - 88 - ], - [ - 42, - 110 - ] - ], - [ - [ - 8947, - 11296 - ], - [ - 23, - 5 - ], - [ - 18, - -66 - ], - [ - 39, - -210 - ], - [ - 37, - -19 - ], - [ - 2, - -83 - ], - [ - -53, - -99 - ], - [ - 22, - -36 - ], - [ - 123, - -19 - ], - [ - 3, - -120 - ], - [ - 53, - 78 - ], - [ - 87, - -43 - ], - [ - 116, - -73 - ], - [ - 34, - -70 - ], - [ - -11, - -67 - ], - [ - 81, - 37 - ], - [ - 136, - -63 - ], - [ - 104, - 5 - ], - [ - 103, - -100 - ], - [ - 89, - -134 - ], - [ - 53, - -35 - ], - [ - 60, - -5 - ], - [ - 25, - -37 - ], - [ - 24, - -153 - ], - [ - 12, - -73 - ], - [ - -28, - -198 - ], - [ - -36, - -78 - ], - [ - -98, - -167 - ], - [ - -44, - -136 - ], - [ - -52, - -104 - ], - [ - -17, - -2 - ], - [ - -20, - -89 - ], - [ - 5, - -224 - ], - [ - -19, - -185 - ], - [ - -8, - -79 - ], - [ - -22, - -48 - ], - [ - -12, - -160 - ], - [ - -71, - -157 - ], - [ - -12, - -124 - ], - [ - -56, - -52 - ], - [ - -16, - -71 - ], - [ - -76, - 0 - ], - [ - -110, - -46 - ], - [ - -49, - -54 - ], - [ - -78, - -35 - ], - [ - -82, - -95 - ], - [ - -59, - -119 - ], - [ - -10, - -90 - ], - [ - 11, - -66 - ], - [ - -13, - -121 - ], - [ - -15, - -59 - ], - [ - -49, - -66 - ], - [ - -77, - -211 - ], - [ - -62, - -95 - ], - [ - -47, - -56 - ], - [ - -32, - -114 - ], - [ - -46, - -69 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -19, - 68 - ], - [ - 30, - 57 - ], - [ - -40, - 82 - ], - [ - -55, - 66 - ], - [ - -71, - 77 - ], - [ - -26, - -4 - ], - [ - -70, - 93 - ], - [ - -45, - -13 - ] - ], - [ - [ - 20508, - 11340 - ], - [ - 28, - 45 - ], - [ - 59, - 66 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - -3, - -59 - ], - [ - -4, - -77 - ], - [ - -33, - 4 - ], - [ - -15, - -41 - ], - [ - -32, - 62 - ] - ], - [ - [ - 18940, - 14129 - ], - [ - 28, - -38 - ], - [ - -5, - -74 - ], - [ - -57, - -4 - ], - [ - -59, - 8 - ], - [ - -44, - -18 - ], - [ - -63, - 45 - ], - [ - -1, - 24 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - 46, - 89 - ], - [ - 37, - 31 - ], - [ - 50, - -28 - ], - [ - 37, - -3 - ], - [ - 31, - -32 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - -98, - -88 - ], - [ - -63, - -90 - ], - [ - -23, - -80 - ], - [ - -21, - -45 - ], - [ - -38, - -10 - ], - [ - -12, - -57 - ], - [ - -7, - -37 - ], - [ - -45, - -28 - ], - [ - -57, - 6 - ], - [ - -33, - 33 - ], - [ - -29, - 15 - ], - [ - -34, - -28 - ], - [ - -18, - -58 - ], - [ - -33, - -36 - ], - [ - -34, - -53 - ], - [ - -50, - -12 - ], - [ - -16, - 42 - ], - [ - 7, - 73 - ], - [ - -42, - 114 - ], - [ - -19, - 18 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - 350 - ], - [ - 69, - 4 - ], - [ - 2, - 427 - ], - [ - 52, - 4 - ], - [ - 108, - 42 - ], - [ - 26, - -49 - ], - [ - 45, - 47 - ], - [ - 21, - 0 - ], - [ - 39, - 27 - ] - ], - [ - [ - 14296, - 8678 - ], - [ - 13, - -9 - ] - ], - [ - [ - 14309, - 8669 - ], - [ - 26, - -96 - ], - [ - 14, - -21 - ], - [ - 22, - -69 - ], - [ - 79, - -132 - ], - [ - 30, - -13 - ], - [ - 0, - -42 - ], - [ - 21, - -76 - ], - [ - 54, - -19 - ], - [ - 44, - -54 - ] - ], - [ - [ - 13613, - 11688 - ], - [ - 57, - 9 - ], - [ - 13, - 30 - ], - [ - 12, - -2 - ], - [ - 17, - -27 - ], - [ - 88, - 46 - ], - [ - 29, - 47 - ], - [ - 37, - 42 - ], - [ - -7, - 42 - ], - [ - 20, - 11 - ], - [ - 67, - -8 - ], - [ - 65, - 56 - ], - [ - 51, - 131 - ], - [ - 35, - 48 - ], - [ - 44, - 21 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 8, - -51 - ], - [ - 40, - -75 - ], - [ - 1, - -49 - ], - [ - -12, - -50 - ], - [ - 5, - -38 - ], - [ - 24, - -34 - ], - [ - 53, - -53 - ] - ], - [ - [ - 14260, - 11784 - ], - [ - 38, - -48 - ], - [ - 1, - -39 - ], - [ - 47, - -63 - ], - [ - 29, - -51 - ], - [ - 17, - -72 - ], - [ - 53, - -48 - ], - [ - 11, - -38 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - -23, - -13 - ], - [ - -45, - 3 - ], - [ - -52, - 13 - ], - [ - -26, - -11 - ], - [ - -11, - -29 - ], - [ - -22, - -3 - ], - [ - -28, - 25 - ], - [ - -77, - -60 - ], - [ - -32, - 12 - ], - [ - -10, - -9 - ], - [ - -21, - -72 - ], - [ - -52, - 23 - ], - [ - -51, - 12 - ], - [ - -44, - 44 - ], - [ - -57, - 41 - ], - [ - -38, - -39 - ], - [ - -27, - -61 - ], - [ - -6, - -83 - ] - ], - [ - [ - 13834, - 11218 - ], - [ - -45, - 6 - ], - [ - -47, - 20 - ], - [ - -42, - -63 - ], - [ - -36, - -112 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -8, - 35 - ], - [ - -3, - 55 - ], - [ - -32, - 38 - ], - [ - -25, - 62 - ], - [ - -6, - 43 - ], - [ - -33, - 63 - ], - [ - 5, - 36 - ], - [ - -7, - 50 - ], - [ - 6, - 93 - ], - [ - 17, - 22 - ], - [ - 35, - 122 - ] - ], - [ - [ - 8110, - 16382 - ], - [ - 50, - -16 - ], - [ - 65, - 3 - ], - [ - -35, - -49 - ], - [ - -25, - -8 - ], - [ - -89, - 51 - ], - [ - -17, - 40 - ], - [ - 26, - 37 - ], - [ - 25, - -58 - ] - ], - [ - [ - 8239, - 16688 - ], - [ - -34, - -2 - ], - [ - -90, - 38 - ], - [ - -65, - 56 - ], - [ - 24, - 10 - ], - [ - 92, - -30 - ], - [ - 71, - -50 - ], - [ - 2, - -22 - ] - ], - [ - [ - 3938, - 16617 - ], - [ - -35, - -17 - ], - [ - -115, - 55 - ], - [ - -21, - 42 - ], - [ - -62, - 42 - ], - [ - -13, - 34 - ], - [ - -71, - 22 - ], - [ - -27, - 65 - ], - [ - 6, - 28 - ], - [ - 73, - -26 - ], - [ - 43, - -18 - ], - [ - 65, - -13 - ], - [ - 24, - -41 - ], - [ - 34, - -57 - ], - [ - 70, - -50 - ], - [ - 29, - -66 - ] - ], - [ - [ - 8634, - 16878 - ], - [ - -46, - -105 - ], - [ - 46, - 41 - ], - [ - 47, - -26 - ], - [ - -25, - -42 - ], - [ - 62, - -33 - ], - [ - 32, - 29 - ], - [ - 70, - -36 - ], - [ - -22, - -88 - ], - [ - 49, - 20 - ], - [ - 9, - -63 - ], - [ - 21, - -75 - ], - [ - -29, - -106 - ], - [ - -31, - -4 - ], - [ - -46, - 23 - ], - [ - 15, - 98 - ], - [ - -20, - 15 - ], - [ - -80, - -104 - ], - [ - -42, - 4 - ], - [ - 49, - 56 - ], - [ - -67, - 30 - ], - [ - -75, - -8 - ], - [ - -135, - 4 - ], - [ - -11, - 36 - ], - [ - 44, - 42 - ], - [ - -30, - 32 - ], - [ - 58, - 73 - ], - [ - 72, - 191 - ], - [ - 43, - 68 - ], - [ - 61, - 41 - ], - [ - 32, - -5 - ], - [ - -13, - -32 - ], - [ - -38, - -76 - ] - ], - [ - [ - 3264, - 17296 - ], - [ - 33, - -16 - ], - [ - 66, - 10 - ], - [ - -20, - -136 - ], - [ - 60, - -97 - ], - [ - -28, - 0 - ], - [ - -42, - 55 - ], - [ - -25, - 56 - ], - [ - -36, - 37 - ], - [ - -12, - 53 - ], - [ - 4, - 38 - ] - ], - [ - [ - 7022, - 18254 - ], - [ - -27, - -63 - ], - [ - -31, - 10 - ], - [ - -18, - 36 - ], - [ - 3, - 9 - ], - [ - 27, - 36 - ], - [ - 28, - -3 - ], - [ - 18, - -25 - ] - ], - [ - [ - 6839, - 18321 - ], - [ - -82, - -67 - ], - [ - -49, - 3 - ], - [ - -16, - 33 - ], - [ - 52, - 55 - ], - [ - 96, - -1 - ], - [ - -1, - -23 - ] - ], - [ - [ - 6611, - 18674 - ], - [ - 13, - -53 - ], - [ - 36, - 19 - ], - [ - 40, - -32 - ], - [ - 77, - -41 - ], - [ - 79, - -37 - ], - [ - 7, - -57 - ], - [ - 51, - 9 - ], - [ - 50, - -40 - ], - [ - -62, - -37 - ], - [ - -109, - 28 - ], - [ - -39, - 54 - ], - [ - -69, - -63 - ], - [ - -99, - -62 - ], - [ - -24, - 70 - ], - [ - -95, - -12 - ], - [ - 61, - 59 - ], - [ - 9, - 95 - ], - [ - 24, - 110 - ], - [ - 50, - -10 - ] - ], - [ - [ - 7259, - 18853 - ], - [ - -78, - -6 - ], - [ - -18, - 59 - ], - [ - 30, - 67 - ], - [ - 64, - 17 - ], - [ - 54, - -34 - ], - [ - 1, - -51 - ], - [ - -8, - -17 - ], - [ - -45, - -35 - ] - ], - [ - [ - 5880, - 19088 - ], - [ - -43, - -42 - ], - [ - -94, - 36 - ], - [ - -57, - -13 - ], - [ - -95, - 54 - ], - [ - 61, - 37 - ], - [ - 49, - 52 - ], - [ - 74, - -34 - ], - [ - 42, - -21 - ], - [ - 21, - -23 - ], - [ - 42, - -46 - ] - ], - [ - [ - 3985, - 16676 - ], - [ - -10, - 0 - ], - [ - -135, - 118 - ], - [ - -50, - 52 - ], - [ - -126, - 49 - ], - [ - -39, - 106 - ], - [ - 10, - 74 - ], - [ - -89, - 51 - ], - [ - -12, - 97 - ], - [ - -84, - 87 - ], - [ - -2, - 62 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - 39, - 58 - ], - [ - -2, - 75 - ], - [ - -119, - 77 - ], - [ - -71, - 137 - ], - [ - -43, - 86 - ], - [ - -64, - 54 - ], - [ - -47, - 49 - ], - [ - -37, - 62 - ], - [ - -70, - -39 - ], - [ - -68, - -67 - ], - [ - -62, - 79 - ], - [ - -49, - 52 - ], - [ - -68, - 34 - ], - [ - -68, - 3 - ], - [ - 0, - 683 - ], - [ - 1, - 445 - ] - ], - [ - [ - 2720, - 19160 - ], - [ - 130, - -28 - ], - [ - 109, - -58 - ], - [ - 73, - -11 - ], - [ - 61, - 50 - ], - [ - 85, - 37 - ], - [ - 103, - -14 - ], - [ - 105, - 52 - ], - [ - 114, - 30 - ], - [ - 48, - -49 - ], - [ - 52, - 28 - ], - [ - 15, - 56 - ], - [ - 48, - -13 - ], - [ - 118, - -107 - ], - [ - 93, - 81 - ], - [ - 9, - -91 - ], - [ - 86, - 20 - ], - [ - 26, - 35 - ], - [ - 85, - -7 - ], - [ - 106, - -51 - ], - [ - 164, - -44 - ], - [ - 96, - -20 - ], - [ - 68, - 8 - ], - [ - 94, - -61 - ], - [ - -98, - -60 - ], - [ - 126, - -25 - ], - [ - 188, - 14 - ], - [ - 59, - 21 - ], - [ - 75, - -72 - ], - [ - 75, - 61 - ], - [ - -71, - 50 - ], - [ - 45, - 42 - ], - [ - 85, - 5 - ], - [ - 56, - 12 - ], - [ - 56, - -29 - ], - [ - 70, - -65 - ], - [ - 78, - 10 - ], - [ - 123, - -54 - ], - [ - 109, - 19 - ], - [ - 101, - -3 - ], - [ - -8, - 75 - ], - [ - 62, - 20 - ], - [ - 108, - -40 - ], - [ - 0, - -114 - ], - [ - 44, - 96 - ], - [ - 56, - -3 - ], - [ - 32, - 120 - ], - [ - -75, - 74 - ], - [ - -81, - 49 - ], - [ - 5, - 132 - ], - [ - 83, - 87 - ], - [ - 92, - -19 - ], - [ - 70, - -53 - ], - [ - 95, - -135 - ], - [ - -62, - -59 - ], - [ - 130, - -24 - ], - [ - -1, - -123 - ], - [ - 93, - 94 - ], - [ - 84, - -77 - ], - [ - -21, - -89 - ], - [ - 67, - -81 - ], - [ - 73, - 87 - ], - [ - 51, - 103 - ], - [ - 4, - 132 - ], - [ - 99, - -9 - ], - [ - 103, - -18 - ], - [ - 94, - -60 - ], - [ - 4, - -59 - ], - [ - -52, - -64 - ], - [ - 49, - -64 - ], - [ - -9, - -59 - ], - [ - -136, - -83 - ], - [ - -97, - -19 - ], - [ - -72, - 36 - ], - [ - -21, - -60 - ], - [ - -67, - -101 - ], - [ - -21, - -53 - ], - [ - -80, - -81 - ], - [ - -100, - -8 - ], - [ - -55, - -51 - ], - [ - -5, - -78 - ], - [ - -81, - -15 - ], - [ - -85, - -97 - ], - [ - -76, - -135 - ], - [ - -27, - -94 - ], - [ - -4, - -140 - ], - [ - 103, - -20 - ], - [ - 31, - -112 - ], - [ - 33, - -91 - ], - [ - 97, - 24 - ], - [ - 130, - -52 - ], - [ - 69, - -46 - ], - [ - 50, - -57 - ], - [ - 88, - -33 - ], - [ - 73, - -50 - ], - [ - 116, - -7 - ], - [ - 75, - -12 - ], - [ - -11, - -104 - ], - [ - 22, - -120 - ], - [ - 50, - -134 - ], - [ - 104, - -114 - ], - [ - 54, - 39 - ], - [ - 37, - 123 - ], - [ - -36, - 189 - ], - [ - -49, - 64 - ], - [ - 111, - 56 - ], - [ - 79, - 84 - ], - [ - 39, - 84 - ], - [ - -6, - 80 - ], - [ - -47, - 102 - ], - [ - -85, - 90 - ], - [ - 82, - 126 - ], - [ - -30, - 108 - ], - [ - -23, - 188 - ], - [ - 48, - 27 - ], - [ - 120, - -32 - ], - [ - 72, - -12 - ], - [ - 57, - 32 - ], - [ - 65, - -41 - ], - [ - 86, - -70 - ], - [ - 21, - -46 - ], - [ - 124, - -9 - ], - [ - -2, - -101 - ], - [ - 24, - -152 - ], - [ - 63, - -19 - ], - [ - 51, - -70 - ], - [ - 101, - 66 - ], - [ - 66, - 133 - ], - [ - 46, - 56 - ], - [ - 55, - -108 - ], - [ - 91, - -153 - ], - [ - 77, - -143 - ], - [ - -28, - -76 - ], - [ - 92, - -67 - ], - [ - 63, - -69 - ], - [ - 111, - -31 - ], - [ - 45, - -38 - ], - [ - 28, - -102 - ], - [ - 54, - -16 - ], - [ - 28, - -45 - ], - [ - 5, - -135 - ], - [ - -51, - -45 - ], - [ - -50, - -42 - ], - [ - -115, - -43 - ], - [ - -87, - -98 - ], - [ - -118, - -20 - ], - [ - -149, - 26 - ], - [ - -105, - 0 - ], - [ - -72, - -8 - ], - [ - -58, - -86 - ], - [ - -89, - -53 - ], - [ - -101, - -159 - ], - [ - -80, - -111 - ], - [ - 59, - 20 - ], - [ - 112, - 158 - ], - [ - 146, - 100 - ], - [ - 105, - 12 - ], - [ - 61, - -59 - ], - [ - -66, - -81 - ], - [ - 23, - -129 - ], - [ - 22, - -91 - ], - [ - 91, - -60 - ], - [ - 115, - 18 - ], - [ - 70, - 135 - ], - [ - 5, - -87 - ], - [ - 45, - -44 - ], - [ - -86, - -78 - ], - [ - -155, - -72 - ], - [ - -69, - -48 - ], - [ - -78, - -87 - ], - [ - -53, - 9 - ], - [ - -3, - 102 - ], - [ - 122, - 99 - ], - [ - -112, - -4 - ], - [ - -78, - -15 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - -45, - 68 - ], - [ - 0, - 164 - ], - [ - -31, - 34 - ], - [ - -47, - -20 - ], - [ - -23, - 31 - ], - [ - -53, - -90 - ], - [ - -21, - -93 - ], - [ - -25, - -55 - ], - [ - -30, - -19 - ], - [ - -22, - -6 - ], - [ - -7, - -29 - ], - [ - -128, - 0 - ], - [ - -106, - -1 - ], - [ - -32, - -22 - ], - [ - -73, - -87 - ], - [ - -9, - -9 - ], - [ - -22, - -47 - ], - [ - -64, - 0 - ], - [ - -69, - 0 - ], - [ - -31, - -19 - ], - [ - 11, - -24 - ], - [ - 6, - -36 - ], - [ - -1, - -13 - ], - [ - -91, - -59 - ], - [ - -72, - -19 - ], - [ - -81, - -64 - ], - [ - -18, - 0 - ], - [ - -23, - 19 - ], - [ - -8, - 17 - ], - [ - 1, - 12 - ], - [ - 16, - 42 - ], - [ - 32, - 66 - ], - [ - 21, - 71 - ], - [ - -14, - 105 - ], - [ - -15, - 108 - ], - [ - -73, - 57 - ], - [ - 9, - 21 - ], - [ - -10, - 15 - ], - [ - -19, - 0 - ], - [ - -14, - 19 - ], - [ - -4, - 28 - ], - [ - -13, - -12 - ], - [ - -19, - 3 - ], - [ - 4, - 12 - ], - [ - -16, - 12 - ], - [ - -7, - 32 - ], - [ - -54, - 38 - ], - [ - -57, - 40 - ], - [ - -68, - 46 - ], - [ - -65, - 44 - ], - [ - -63, - -34 - ], - [ - -22, - -1 - ], - [ - -86, - 31 - ], - [ - -57, - -16 - ], - [ - -67, - 38 - ], - [ - -71, - 19 - ], - [ - -49, - 7 - ], - [ - -22, - 20 - ], - [ - -12, - 66 - ], - [ - -24, - 0 - ], - [ - 0, - -46 - ], - [ - -144, - 0 - ], - [ - -239, - 0 - ], - [ - -237, - 0 - ], - [ - -209, - 0 - ], - [ - -209, - 0 - ], - [ - -206, - 0 - ], - [ - -212, - 0 - ], - [ - -69, - 0 - ], - [ - -207, - 0 - ], - [ - -197, - 0 - ] - ], - [ - [ - 4589, - 19569 - ], - [ - -35, - -56 - ], - [ - 155, - 37 - ], - [ - 97, - -61 - ], - [ - 79, - 61 - ], - [ - 64, - -39 - ], - [ - 57, - -118 - ], - [ - 35, - 50 - ], - [ - -50, - 123 - ], - [ - 62, - 17 - ], - [ - 69, - -19 - ], - [ - 78, - -48 - ], - [ - 44, - -117 - ], - [ - 21, - -85 - ], - [ - 118, - -59 - ], - [ - 125, - -57 - ], - [ - -7, - -53 - ], - [ - -115, - -9 - ], - [ - 45, - -47 - ], - [ - -24, - -44 - ], - [ - -126, - 19 - ], - [ - -120, - 33 - ], - [ - -81, - -8 - ], - [ - -131, - -40 - ], - [ - -176, - -18 - ], - [ - -124, - -12 - ], - [ - -38, - 57 - ], - [ - -95, - 33 - ], - [ - -62, - -14 - ], - [ - -86, - 95 - ], - [ - 46, - 13 - ], - [ - 108, - 20 - ], - [ - 98, - -5 - ], - [ - 91, - 21 - ], - [ - -135, - 28 - ], - [ - -149, - -10 - ], - [ - -98, - 3 - ], - [ - -37, - 44 - ], - [ - 161, - 48 - ], - [ - -107, - -2 - ], - [ - -122, - 32 - ], - [ - 59, - 90 - ], - [ - 48, - 48 - ], - [ - 187, - 73 - ], - [ - 71, - -24 - ] - ], - [ - [ - 5263, - 19605 - ], - [ - -61, - -79 - ], - [ - -109, - 84 - ], - [ - 24, - 17 - ], - [ - 93, - 5 - ], - [ - 53, - -27 - ] - ], - [ - [ - 7226, - 19567 - ], - [ - 6, - -33 - ], - [ - -74, - 4 - ], - [ - -75, - 2 - ], - [ - -76, - -16 - ], - [ - -21, - 7 - ], - [ - -76, - 64 - ], - [ - 3, - 43 - ], - [ - 33, - 8 - ], - [ - 160, - -13 - ], - [ - 120, - -66 - ] - ], - [ - [ - 6513, - 19574 - ], - [ - 55, - -75 - ], - [ - 65, - 97 - ], - [ - 176, - 49 - ], - [ - 120, - -124 - ], - [ - -10, - -79 - ], - [ - 138, - 35 - ], - [ - 65, - 48 - ], - [ - 155, - -61 - ], - [ - 96, - -57 - ], - [ - 9, - -52 - ], - [ - 130, - 27 - ], - [ - 72, - -77 - ], - [ - 169, - -47 - ], - [ - 60, - -48 - ], - [ - 66, - -113 - ], - [ - -128, - -56 - ], - [ - 164, - -78 - ], - [ - 111, - -26 - ], - [ - 100, - -110 - ], - [ - 110, - -8 - ], - [ - -22, - -85 - ], - [ - -122, - -139 - ], - [ - -86, - 51 - ], - [ - -110, - 116 - ], - [ - -90, - -15 - ], - [ - -9, - -69 - ], - [ - 74, - -70 - ], - [ - 94, - -55 - ], - [ - 29, - -32 - ], - [ - 46, - -119 - ], - [ - -25, - -86 - ], - [ - -87, - 33 - ], - [ - -175, - 96 - ], - [ - 98, - -104 - ], - [ - 73, - -72 - ], - [ - 11, - -42 - ], - [ - -189, - 48 - ], - [ - -149, - 70 - ], - [ - -85, - 58 - ], - [ - 24, - 34 - ], - [ - -104, - 61 - ], - [ - -101, - 59 - ], - [ - 1, - -35 - ], - [ - -202, - -19 - ], - [ - -59, - 41 - ], - [ - 46, - 88 - ], - [ - 131, - 2 - ], - [ - 144, - 16 - ], - [ - -23, - 43 - ], - [ - 24, - 59 - ], - [ - 90, - 117 - ], - [ - -19, - 53 - ], - [ - -27, - 41 - ], - [ - -107, - 59 - ], - [ - -141, - 40 - ], - [ - 45, - 31 - ], - [ - -74, - 74 - ], - [ - -62, - 7 - ], - [ - -54, - 41 - ], - [ - -38, - -35 - ], - [ - -126, - -16 - ], - [ - -254, - 27 - ], - [ - -147, - 35 - ], - [ - -113, - 18 - ], - [ - -58, - 42 - ], - [ - 73, - 55 - ], - [ - -99, - 1 - ], - [ - -23, - 121 - ], - [ - 54, - 107 - ], - [ - 72, - 49 - ], - [ - 180, - 32 - ], - [ - -52, - -77 - ] - ], - [ - [ - 5552, - 19656 - ], - [ - 83, - -25 - ], - [ - 124, - 15 - ], - [ - 18, - -35 - ], - [ - -65, - -57 - ], - [ - 106, - -52 - ], - [ - -13, - -108 - ], - [ - -114, - -46 - ], - [ - -67, - 10 - ], - [ - -48, - 46 - ], - [ - -174, - 92 - ], - [ - 2, - 39 - ], - [ - 142, - -15 - ], - [ - -77, - 78 - ], - [ - 83, - 58 - ] - ], - [ - [ - 6051, - 19528 - ], - [ - -75, - -90 - ], - [ - -79, - 4 - ], - [ - -44, - 106 - ], - [ - 1, - 59 - ], - [ - 37, - 51 - ], - [ - 69, - 33 - ], - [ - 145, - -4 - ], - [ - 133, - -29 - ], - [ - -104, - -107 - ], - [ - -83, - -23 - ] - ], - [ - [ - 4150, - 19361 - ], - [ - -183, - -58 - ], - [ - -37, - 53 - ], - [ - -161, - 63 - ], - [ - 30, - 51 - ], - [ - 48, - 88 - ], - [ - 61, - 78 - ], - [ - -68, - 74 - ], - [ - 235, - 19 - ], - [ - 100, - -25 - ], - [ - 178, - -7 - ], - [ - 68, - -35 - ], - [ - 74, - -50 - ], - [ - -87, - -30 - ], - [ - -171, - -85 - ], - [ - -87, - -84 - ], - [ - 0, - -52 - ] - ], - [ - [ - 6022, - 19792 - ], - [ - -38, - -46 - ], - [ - -101, - 9 - ], - [ - -85, - 31 - ], - [ - 37, - 54 - ], - [ - 101, - 32 - ], - [ - 60, - -42 - ], - [ - 26, - -38 - ] - ], - [ - [ - 5681, - 20001 - ], - [ - 54, - -55 - ], - [ - 2, - -62 - ], - [ - -32, - -89 - ], - [ - -115, - -12 - ], - [ - -75, - 19 - ], - [ - 2, - 70 - ], - [ - -115, - -10 - ], - [ - -4, - 93 - ], - [ - 75, - -4 - ], - [ - 105, - 41 - ], - [ - 98, - -7 - ], - [ - 5, - 16 - ] - ], - [ - [ - 5004, - 19939 - ], - [ - 28, - -43 - ], - [ - 62, - 20 - ], - [ - 73, - -5 - ], - [ - 12, - -59 - ], - [ - -42, - -57 - ], - [ - -237, - -18 - ], - [ - -175, - -52 - ], - [ - -106, - -3 - ], - [ - -9, - 39 - ], - [ - 145, - 53 - ], - [ - -315, - -14 - ], - [ - -98, - 22 - ], - [ - 95, - 117 - ], - [ - 66, - 33 - ], - [ - 196, - -40 - ], - [ - 124, - -71 - ], - [ - 122, - -9 - ], - [ - -100, - 114 - ], - [ - 64, - 44 - ], - [ - 72, - -14 - ], - [ - 23, - -57 - ] - ], - [ - [ - 5947, - 20047 - ], - [ - 78, - -39 - ], - [ - 137, - 0 - ], - [ - 60, - -39 - ], - [ - -16, - -45 - ], - [ - 80, - -27 - ], - [ - 44, - -29 - ], - [ - 94, - -5 - ], - [ - 102, - -10 - ], - [ - 111, - 26 - ], - [ - 142, - 10 - ], - [ - 113, - -8 - ], - [ - 75, - -46 - ], - [ - 15, - -49 - ], - [ - -43, - -32 - ], - [ - -104, - -26 - ], - [ - -89, - 15 - ], - [ - -200, - -19 - ], - [ - -143, - -2 - ], - [ - -113, - 15 - ], - [ - -185, - 38 - ], - [ - -24, - 66 - ], - [ - -9, - 60 - ], - [ - -70, - 52 - ], - [ - -144, - 15 - ], - [ - -81, - 37 - ], - [ - 27, - 49 - ], - [ - 143, - -7 - ] - ], - [ - [ - 4447, - 20112 - ], - [ - -9, - -92 - ], - [ - -54, - -42 - ], - [ - -65, - -5 - ], - [ - -129, - -52 - ], - [ - -112, - -18 - ], - [ - -95, - 26 - ], - [ - 119, - 90 - ], - [ - 143, - 77 - ], - [ - 107, - -1 - ], - [ - 95, - 17 - ] - ], - [ - [ - 6006, - 20097 - ], - [ - -32, - -3 - ], - [ - -130, - 7 - ], - [ - -19, - 34 - ], - [ - 140, - -2 - ], - [ - 49, - -22 - ], - [ - -8, - -14 - ] - ], - [ - [ - 4867, - 20118 - ], - [ - -130, - -34 - ], - [ - -104, - 39 - ], - [ - 57, - 38 - ], - [ - 101, - 12 - ], - [ - 99, - -19 - ], - [ - -23, - -36 - ] - ], - [ - [ - 4903, - 20227 - ], - [ - -85, - -23 - ], - [ - -116, - 0 - ], - [ - 2, - 17 - ], - [ - 71, - 36 - ], - [ - 37, - -6 - ], - [ - 91, - -24 - ] - ], - [ - [ - 5867, - 20162 - ], - [ - -103, - -25 - ], - [ - -57, - 28 - ], - [ - -29, - 45 - ], - [ - -6, - 49 - ], - [ - 90, - -4 - ], - [ - 41, - -8 - ], - [ - 83, - -42 - ], - [ - -19, - -43 - ] - ], - [ - [ - 5572, - 20194 - ], - [ - 28, - -50 - ], - [ - -114, - 13 - ], - [ - -115, - 39 - ], - [ - -155, - 4 - ], - [ - 67, - 36 - ], - [ - -84, - 29 - ], - [ - -5, - 46 - ], - [ - 137, - -16 - ], - [ - 188, - -44 - ], - [ - 53, - -57 - ] - ], - [ - [ - 6481, - 20354 - ], - [ - 85, - -39 - ], - [ - -96, - -36 - ], - [ - -129, - -90 - ], - [ - -123, - -8 - ], - [ - -145, - 15 - ], - [ - -75, - 49 - ], - [ - 1, - 43 - ], - [ - 56, - 32 - ], - [ - -128, - -1 - ], - [ - -77, - 40 - ], - [ - -44, - 55 - ], - [ - 48, - 53 - ], - [ - 49, - 37 - ], - [ - 71, - 8 - ], - [ - -30, - 27 - ], - [ - 162, - 7 - ], - [ - 89, - -65 - ], - [ - 117, - -25 - ], - [ - 114, - -23 - ], - [ - 55, - -79 - ] - ], - [ - [ - 7772, - 20767 - ], - [ - 187, - -9 - ], - [ - 149, - -15 - ], - [ - 128, - -33 - ], - [ - -3, - -32 - ], - [ - -170, - -52 - ], - [ - -169, - -24 - ], - [ - -63, - -27 - ], - [ - 152, - 0 - ], - [ - -165, - -72 - ], - [ - -113, - -34 - ], - [ - -119, - -98 - ], - [ - -144, - -20 - ], - [ - -45, - -25 - ], - [ - -211, - -13 - ], - [ - 96, - -15 - ], - [ - -48, - -21 - ], - [ - 58, - -59 - ], - [ - -66, - -41 - ], - [ - -108, - -34 - ], - [ - -33, - -47 - ], - [ - -97, - -36 - ], - [ - 9, - -27 - ], - [ - 119, - 4 - ], - [ - 2, - -29 - ], - [ - -186, - -72 - ], - [ - -182, - 33 - ], - [ - -205, - -18 - ], - [ - -104, - 14 - ], - [ - -132, - 6 - ], - [ - -8, - 58 - ], - [ - 128, - 27 - ], - [ - -34, - 87 - ], - [ - 43, - 8 - ], - [ - 186, - -52 - ], - [ - -95, - 77 - ], - [ - -113, - 23 - ], - [ - 56, - 47 - ], - [ - 124, - 28 - ], - [ - 20, - 42 - ], - [ - -99, - 47 - ], - [ - -29, - 62 - ], - [ - 190, - -5 - ], - [ - 55, - -13 - ], - [ - 109, - 43 - ], - [ - -157, - 14 - ], - [ - -244, - -7 - ], - [ - -123, - 40 - ], - [ - -58, - 49 - ], - [ - -82, - 35 - ], - [ - -15, - 41 - ], - [ - 104, - 23 - ], - [ - 81, - 4 - ], - [ - 137, - 19 - ], - [ - 102, - 45 - ], - [ - 87, - -6 - ], - [ - 75, - -34 - ], - [ - 53, - 65 - ], - [ - 92, - 19 - ], - [ - 125, - 13 - ], - [ - 213, - 5 - ], - [ - 37, - -13 - ], - [ - 202, - 21 - ], - [ - 151, - -8 - ], - [ - 150, - -8 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -5, - -49 - ], - [ - -31, - -20 - ], - [ - -51, - 15 - ], - [ - -15, - -49 - ], - [ - -34, - -4 - ], - [ - -12, - 19 - ], - [ - -39, - -40 - ], - [ - -33, - -6 - ], - [ - -30, - 26 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -24, - 52 - ], - [ - -34, - -18 - ], - [ - 1, - 54 - ], - [ - 51, - 67 - ], - [ - -2, - 31 - ], - [ - 32, - -11 - ], - [ - 19, - 20 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 59, - -1 - ], - [ - 15, - 26 - ], - [ - 74, - -36 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -23, - -48 - ], - [ - -60, - -37 - ], - [ - -34, - 3 - ], - [ - -42, - 10 - ], - [ - -50, - 36 - ], - [ - -73, - 17 - ], - [ - -88, - 67 - ], - [ - -71, - 65 - ], - [ - -96, - 134 - ], - [ - 57, - -25 - ], - [ - 98, - -80 - ], - [ - 93, - -43 - ], - [ - 36, - 55 - ], - [ - 22, - 82 - ], - [ - 65, - 50 - ], - [ - 49, - -15 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -62, - 1 - ], - [ - -33, - -30 - ], - [ - -63, - -43 - ], - [ - -11, - -112 - ], - [ - -30, - -3 - ], - [ - -78, - 39 - ], - [ - -80, - 84 - ], - [ - -87, - 68 - ], - [ - -22, - 76 - ], - [ - 20, - 71 - ], - [ - -35, - 79 - ], - [ - -9, - 205 - ], - [ - 30, - 115 - ], - [ - 73, - 93 - ], - [ - -106, - 35 - ], - [ - 67, - 106 - ], - [ - 24, - 199 - ], - [ - 77, - -42 - ], - [ - 36, - 249 - ], - [ - -46, - 31 - ], - [ - -22, - -149 - ], - [ - -44, - 17 - ], - [ - 22, - 171 - ], - [ - 24, - 222 - ], - [ - 32, - 82 - ], - [ - -20, - 117 - ], - [ - -6, - 136 - ], - [ - 29, - 3 - ], - [ - 43, - 194 - ], - [ - 48, - 192 - ], - [ - 30, - 179 - ], - [ - -16, - 180 - ], - [ - 20, - 99 - ], - [ - -8, - 148 - ], - [ - 41, - 146 - ], - [ - 12, - 232 - ], - [ - 23, - 249 - ], - [ - 22, - 269 - ], - [ - -6, - 196 - ], - [ - -14, - 169 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - 36, - 31 - ], - [ - 18, - 61 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - -10, - 69 - ], - [ - 2, - 46 - ], - [ - -42, - 28 - ], - [ - -23, - -12 - ], - [ - -18, - 111 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - 20, - 27 - ], - [ - -9, - 28 - ], - [ - 66, - 57 - ], - [ - 48, - 23 - ], - [ - 74, - -16 - ], - [ - 26, - 77 - ], - [ - 90, - 14 - ], - [ - 25, - 48 - ], - [ - 109, - 65 - ], - [ - 10, - 27 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -5, - 68 - ], - [ - 48, - 31 - ], - [ - -63, - 209 - ], - [ - 138, - 48 - ], - [ - 36, - 27 - ], - [ - 50, - 214 - ], - [ - 138, - -39 - ], - [ - 39, - 54 - ], - [ - 3, - 120 - ], - [ - 58, - 12 - ], - [ - 53, - 79 - ] - ], - [ - [ - 18637, - 16701 - ], - [ - 27, - 10 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 19, - -83 - ], - [ - 58, - -64 - ], - [ - 100, - -45 - ], - [ - 48, - -97 - ], - [ - -27, - -140 - ], - [ - 25, - -52 - ], - [ - 83, - -20 - ], - [ - 94, - -17 - ], - [ - 84, - -75 - ], - [ - 43, - -13 - ], - [ - 32, - -111 - ], - [ - 41, - -71 - ], - [ - 77, - 3 - ], - [ - 144, - -27 - ], - [ - 92, - 17 - ], - [ - 69, - -18 - ], - [ - 103, - -73 - ], - [ - 85, - 0 - ], - [ - 30, - -37 - ], - [ - 82, - 64 - ], - [ - 112, - 42 - ], - [ - 105, - 4 - ], - [ - 81, - 42 - ], - [ - 50, - 65 - ], - [ - 49, - 40 - ], - [ - -11, - 40 - ], - [ - -23, - 46 - ], - [ - 37, - 77 - ], - [ - 39, - -11 - ], - [ - 72, - -24 - ], - [ - 69, - 64 - ], - [ - 107, - 46 - ], - [ - 51, - 79 - ], - [ - 49, - 34 - ], - [ - 101, - 16 - ], - [ - 55, - -13 - ], - [ - 8, - 42 - ], - [ - -64, - 84 - ], - [ - -55, - 39 - ], - [ - -54, - -45 - ], - [ - -69, - 19 - ], - [ - -39, - -15 - ], - [ - -18, - 49 - ], - [ - 49, - 120 - ], - [ - 34, - 90 - ] - ], - [ - [ - 20681, - 16782 - ], - [ - 84, - -45 - ], - [ - 98, - 76 - ], - [ - -1, - 53 - ], - [ - 63, - 127 - ], - [ - 39, - 38 - ], - [ - -1, - 67 - ], - [ - -38, - 28 - ], - [ - 57, - 60 - ], - [ - 87, - 21 - ], - [ - 92, - 4 - ], - [ - 105, - -36 - ], - [ - 61, - -44 - ], - [ - 43, - -121 - ], - [ - 26, - -52 - ], - [ - 24, - -74 - ], - [ - 26, - -117 - ], - [ - 122, - -38 - ], - [ - 82, - -86 - ], - [ - 28, - -112 - ], - [ - 106, - -1 - ], - [ - 61, - 48 - ], - [ - 115, - 35 - ], - [ - -37, - -108 - ], - [ - -27, - -44 - ], - [ - -24, - -131 - ], - [ - -47, - -117 - ], - [ - -84, - 21 - ], - [ - -60, - -42 - ], - [ - 18, - -103 - ], - [ - -10, - -142 - ], - [ - -35, - -3 - ], - [ - 0, - -61 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - -45, - 71 - ], - [ - -28, - -67 - ], - [ - -107, - -52 - ], - [ - 11, - -63 - ], - [ - -61, - 4 - ], - [ - -33, - 38 - ], - [ - -48, - -85 - ], - [ - -76, - -65 - ], - [ - -57, - -77 - ] - ], - [ - [ - 21210, - 15587 - ], - [ - -98, - -35 - ], - [ - -51, - -56 - ], - [ - -75, - -32 - ], - [ - 37, - 55 - ], - [ - -15, - 47 - ], - [ - 56, - 81 - ], - [ - -37, - 62 - ], - [ - -61, - -42 - ], - [ - -79, - -83 - ], - [ - -43, - -78 - ], - [ - -68, - -6 - ], - [ - -35, - -55 - ], - [ - 36, - -82 - ], - [ - 57, - -19 - ], - [ - 3, - -54 - ], - [ - 55, - -35 - ], - [ - 78, - 85 - ], - [ - 62, - -46 - ], - [ - 45, - -3 - ], - [ - 11, - -63 - ], - [ - -99, - -34 - ], - [ - -32, - -65 - ], - [ - -68, - -60 - ], - [ - -36, - -84 - ], - [ - 75, - -66 - ], - [ - 28, - -118 - ], - [ - 42, - -110 - ], - [ - 48, - -92 - ], - [ - -2, - -89 - ], - [ - -43, - -33 - ], - [ - 16, - -64 - ], - [ - 41, - -37 - ], - [ - -10, - -98 - ], - [ - -18, - -95 - ], - [ - -39, - -10 - ], - [ - -51, - -130 - ], - [ - -56, - -158 - ], - [ - -65, - -143 - ], - [ - -96, - -111 - ], - [ - -97, - -101 - ], - [ - -79, - -13 - ], - [ - -42, - -54 - ], - [ - -24, - 39 - ], - [ - -40, - -59 - ], - [ - -97, - -60 - ], - [ - -74, - -19 - ], - [ - -24, - -127 - ], - [ - -38, - -7 - ], - [ - -19, - 88 - ], - [ - 17, - 46 - ], - [ - -94, - 38 - ], - [ - -33, - -19 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -70, - 31 - ], - [ - -33, - 49 - ], - [ - 11, - 69 - ], - [ - -64, - 22 - ], - [ - -33, - 45 - ], - [ - -60, - -64 - ], - [ - -67, - -14 - ], - [ - -56, - 1 - ], - [ - -37, - -30 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - -37, - -17 - ], - [ - 11, - -138 - ], - [ - -37, - 4 - ], - [ - -6, - 28 - ] - ], - [ - [ - 19601, - 13369 - ], - [ - -2, - 50 - ], - [ - -52, - -35 - ], - [ - -30, - 22 - ], - [ - -52, - 45 - ], - [ - 21, - 99 - ], - [ - -44, - 24 - ], - [ - -17, - 110 - ], - [ - -74, - -20 - ], - [ - 9, - 142 - ], - [ - 66, - 101 - ], - [ - 3, - 99 - ], - [ - -2, - 91 - ], - [ - -31, - 29 - ], - [ - -23, - 71 - ], - [ - -41, - -9 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - -75, - 18 - ], - [ - 23, - 50 - ], - [ - -32, - 75 - ], - [ - -50, - -51 - ], - [ - -58, - 30 - ], - [ - -81, - -77 - ], - [ - -63, - -89 - ], - [ - -56, - -15 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - -6, - 95 - ], - [ - -43, - -25 - ] - ], - [ - [ - 18690, - 14142 - ], - [ - -81, - 11 - ], - [ - -79, - 28 - ], - [ - -56, - 52 - ], - [ - -55, - 24 - ], - [ - -23, - 58 - ], - [ - -39, - 17 - ], - [ - -71, - 78 - ], - [ - -55, - 37 - ], - [ - -29, - -29 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -97, - 84 - ], - [ - -69, - 76 - ], - [ - -19, - 132 - ], - [ - 50, - -16 - ], - [ - 2, - 61 - ], - [ - -28, - 62 - ], - [ - 7, - 98 - ], - [ - -75, - 140 - ] - ], - [ - [ - 17973, - 15055 - ], - [ - -114, - 49 - ], - [ - -21, - 92 - ], - [ - -51, - 56 - ] - ], - [ - [ - 20239, - 13038 - ], - [ - -60, - -58 - ], - [ - -57, - 38 - ], - [ - -2, - 103 - ], - [ - 34, - 54 - ], - [ - 76, - 34 - ], - [ - 40, - -3 - ], - [ - 16, - -46 - ], - [ - -31, - -53 - ], - [ - -16, - -69 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - 19, - -171 - ], - [ - -29, - -100 - ], - [ - -19, - -136 - ], - [ - 31, - -103 - ], - [ - -4, - -48 - ] - ], - [ - [ - 12348, - 11396 - ], - [ - -31, - -1 - ], - [ - -49, - 24 - ], - [ - -45, - -2 - ], - [ - -82, - -21 - ], - [ - -49, - -34 - ], - [ - -69, - -44 - ], - [ - -13, - 3 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - 5, - 99 - ], - [ - 7, - 15 - ], - [ - -2, - 47 - ], - [ - -30, - 50 - ], - [ - -22, - 8 - ], - [ - -20, - 33 - ], - [ - 15, - 53 - ], - [ - -7, - 58 - ], - [ - 3, - 35 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - 11, - 0 - ], - [ - 4, - 53 - ], - [ - -5, - 23 - ], - [ - 7, - 17 - ], - [ - 26, - 14 - ], - [ - -18, - 96 - ], - [ - -16, - 50 - ], - [ - 6, - 40 - ], - [ - 14, - 10 - ] - ], - [ - [ - 11988, - 12022 - ], - [ - 9, - 11 - ], - [ - 19, - -18 - ], - [ - 54, - -1 - ], - [ - 13, - 35 - ], - [ - 12, - -3 - ], - [ - 20, - 14 - ], - [ - 11, - -52 - ], - [ - 16, - 16 - ], - [ - 29, - 17 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -5, - -65 - ], - [ - -56, - 29 - ], - [ - -56, - 31 - ], - [ - -88, - 5 - ] - ], - [ - [ - 13459, - 11069 - ], - [ - -9, - 7 - ], - [ - -41, - -16 - ], - [ - -42, - 16 - ], - [ - -33, - -8 - ] - ], - [ - [ - 13334, - 11068 - ], - [ - -114, - 3 - ] - ], - [ - [ - 13220, - 11071 - ], - [ - 10, - 95 - ], - [ - -27, - 79 - ], - [ - -32, - 21 - ], - [ - -14, - 53 - ], - [ - -18, - 18 - ], - [ - 1, - 33 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - 18, - 85 - ], - [ - 33, - 115 - ], - [ - 20, - 1 - ], - [ - 42, - 71 - ], - [ - 26, - 2 - ], - [ - 39, - -50 - ], - [ - 48, - 41 - ], - [ - 7, - 50 - ], - [ - 15, - 48 - ], - [ - 11, - 61 - ], - [ - 38, - 49 - ], - [ - 14, - 84 - ], - [ - 14, - 27 - ], - [ - 10, - 62 - ], - [ - 19, - 77 - ], - [ - 58, - 93 - ], - [ - 4, - 39 - ], - [ - 8, - 22 - ], - [ - -28, - 48 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - 2, - 38 - ], - [ - 20, - 7 - ] - ], - [ - [ - 13558, - 12340 - ], - [ - 28, - -77 - ], - [ - 4, - -79 - ], - [ - -2, - -80 - ], - [ - 38, - -109 - ], - [ - -39, - 1 - ], - [ - -20, - -9 - ], - [ - -32, - 12 - ], - [ - -15, - -56 - ], - [ - 41, - -70 - ], - [ - 31, - -21 - ], - [ - 10, - -49 - ], - [ - 22, - -83 - ], - [ - -11, - -32 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - -9, - 38 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - 19, - -13 - ], - [ - 24, - 46 - ], - [ - 38, - -1 - ], - [ - 4, - -34 - ], - [ - 26, - -21 - ], - [ - 41, - 75 - ], - [ - 41, - 59 - ], - [ - 17, - 38 - ], - [ - -2, - 99 - ], - [ - 30, - 116 - ], - [ - 32, - 62 - ], - [ - 46, - 58 - ], - [ - 8, - 38 - ], - [ - 2, - 44 - ], - [ - 11, - 42 - ], - [ - -3, - 68 - ], - [ - 8, - 106 - ], - [ - 14, - 75 - ], - [ - 21, - 64 - ], - [ - 4, - 73 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - 42, - -99 - ], - [ - 31, - -14 - ], - [ - 19, - 20 - ], - [ - 32, - -8 - ], - [ - 39, - 25 - ], - [ - 17, - -51 - ], - [ - 61, - -80 - ] - ], - [ - [ - 14697, - 11218 - ], - [ - -4, - -140 - ], - [ - 28, - -16 - ], - [ - -23, - -43 - ], - [ - -27, - -32 - ], - [ - -26, - -62 - ], - [ - -15, - -56 - ], - [ - -4, - -96 - ], - [ - -16, - -46 - ], - [ - -1, - -91 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - -20, - -33 - ], - [ - -2, - -72 - ], - [ - -10, - -9 - ], - [ - -6, - -65 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - 12, - -110 - ], - [ - -7, - -62 - ], - [ - 14, - -70 - ], - [ - 41, - -67 - ], - [ - 37, - -151 - ] - ], - [ - [ - 14690, - 9797 - ], - [ - -27, - 12 - ], - [ - -94, - -20 - ], - [ - -18, - -15 - ], - [ - -20, - -76 - ], - [ - 15, - -53 - ], - [ - -12, - -142 - ], - [ - -9, - -121 - ], - [ - 19, - -21 - ], - [ - 49, - -47 - ], - [ - 19, - 22 - ], - [ - 6, - -129 - ], - [ - -54, - 1 - ], - [ - -28, - 66 - ], - [ - -26, - 51 - ], - [ - -53, - 17 - ], - [ - -16, - 63 - ], - [ - -43, - -38 - ], - [ - -55, - 16 - ], - [ - -24, - 55 - ], - [ - -44, - 11 - ], - [ - -33, - -3 - ], - [ - -4, - 37 - ], - [ - -24, - 3 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - -57, - 127 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - 53, - 66 - ], - [ - -26, - 79 - ], - [ - 24, - 31 - ], - [ - 47, - 14 - ], - [ - 5, - 53 - ], - [ - 37, - -57 - ], - [ - 62, - -5 - ], - [ - 21, - 56 - ], - [ - 9, - 80 - ], - [ - -8, - 94 - ], - [ - -33, - 71 - ], - [ - 31, - 139 - ], - [ - -18, - 24 - ], - [ - -52, - -10 - ], - [ - -19, - 62 - ], - [ - 5, - 52 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - -35, - 63 - ], - [ - -20, - 3 - ], - [ - 45, - 122 - ], - [ - -54, - 56 - ], - [ - -42, - -10 - ], - [ - -25, - 21 - ], - [ - -38, - -32 - ], - [ - -52, - 15 - ], - [ - -41, - 126 - ], - [ - -32, - 31 - ], - [ - -23, - 57 - ], - [ - -46, - 56 - ], - [ - -19, - -11 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - -29, - 28 - ], - [ - -35, - 40 - ], - [ - -20, - -19 - ], - [ - -59, - 17 - ], - [ - -17, - 51 - ], - [ - -13, - -2 - ], - [ - -69, - 69 - ] - ], - [ - [ - 7051, - 10963 - ], - [ - -10, - 37 - ], - [ - 26, - 9 - ], - [ - -3, - 60 - ], - [ - 16, - 44 - ], - [ - 35, - 8 - ], - [ - 29, - 75 - ], - [ - 27, - 63 - ], - [ - -26, - 29 - ], - [ - 14, - 69 - ], - [ - -16, - 110 - ], - [ - 15, - 31 - ], - [ - -11, - 102 - ], - [ - -28, - 64 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - 8, - 58 - ], - [ - 23, - -8 - ], - [ - 13, - 35 - ], - [ - -16, - 71 - ], - [ - 8, - 17 - ] - ], - [ - [ - 7155, - 11837 - ], - [ - 36, - -3 - ], - [ - 53, - 83 - ], - [ - 28, - 13 - ], - [ - 1, - 40 - ], - [ - 13, - 101 - ], - [ - 40, - 56 - ], - [ - 44, - 2 - ], - [ - 5, - 25 - ], - [ - 55, - -10 - ], - [ - 55, - 61 - ], - [ - 27, - 26 - ], - [ - 34, - 58 - ], - [ - 24, - -7 - ], - [ - 19, - -32 - ], - [ - -14, - -40 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -45, - -20 - ], - [ - -17, - -60 - ], - [ - -27, - -35 - ], - [ - -21, - -44 - ], - [ - -8, - -86 - ], - [ - -19, - -70 - ], - [ - 36, - -8 - ], - [ - 8, - -55 - ], - [ - 16, - -26 - ], - [ - 5, - -49 - ], - [ - -8, - -44 - ], - [ - 3, - -25 - ], - [ - 17, - -10 - ], - [ - 16, - -42 - ], - [ - 90, - 12 - ], - [ - 40, - -16 - ], - [ - 49, - -103 - ], - [ - 29, - 13 - ], - [ - 50, - -7 - ], - [ - 40, - 14 - ], - [ - 24, - -21 - ], - [ - -12, - -64 - ], - [ - -16, - -40 - ], - [ - -5, - -86 - ], - [ - 14, - -80 - ], - [ - 20, - -36 - ], - [ - 2, - -27 - ], - [ - -35, - -59 - ], - [ - 25, - -27 - ], - [ - 18, - -42 - ], - [ - 22, - -119 - ] - ], - [ - [ - 6764, - 11784 - ], - [ - -38, - 27 - ], - [ - -14, - 25 - ], - [ - 8, - 21 - ], - [ - -2, - 26 - ], - [ - -20, - 29 - ], - [ - -27, - 23 - ], - [ - -24, - 16 - ], - [ - -5, - 35 - ], - [ - -18, - 21 - ], - [ - 4, - -35 - ], - [ - -13, - -28 - ], - [ - -16, - 33 - ], - [ - -23, - 12 - ], - [ - -9, - 24 - ], - [ - 0, - 37 - ], - [ - 9, - 37 - ], - [ - -19, - 17 - ], - [ - 16, - 23 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - 10, - 16 - ], - [ - 46, - -32 - ], - [ - 16, - 16 - ], - [ - 22, - -10 - ], - [ - 12, - -25 - ], - [ - 20, - -8 - ], - [ - 17, - 26 - ] - ], - [ - [ - 6716, - 12110 - ], - [ - 18, - -66 - ], - [ - 27, - -48 - ], - [ - 32, - -51 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - -27, - -11 - ], - [ - 1, - -48 - ], - [ - 14, - -18 - ], - [ - -10, - -14 - ], - [ - 3, - -22 - ], - [ - -6, - -24 - ], - [ - -4, - -24 - ] - ], - [ - [ - 6813, - 13579 - ], - [ - 60, - -8 - ], - [ - 55, - -2 - ], - [ - 65, - -41 - ], - [ - 28, - -44 - ], - [ - 65, - 14 - ], - [ - 25, - -28 - ], - [ - 59, - -75 - ], - [ - 43, - -54 - ], - [ - 23, - 2 - ], - [ - 42, - -24 - ], - [ - -5, - -34 - ], - [ - 51, - -5 - ], - [ - 53, - -49 - ], - [ - -9, - -28 - ], - [ - -46, - -16 - ], - [ - -47, - -6 - ], - [ - -48, - 10 - ], - [ - -100, - -12 - ], - [ - 47, - 67 - ], - [ - -28, - 31 - ], - [ - -45, - 8 - ], - [ - -24, - 35 - ], - [ - -17, - 68 - ], - [ - -39, - -4 - ], - [ - -65, - 32 - ], - [ - -21, - 25 - ], - [ - -91, - 19 - ], - [ - -24, - 23 - ], - [ - 26, - 30 - ], - [ - -69, - 6 - ], - [ - -50, - -62 - ], - [ - -29, - -2 - ], - [ - -10, - -29 - ], - [ - -34, - -13 - ], - [ - -30, - 11 - ], - [ - 37, - 37 - ], - [ - 15, - 43 - ], - [ - 31, - 27 - ], - [ - 36, - 23 - ], - [ - 53, - 12 - ], - [ - 17, - 13 - ] - ], - [ - [ - 14829, - 15013 - ], - [ - 5, - 1 - ], - [ - 10, - 28 - ], - [ - 50, - -1 - ], - [ - 64, - 36 - ], - [ - -47, - -51 - ], - [ - 5, - -23 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - -8, - 4 - ], - [ - -13, - -9 - ], - [ - -10, - 3 - ], - [ - -4, - -5 - ], - [ - -1, - 12 - ], - [ - -5, - 8 - ], - [ - -14, - 1 - ], - [ - -19, - -10 - ], - [ - -13, - 6 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - 2, - -10 - ], - [ - -72, - -48 - ], - [ - -34, - 15 - ], - [ - -16, - 48 - ], - [ - 33, - 5 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - -39, - 52 - ], - [ - -36, - 28 - ], - [ - -7, - 51 - ], - [ - -12, - 36 - ], - [ - 50, - 26 - ], - [ - 26, - 30 - ], - [ - 50, - 23 - ], - [ - 18, - 23 - ], - [ - 18, - -14 - ], - [ - 31, - 12 - ] - ], - [ - [ - 13594, - 16928 - ], - [ - 33, - -38 - ], - [ - 52, - -11 - ], - [ - -4, - -33 - ], - [ - 38, - -24 - ], - [ - 10, - 30 - ], - [ - 48, - -13 - ], - [ - 7, - -37 - ], - [ - 52, - -8 - ], - [ - 32, - -59 - ] - ], - [ - [ - 13862, - 16735 - ], - [ - -21, - 0 - ], - [ - -11, - -22 - ], - [ - -16, - -5 - ], - [ - -4, - -27 - ], - [ - -14, - -6 - ], - [ - -2, - -11 - ], - [ - -23, - -12 - ], - [ - -31, - 2 - ], - [ - -10, - -27 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 9, - 86 - ], - [ - 35, - 82 - ], - [ - -100, - 22 - ], - [ - -33, - 31 - ] - ], - [ - [ - 12979, - 16731 - ], - [ - 4, - 53 - ], - [ - -14, - 27 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -12, - 126 - ], - [ - 42, - 0 - ], - [ - 18, - 45 - ], - [ - 17, - 110 - ], - [ - -13, - 40 - ] - ], - [ - [ - 13029, - 17213 - ], - [ - 13, - 26 - ], - [ - 59, - 6 - ], - [ - 13, - -26 - ], - [ - 47, - 59 - ], - [ - -16, - 45 - ], - [ - -3, - 68 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - 53, - -16 - ], - [ - 44, - 18 - ] - ], - [ - [ - 13239, - 17393 - ], - [ - 1, - -46 - ], - [ - 71, - -28 - ], - [ - -1, - -42 - ], - [ - 71, - 22 - ], - [ - 39, - 33 - ], - [ - 79, - -47 - ], - [ - 33, - -39 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 16, - -61 - ], - [ - -19, - -32 - ], - [ - 25, - -42 - ], - [ - 17, - -65 - ], - [ - -5, - -41 - ], - [ - 28, - -77 - ] - ], - [ - [ - 15551, - 12321 - ], - [ - 16, - -37 - ], - [ - -2, - -50 - ], - [ - -40, - -29 - ], - [ - 30, - -33 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - -26, - -64 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - 21 - ], - [ - -17, - -8 - ], - [ - -39, - 2 - ], - [ - -1, - 36 - ], - [ - -5, - 34 - ], - [ - 23, - 56 - ], - [ - 25, - 53 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - 30, - -11 - ], - [ - 21, - 30 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - -28, - 67 - ], - [ - -3, - 122 - ], - [ - 12, - 33 - ], - [ - 20, - 36 - ], - [ - 61, - 7 - ], - [ - 25, - 33 - ], - [ - 56, - 34 - ], - [ - -2, - -62 - ], - [ - -21, - -39 - ], - [ - 8, - -33 - ], - [ - 38, - -19 - ], - [ - -17, - -45 - ], - [ - -21, - 13 - ], - [ - -50, - -86 - ], - [ - 19, - -59 - ] - ], - [ - [ - 13432, - 17469 - ], - [ - -42, - -98 - ], - [ - -73, - 68 - ], - [ - -9, - 50 - ], - [ - 102, - 40 - ], - [ - 22, - -60 - ] - ], - [ - [ - 7549, - 13162 - ], - [ - 8, - 21 - ], - [ - 55, - -1 - ], - [ - 41, - -31 - ], - [ - 18, - 3 - ], - [ - 13, - -42 - ], - [ - 38, - 2 - ], - [ - -2, - -36 - ], - [ - 31, - -4 - ], - [ - 34, - -44 - ], - [ - -26, - -49 - ], - [ - -33, - 26 - ], - [ - -32, - -5 - ], - [ - -23, - 6 - ], - [ - -12, - -22 - ], - [ - -27, - -7 - ], - [ - -11, - 29 - ], - [ - -23, - -17 - ], - [ - -28, - -83 - ], - [ - -18, - 20 - ], - [ - -3, - 34 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - 1, - 33 - ], - [ - -18, - 36 - ], - [ - 17, - 20 - ], - [ - 6, - 46 - ], - [ - -6, - 65 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - -77, - -12 - ], - [ - -1, - 77 - ], - [ - -32, - 19 - ], - [ - -44, - 35 - ], - [ - -16, - 56 - ], - [ - -236, - 262 - ], - [ - -235, - 261 - ] - ], - [ - [ - 12204, - 13793 - ], - [ - -262, - 291 - ] - ], - [ - [ - 11942, - 14084 - ], - [ - 1, - 23 - ], - [ - 0, - 8 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - 0, - 142 - ], - [ - 112, - 89 - ], - [ - 70, - 18 - ], - [ - 57, - 32 - ], - [ - 27, - 60 - ], - [ - 81, - 48 - ], - [ - 3, - 89 - ], - [ - 41, - 10 - ], - [ - 31, - 45 - ], - [ - 91, - 20 - ], - [ - 13, - 46 - ], - [ - -18, - 26 - ], - [ - -24, - 127 - ], - [ - -4, - 72 - ], - [ - -27, - 77 - ] - ], - [ - [ - 12396, - 15016 - ], - [ - 67, - 66 - ], - [ - 76, - 21 - ], - [ - 44, - 49 - ], - [ - 67, - 37 - ], - [ - 118, - 21 - ], - [ - 115, - 10 - ], - [ - 35, - -18 - ], - [ - 66, - 47 - ], - [ - 74, - 1 - ], - [ - 29, - -28 - ], - [ - 48, - 8 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - -15, - -62 - ], - [ - 11, - -114 - ], - [ - -16, - -99 - ], - [ - -43, - -67 - ], - [ - 6, - -91 - ], - [ - 57, - -71 - ], - [ - 1, - -29 - ], - [ - 43, - -48 - ], - [ - 29, - -216 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 23, - -106 - ], - [ - 4, - -56 - ], - [ - -12, - -97 - ], - [ - 5, - -55 - ], - [ - -9, - -66 - ], - [ - 6, - -75 - ], - [ - -28, - -50 - ], - [ - 41, - -88 - ], - [ - 3, - -51 - ], - [ - 25, - -67 - ], - [ - 32, - 22 - ], - [ - 55, - -56 - ], - [ - 31, - -75 - ] - ], - [ - [ - 13384, - 13613 - ], - [ - -239, - -229 - ], - [ - -202, - -235 - ], - [ - -98, - -54 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - 10, - -91 - ], - [ - -22, - -78 - ], - [ - -76, - -126 - ], - [ - -83, - -47 - ], - [ - -43, - -104 - ], - [ - -13, - -81 - ], - [ - -40, - -50 - ], - [ - -29, - 61 - ], - [ - -28, - 13 - ], - [ - -29, - -10 - ], - [ - -2, - 44 - ], - [ - 20, - 29 - ], - [ - -8, - 50 - ] - ], - [ - [ - 6950, - 10389 - ], - [ - 37, - 89 - ], - [ - -15, - 53 - ], - [ - -27, - -56 - ], - [ - -42, - 53 - ], - [ - 15, - 33 - ], - [ - -12, - 109 - ], - [ - 24, - 18 - ], - [ - 13, - 75 - ], - [ - 26, - 77 - ], - [ - -4, - 49 - ], - [ - 38, - 26 - ], - [ - 48, - 48 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - -276, - 0 - ], - [ - -271, - 0 - ], - [ - -280, - 0 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - 441 - ], - [ - 0, - 427 - ], - [ - -21, - 97 - ], - [ - 18, - 74 - ], - [ - -11, - 51 - ], - [ - 26, - 58 - ] - ], - [ - [ - 14302, - 14585 - ], - [ - 92, - 1 - ], - [ - 68, - -31 - ], - [ - 69, - -36 - ], - [ - 32, - -18 - ], - [ - 54, - 38 - ], - [ - 28, - 34 - ], - [ - 62, - 10 - ], - [ - 49, - -15 - ], - [ - 19, - -60 - ], - [ - 17, - 39 - ], - [ - 55, - -28 - ], - [ - 55, - -7 - ], - [ - 34, - 31 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 39, - -175 - ], - [ - 7, - -32 - ] - ], - [ - [ - 14982, - 14336 - ], - [ - -20, - -48 - ], - [ - -15, - -90 - ], - [ - -19, - -63 - ], - [ - -16, - -21 - ], - [ - -23, - 39 - ], - [ - -32, - 53 - ], - [ - -49, - 172 - ], - [ - -7, - -10 - ], - [ - 28, - -127 - ], - [ - 43, - -121 - ], - [ - 53, - -187 - ], - [ - 26, - -65 - ], - [ - 22, - -68 - ], - [ - 63, - -132 - ], - [ - -14, - -21 - ], - [ - 2, - -78 - ], - [ - 81, - -108 - ], - [ - 12, - -24 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - -24, - 39 - ], - [ - -29, - 70 - ], - [ - -31, - 39 - ], - [ - -18, - 41 - ], - [ - -60, - 48 - ], - [ - -48, - 2 - ], - [ - -17, - 25 - ], - [ - -41, - -29 - ], - [ - -42, - 55 - ], - [ - -22, - -90 - ], - [ - -81, - 25 - ] - ], - [ - [ - 15087, - 12527 - ], - [ - -7, - 48 - ], - [ - 30, - 177 - ], - [ - 6, - 79 - ], - [ - 22, - 37 - ], - [ - 52, - 20 - ], - [ - 35, - 68 - ] - ], - [ - [ - 15225, - 12956 - ], - [ - 40, - -138 - ], - [ - 20, - -111 - ], - [ - 38, - -58 - ], - [ - 95, - -113 - ], - [ - 39, - -69 - ], - [ - 38, - -69 - ], - [ - 21, - -41 - ], - [ - 35, - -36 - ] - ], - [ - [ - 11918, - 15822 - ], - [ - 3, - 85 - ], - [ - -28, - 52 - ], - [ - 98, - 87 - ], - [ - 86, - -22 - ], - [ - 93, - 1 - ], - [ - 74, - -21 - ], - [ - 58, - 7 - ], - [ - 113, - -4 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 28, - -47 - ], - [ - 128, - -55 - ], - [ - 25, - 26 - ], - [ - 79, - -54 - ], - [ - 81, - 16 - ] - ], - [ - [ - 12756, - 15893 - ], - [ - 3, - -70 - ], - [ - -66, - -80 - ], - [ - -89, - -25 - ], - [ - -6, - -41 - ], - [ - -43, - -66 - ], - [ - -27, - -98 - ], - [ - 27, - -68 - ], - [ - -40, - -54 - ], - [ - -15, - -78 - ], - [ - -53, - -24 - ], - [ - -49, - -92 - ], - [ - -89, - -2 - ], - [ - -66, - 2 - ], - [ - -44, - -42 - ], - [ - -26, - -45 - ], - [ - -34, - 10 - ], - [ - -26, - 40 - ], - [ - -20, - 69 - ], - [ - -65, - 19 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -6, - 39 - ], - [ - 26, - 45 - ], - [ - 10, - 33 - ], - [ - -25, - 36 - ], - [ - 20, - 79 - ], - [ - -28, - 72 - ], - [ - 30, - 9 - ], - [ - 3, - 57 - ], - [ - 11, - 18 - ], - [ - 1, - 93 - ], - [ - 32, - 33 - ], - [ - -19, - 60 - ], - [ - -41, - 4 - ], - [ - -12, - -15 - ], - [ - -41, - 0 - ], - [ - -18, - 59 - ], - [ - -28, - -18 - ], - [ - -25, - -30 - ] - ], - [ - [ - 14242, - 17731 - ], - [ - 8, - 70 - ], - [ - -25, - -15 - ], - [ - -44, - 43 - ], - [ - -7, - 69 - ], - [ - 89, - 33 - ], - [ - 87, - 18 - ], - [ - 76, - -20 - ], - [ - 72, - 3 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 11, - -21 - ], - [ - -50, - -69 - ], - [ - 21, - -112 - ], - [ - -30, - -38 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - -58, - 1 - ], - [ - -60, - 44 - ], - [ - -30, - 15 - ], - [ - -60, - -21 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - -42 - ], - [ - 26, - -66 - ], - [ - 26, - -58 - ], - [ - 26, - -43 - ], - [ - 228, - -142 - ], - [ - 59, - 0 - ] - ], - [ - [ - 15879, - 11757 - ], - [ - -197, - -360 - ], - [ - -91, - -5 - ], - [ - -62, - -85 - ], - [ - -45, - -2 - ], - [ - -19, - -38 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -47, - 0 - ], - [ - -29, - 41 - ], - [ - -63, - -50 - ], - [ - -21, - -50 - ], - [ - -46, - 9 - ], - [ - -16, - 14 - ], - [ - -16, - -3 - ], - [ - -22, - 1 - ], - [ - -88, - 102 - ], - [ - -49, - 0 - ], - [ - -24, - 39 - ], - [ - 0, - 68 - ], - [ - -36, - 20 - ] - ], - [ - [ - 15008, - 11458 - ], - [ - -41, - 130 - ], - [ - -32, - 28 - ], - [ - -12, - 48 - ], - [ - -36, - 59 - ], - [ - -42, - 8 - ], - [ - 23, - 68 - ], - [ - 37, - 3 - ], - [ - 11, - 37 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 108 - ], - [ - 21, - 125 - ], - [ - 33, - 34 - ], - [ - 7, - 49 - ], - [ - 29, - 92 - ], - [ - 42, - 59 - ], - [ - 29, - 118 - ], - [ - 11, - 103 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -24, - 47 - ], - [ - -2, - 184 - ], - [ - -108, - 82 - ], - [ - -93, - 59 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - 41, - 31 - ], - [ - 78, - -63 - ], - [ - 91, - 6 - ], - [ - 75, - -29 - ], - [ - 66, - 53 - ], - [ - 34, - 88 - ], - [ - 109, - 41 - ], - [ - 89, - -48 - ], - [ - -29, - -84 - ] - ], - [ - [ - 14541, - 19083 - ], - [ - -11, - -84 - ], - [ - 107, - -80 - ], - [ - -64, - -91 - ], - [ - 81, - -136 - ], - [ - -47, - -103 - ], - [ - 63, - -89 - ], - [ - -29, - -78 - ], - [ - 103, - -83 - ], - [ - -26, - -61 - ], - [ - -65, - -69 - ], - [ - -149, - -153 - ] - ], - [ - [ - 14504, - 18056 - ], - [ - -126, - -10 - ], - [ - -123, - -44 - ], - [ - -113, - -25 - ], - [ - -41, - 65 - ], - [ - -67, - 40 - ], - [ - 15, - 118 - ], - [ - -33, - 108 - ], - [ - 33, - 70 - ], - [ - 63, - 75 - ], - [ - 159, - 130 - ], - [ - 47, - 26 - ], - [ - -7, - 50 - ], - [ - -97, - 57 - ] - ], - [ - [ - 24982, - 8717 - ], - [ - 24, - -35 - ], - [ - -12, - -62 - ], - [ - -43, - -17 - ], - [ - -39, - 15 - ], - [ - -6, - 53 - ], - [ - 27, - 41 - ], - [ - 31, - -15 - ], - [ - 18, - 20 - ] - ], - [ - [ - 25051, - 8782 - ], - [ - -45, - -26 - ], - [ - -9, - 45 - ], - [ - 35, - 25 - ], - [ - 22, - 6 - ], - [ - 41, - 38 - ], - [ - 0, - -59 - ], - [ - -44, - -29 - ] - ], - [ - [ - 6, - 8817 - ], - [ - -6, - -6 - ], - [ - 0, - 59 - ], - [ - 14, - 5 - ], - [ - -8, - -58 - ] - ], - [ - [ - 8281, - 4577 - ], - [ - 84, - 72 - ], - [ - 59, - -30 - ], - [ - 42, - 48 - ], - [ - 56, - -54 - ], - [ - -21, - -42 - ], - [ - -94, - -36 - ], - [ - -32, - 42 - ], - [ - -59, - -54 - ], - [ - -35, - 54 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - 16, - -10 - ], - [ - 20, - 2 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -3, - -34 - ], - [ - 20, - -45 - ], - [ - -24, - -37 - ], - [ - 18, - -93 - ], - [ - 38, - -15 - ], - [ - -8, - -52 - ] - ], - [ - [ - 13066, - 16039 - ], - [ - -63, - -68 - ], - [ - -138, - 33 - ], - [ - -101, - -39 - ], - [ - -8, - -72 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 36, - 72 - ], - [ - 13, - 239 - ], - [ - -72, - 125 - ], - [ - -51, - 61 - ], - [ - -107, - 46 - ], - [ - -7, - 88 - ], - [ - 91, - 26 - ], - [ - 117, - -31 - ], - [ - -22, - 136 - ], - [ - 66, - -52 - ], - [ - 162, - 94 - ], - [ - 21, - 98 - ], - [ - 61, - 24 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 17, - 51 - ], - [ - 6, - 54 - ], - [ - 12, - 52 - ], - [ - -27, - 71 - ], - [ - -5, - 82 - ], - [ - 36, - 103 - ] - ], - [ - [ - 8786, - 11488 - ], - [ - 24, - -13 - ], - [ - 51, - -29 - ], - [ - 74, - -101 - ], - [ - 12, - -49 - ] - ], - [ - [ - 13214, - 15854 - ], - [ - -23, - -92 - ], - [ - -32, - 24 - ], - [ - -16, - 81 - ], - [ - 14, - 44 - ], - [ - 45, - 46 - ], - [ - 12, - -103 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - -72, - 121 - ], - [ - -46, - 99 - ], - [ - -42, - 124 - ], - [ - 2, - 40 - ], - [ - 15, - 38 - ], - [ - 17, - 87 - ], - [ - 14, - 89 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - 24, - 7 - ], - [ - 101, - -1 - ], - [ - 0, - 144 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - -52, - 24 - ], - [ - -43, - -1 - ], - [ - 14, - 64 - ], - [ - -14, - 64 - ] - ], - [ - [ - 12020, - 17411 - ], - [ - 58, - 5 - ], - [ - 75, - -74 - ], - [ - -38, - -82 - ] - ], - [ - [ - 12338, - 17832 - ], - [ - -74, - -130 - ], - [ - 71, - 16 - ], - [ - 76, - 0 - ], - [ - -18, - -98 - ], - [ - -63, - -108 - ], - [ - 72, - -7 - ], - [ - 6, - -13 - ], - [ - 62, - -142 - ], - [ - 47, - -19 - ], - [ - 43, - -136 - ], - [ - 20, - -48 - ], - [ - 85, - -23 - ], - [ - -9, - -76 - ], - [ - -35, - -36 - ], - [ - 28, - -62 - ], - [ - -63, - -63 - ], - [ - -93, - 2 - ], - [ - -119, - -33 - ], - [ - -33, - 23 - ], - [ - -46, - -56 - ], - [ - -64, - 14 - ], - [ - -49, - -46 - ], - [ - -37, - 24 - ], - [ - 102, - 126 - ], - [ - 62, - 26 - ], - [ - 0, - 0 - ], - [ - -109, - 20 - ], - [ - -20, - 48 - ], - [ - 73, - 37 - ], - [ - -38, - 64 - ], - [ - 13, - 79 - ], - [ - 104, - -11 - ], - [ - 10, - 70 - ], - [ - -46, - 74 - ], - [ - -2, - 1 - ], - [ - -84, - 21 - ], - [ - -17, - 33 - ], - [ - 26, - 53 - ], - [ - -23, - 34 - ], - [ - -38, - -57 - ], - [ - -4, - 115 - ], - [ - -35, - 62 - ], - [ - 25, - 124 - ], - [ - 54, - 97 - ], - [ - 56, - -10 - ], - [ - 84, - 11 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - -68, - 59 - ], - [ - -74, - -6 - ] - ], - [ - [ - 15444, - 15780 - ], - [ - 11, - 51 - ], - [ - -18, - 82 - ], - [ - -40, - 44 - ], - [ - -39, - 14 - ], - [ - -25, - 37 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - 8, - 14 - ], - [ - 59, - -20 - ], - [ - 103, - -20 - ], - [ - 95, - -57 - ], - [ - 12, - -23 - ], - [ - 42, - 19 - ], - [ - 65, - -25 - ], - [ - 21, - -49 - ], - [ - 44, - -28 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -5, - -37 - ], - [ - 29, - -62 - ], - [ - 0, - -87 - ], - [ - 7, - -95 - ], - [ - 17, - -44 - ], - [ - -15, - -108 - ], - [ - 5, - -59 - ], - [ - 19, - -76 - ], - [ - 15, - -43 - ] - ], - [ - [ - 12621, - 11508 - ], - [ - -109, - -70 - ], - [ - -39, - -41 - ], - [ - -62, - -35 - ], - [ - -63, - 34 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - -20, - 3 - ], - [ - -14, - -48 - ], - [ - -19, - 1 - ], - [ - -14, - 25 - ], - [ - 5, - 48 - ], - [ - -30, - 74 - ], - [ - -18, - -14 - ], - [ - -15, - -2 - ] - ], - [ - [ - 11834, - 11806 - ], - [ - -19, - -7 - ], - [ - 1, - 44 - ], - [ - -11, - 31 - ], - [ - 2, - 35 - ], - [ - -15, - 50 - ], - [ - -19, - 43 - ], - [ - -56, - 1 - ], - [ - -16, - -23 - ], - [ - -20, - -3 - ], - [ - -12, - -26 - ], - [ - -8, - -33 - ], - [ - -37, - -53 - ] - ], - [ - [ - 11624, - 11865 - ], - [ - -30, - 71 - ], - [ - -28, - 47 - ], - [ - -17, - 16 - ], - [ - -18, - 24 - ], - [ - -8, - 53 - ], - [ - -10, - 26 - ], - [ - -20, - 20 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - 31, - 58 - ], - [ - 21, - -2 - ], - [ - 18, - 20 - ], - [ - 15, - 0 - ], - [ - 11, - 16 - ], - [ - -5, - 40 - ], - [ - 7, - 12 - ], - [ - 1, - 41 - ] - ], - [ - [ - 11592, - 12307 - ], - [ - 34, - -1 - ], - [ - 50, - -29 - ], - [ - 16, - 2 - ], - [ - 5, - 14 - ], - [ - 38, - -10 - ], - [ - 10, - 7 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 4, - -44 - ], - [ - 11, - 0 - ], - [ - 18, - 16 - ], - [ - 12, - -4 - ], - [ - 19, - -30 - ], - [ - 30, - -10 - ], - [ - 19, - 26 - ], - [ - 23, - 16 - ], - [ - 16, - 17 - ], - [ - 14, - -3 - ], - [ - 16, - -27 - ], - [ - 8, - -33 - ], - [ - 29, - -50 - ], - [ - -15, - -31 - ], - [ - -2, - -39 - ], - [ - 14, - 12 - ], - [ - 9, - -14 - ], - [ - -4, - -36 - ], - [ - 22, - -34 - ] - ], - [ - [ - 11374, - 12375 - ], - [ - 8, - 53 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - 76, - 4 - ], - [ - 16, - 28 - ], - [ - 22, - 2 - ], - [ - 28, - -30 - ], - [ - 21, - 0 - ], - [ - 23, - 20 - ], - [ - 14, - -35 - ], - [ - -30, - -27 - ], - [ - -30, - 3 - ], - [ - -30, - 25 - ], - [ - -26, - -28 - ], - [ - -12, - -1 - ], - [ - -17, - -17 - ], - [ - -63, - 3 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - -37, - 50 - ], - [ - -30, - 8 - ], - [ - -16, - 34 - ], - [ - 1, - 18 - ], - [ - -22, - 25 - ], - [ - -4, - 26 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - 37, - 20 - ], - [ - 23, - -4 - ], - [ - 19, - 13 - ], - [ - 128, - -5 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - -13, - 18 - ], - [ - 24, - 135 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - 45, - 11 - ], - [ - 27, - 26 - ], - [ - 38, - -2 - ], - [ - 11, - 20 - ], - [ - 13, - 4 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - 34, - -32 - ], - [ - -22, - -75 - ], - [ - -16, - -13 - ] - ], - [ - [ - 14364, - 15695 - ], - [ - -43, - 3 - ], - [ - -36, - 12 - ], - [ - -84, - -32 - ], - [ - 48, - -67 - ], - [ - -35, - -20 - ], - [ - -39, - 0 - ], - [ - -37, - 62 - ], - [ - -13, - -26 - ], - [ - 15, - -72 - ], - [ - 35, - -56 - ], - [ - -26, - -27 - ], - [ - 39, - -55 - ], - [ - 34, - -35 - ], - [ - 1, - -67 - ], - [ - -64, - 31 - ], - [ - 20, - -61 - ], - [ - -44, - -12 - ], - [ - 27, - -106 - ], - [ - -47, - -2 - ], - [ - -57, - 52 - ], - [ - -26, - 96 - ], - [ - -12, - 80 - ], - [ - -27, - 55 - ], - [ - -36, - 69 - ], - [ - -5, - 34 - ] - ], - [ - [ - 14200, - 15081 - ], - [ - 38, - -41 - ], - [ - 54, - 7 - ], - [ - 52, - -8 - ], - [ - -2, - -21 - ], - [ - 38, - 14 - ], - [ - -9, - -35 - ], - [ - -100, - -10 - ], - [ - 1, - 19 - ], - [ - -85, - 24 - ], - [ - 13, - 51 - ] - ], - [ - [ - 9288, - 20710 - ], - [ - 234, - 72 - ], - [ - 244, - -6 - ], - [ - 89, - 44 - ], - [ - 247, - 12 - ], - [ - 556, - -15 - ], - [ - 436, - -95 - ], - [ - -128, - -46 - ], - [ - -267, - -6 - ], - [ - -375, - -11 - ], - [ - 35, - -22 - ], - [ - 247, - 13 - ], - [ - 210, - -41 - ], - [ - 135, - 37 - ], - [ - 58, - -43 - ], - [ - -77, - -70 - ], - [ - 178, - 45 - ], - [ - 338, - 46 - ], - [ - 209, - -23 - ], - [ - 39, - -51 - ], - [ - -284, - -86 - ], - [ - -39, - -27 - ], - [ - -223, - -21 - ], - [ - 162, - -6 - ], - [ - -82, - -87 - ], - [ - -56, - -78 - ], - [ - 2, - -134 - ], - [ - 84, - -78 - ], - [ - -109, - -5 - ], - [ - -115, - -38 - ], - [ - 129, - -63 - ], - [ - 16, - -102 - ], - [ - -74, - -11 - ], - [ - 90, - -104 - ], - [ - -155, - -8 - ], - [ - 81, - -49 - ], - [ - -23, - -42 - ], - [ - -98, - -19 - ], - [ - -97, - 0 - ], - [ - 87, - -82 - ], - [ - 1, - -53 - ], - [ - -138, - 50 - ], - [ - -36, - -32 - ], - [ - 94, - -30 - ], - [ - 92, - -74 - ], - [ - 26, - -96 - ], - [ - -124, - -23 - ], - [ - -54, - 46 - ], - [ - -86, - 69 - ], - [ - 24, - -82 - ], - [ - -81, - -63 - ], - [ - 184, - -5 - ], - [ - 96, - -6 - ], - [ - -187, - -105 - ], - [ - -190, - -94 - ], - [ - -204, - -42 - ], - [ - -77, - 0 - ], - [ - -72, - -47 - ], - [ - -97, - -126 - ], - [ - -150, - -84 - ], - [ - -48, - -5 - ], - [ - -93, - -30 - ], - [ - -100, - -28 - ], - [ - -59, - -74 - ], - [ - -1, - -84 - ], - [ - -36, - -79 - ], - [ - -113, - -96 - ], - [ - 28, - -94 - ], - [ - -32, - -99 - ], - [ - -35, - -117 - ], - [ - -99, - -7 - ], - [ - -102, - 98 - ], - [ - -140, - 0 - ], - [ - -67, - 66 - ], - [ - -47, - 117 - ], - [ - -121, - 149 - ], - [ - -35, - 79 - ], - [ - -10, - 107 - ], - [ - -96, - 111 - ], - [ - 25, - 88 - ], - [ - -47, - 43 - ], - [ - 69, - 140 - ], - [ - 105, - 45 - ], - [ - 28, - 50 - ], - [ - 14, - 94 - ], - [ - -79, - -43 - ], - [ - -38, - -18 - ], - [ - -63, - -17 - ], - [ - -85, - 39 - ], - [ - -5, - 82 - ], - [ - 27, - 64 - ], - [ - 65, - 1 - ], - [ - 142, - -32 - ], - [ - -120, - 77 - ], - [ - -62, - 41 - ], - [ - -69, - -17 - ], - [ - -59, - 29 - ], - [ - 78, - 112 - ], - [ - -42, - 45 - ], - [ - -56, - 83 - ], - [ - -83, - 127 - ], - [ - -89, - 47 - ], - [ - 1, - 50 - ], - [ - -187, - 70 - ], - [ - -148, - 9 - ], - [ - -187, - -5 - ], - [ - -170, - -9 - ], - [ - -81, - 38 - ], - [ - -121, - 76 - ], - [ - 183, - 38 - ], - [ - 140, - 6 - ], - [ - -298, - 31 - ], - [ - -157, - 49 - ], - [ - 10, - 47 - ], - [ - 264, - 57 - ], - [ - 255, - 58 - ], - [ - 27, - 44 - ], - [ - -188, - 43 - ], - [ - 60, - 48 - ], - [ - 242, - 83 - ], - [ - 101, - 13 - ], - [ - -29, - 54 - ], - [ - 165, - 32 - ], - [ - 215, - 19 - ], - [ - 214, - 1 - ], - [ - 76, - -38 - ], - [ - 185, - 66 - ], - [ - 166, - -45 - ], - [ - 98, - -9 - ], - [ - 145, - -39 - ], - [ - -166, - 65 - ], - [ - 10, - 51 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - 23, - -22 - ], - [ - 6, - 18 - ], - [ - 20, - -15 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - -31, - -46 - ], - [ - -33, - -33 - ], - [ - -5, - -23 - ], - [ - 5, - -24 - ], - [ - -14, - -30 - ] - ], - [ - [ - 6319, - 12528 - ], - [ - -16, - -8 - ], - [ - 3, - -14 - ], - [ - -13, - -13 - ], - [ - -24, - -30 - ], - [ - -2, - -18 - ] - ], - [ - [ - 6267, - 12445 - ], - [ - -36, - 21 - ], - [ - -43, - 2 - ], - [ - -32, - 24 - ], - [ - -38, - 49 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - 2, - 35 - ], - [ - 8, - 28 - ], - [ - -10, - 23 - ], - [ - 34, - 98 - ], - [ - 89, - 0 - ], - [ - 2, - 41 - ], - [ - -11, - 7 - ], - [ - -8, - 26 - ], - [ - -26, - 28 - ], - [ - -26, - 40 - ], - [ - 32, - 0 - ], - [ - 0, - 68 - ], - [ - 65, - 0 - ], - [ - 64, - -1 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - -47, - 91 - ], - [ - 19, - 33 - ], - [ - -2, - 56 - ], - [ - 43, - 19 - ], - [ - 17, - 22 - ], - [ - -23, - 45 - ], - [ - 6, - 44 - ], - [ - 55, - 70 - ] - ], - [ - [ - 8382, - 11801 - ], - [ - 46, - -44 - ], - [ - 43, - -78 - ], - [ - 2, - -62 - ], - [ - 26, - -3 - ], - [ - 37, - -58 - ], - [ - 28, - -42 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - -11, - -108 - ], - [ - -43, - -31 - ], - [ - 4, - -29 - ], - [ - -13, - -62 - ], - [ - 31, - -87 - ], - [ - 23, - 0 - ], - [ - 9, - -68 - ], - [ - 42, - -104 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - 8, - -5 - ], - [ - 15, - 21 - ], - [ - 20, - 2 - ], - [ - 6, - -10 - ], - [ - 11, - 6 - ], - [ - 33, - -10 - ], - [ - 32, - 3 - ], - [ - 22, - 13 - ], - [ - 8, - 13 - ], - [ - 23, - -6 - ], - [ - 16, - -8 - ], - [ - 19, - 3 - ], - [ - 13, - 10 - ], - [ - 32, - -16 - ], - [ - 11, - -3 - ], - [ - 22, - -23 - ], - [ - 20, - -26 - ], - [ - 25, - -19 - ], - [ - 18, - -33 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -23, - 3 - ], - [ - -10, - -17 - ], - [ - -24, - -15 - ], - [ - -18, - 0 - ], - [ - -15, - -16 - ], - [ - -14, - 6 - ], - [ - -12, - 18 - ], - [ - -7, - -3 - ], - [ - -9, - -29 - ], - [ - -7, - 1 - ], - [ - -1, - -25 - ], - [ - -25, - -33 - ], - [ - -12, - -14 - ], - [ - -8, - -15 - ], - [ - -20, - 24 - ], - [ - -15, - -32 - ], - [ - -15, - 1 - ], - [ - -16, - -3 - ], - [ - 1, - -59 - ], - [ - -10, - -1 - ], - [ - -9, - -27 - ], - [ - -21, - -5 - ] - ], - [ - [ - 6461, - 12355 - ], - [ - -12, - 37 - ], - [ - -21, - 11 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - 4, - 48 - ], - [ - -9, - 13 - ], - [ - -14, - 9 - ], - [ - -31, - -15 - ], - [ - -3, - 16 - ], - [ - -21, - 20 - ], - [ - -15, - 24 - ], - [ - -20, - 10 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -7, - -21 - ] - ], - [ - [ - 13834, - 15893 - ], - [ - -66, - 45 - ], - [ - -40, - 43 - ], - [ - -64, - 36 - ], - [ - -59, - 88 - ], - [ - 14, - 9 - ], - [ - -31, - 50 - ], - [ - -2, - 41 - ], - [ - -45, - 19 - ], - [ - -21, - -52 - ], - [ - -20, - 40 - ], - [ - 1, - 42 - ], - [ - 3, - 2 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 48, - -4 - ], - [ - 13, - 20 - ], - [ - 24, - -20 - ], - [ - 27, - -2 - ], - [ - 0, - 34 - ], - [ - 24, - 12 - ], - [ - 7, - 48 - ], - [ - 55, - 32 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - 22, - -15 - ], - [ - 52, - -51 - ], - [ - 58, - -23 - ], - [ - 26, - 18 - ] - ], - [ - [ - 13860, - 16305 - ], - [ - 17, - -47 - ], - [ - 22, - -34 - ], - [ - -27, - -45 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - -46, - 20 - ], - [ - -33, - -8 - ], - [ - -43, - 9 - ], - [ - -33, - -23 - ], - [ - -37, - 38 - ], - [ - 6, - 38 - ], - [ - 64, - -16 - ], - [ - 53, - -10 - ], - [ - 25, - 27 - ], - [ - -32, - 52 - ], - [ - 1, - 46 - ], - [ - -44, - 18 - ], - [ - 16, - 33 - ], - [ - 42, - -5 - ], - [ - 61, - -19 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - 36, - -31 - ], - [ - 25, - -13 - ], - [ - 59, - 14 - ], - [ - 5, - 25 - ], - [ - 28, - 3 - ], - [ - 34, - 19 - ], - [ - 8, - -8 - ], - [ - 32, - 15 - ], - [ - 17, - 28 - ], - [ - 23, - 8 - ], - [ - 74, - -37 - ], - [ - 15, - 12 - ] - ], - [ - [ - 14087, - 16606 - ], - [ - 39, - -32 - ], - [ - 5, - -32 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - -43, - -26 - ], - [ - -33, - -81 - ], - [ - -42, - -81 - ], - [ - -56, - -23 - ] - ], - [ - [ - 13957, - 16331 - ], - [ - -43, - 5 - ], - [ - -54, - -31 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - -13, - 41 - ], - [ - -12, - 1 - ] - ], - [ - [ - 20962, - 9569 - ], - [ - -29, - -3 - ], - [ - -92, - 85 - ], - [ - 65, - 23 - ], - [ - 36, - -36 - ], - [ - 25, - -37 - ], - [ - -5, - -32 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 7, - -23 - ], - [ - 1, - -37 - ] - ], - [ - [ - 21267, - 9670 - ], - [ - -45, - -89 - ], - [ - -60, - -27 - ], - [ - -8, - 15 - ], - [ - 6, - 40 - ], - [ - 30, - 74 - ], - [ - 69, - 47 - ] - ], - [ - [ - 20766, - 9826 - ], - [ - 25, - -32 - ], - [ - 43, - 10 - ], - [ - 18, - -51 - ], - [ - -81, - -24 - ], - [ - -48, - -16 - ], - [ - -38, - 1 - ], - [ - 24, - 69 - ], - [ - 38, - 1 - ], - [ - 19, - 42 - ] - ], - [ - [ - 21115, - 9826 - ], - [ - -10, - -67 - ], - [ - -105, - -34 - ], - [ - -93, - 15 - ], - [ - 0, - 44 - ], - [ - 55, - 25 - ], - [ - 44, - -36 - ], - [ - 46, - 9 - ], - [ - 63, - 44 - ] - ], - [ - [ - 20119, - 9984 - ], - [ - 134, - -12 - ], - [ - 15, - 50 - ], - [ - 130, - -58 - ], - [ - 25, - -78 - ], - [ - 105, - -22 - ], - [ - 85, - -71 - ], - [ - -79, - -46 - ], - [ - -77, - 49 - ], - [ - -63, - -4 - ], - [ - -72, - 9 - ], - [ - -66, - 22 - ], - [ - -80, - 46 - ], - [ - -52, - 11 - ], - [ - -29, - -15 - ], - [ - -127, - 50 - ], - [ - -12, - 51 - ], - [ - -64, - 9 - ], - [ - 48, - 115 - ], - [ - 85, - -7 - ], - [ - 56, - -47 - ], - [ - 29, - -9 - ], - [ - 9, - -43 - ] - ], - [ - [ - 21939, - 10052 - ], - [ - -36, - -82 - ], - [ - -7, - 90 - ], - [ - 13, - 43 - ], - [ - 14, - 41 - ], - [ - 16, - -35 - ], - [ - 0, - -57 - ] - ], - [ - [ - 21418, - 10382 - ], - [ - -26, - -40 - ], - [ - -48, - 22 - ], - [ - -14, - 52 - ], - [ - 71, - 6 - ], - [ - 17, - -40 - ] - ], - [ - [ - 21642, - 10426 - ], - [ - 26, - -92 - ], - [ - -59, - 50 - ], - [ - -58, - 10 - ], - [ - -40, - -8 - ], - [ - -48, - 4 - ], - [ - 17, - 66 - ], - [ - 86, - 5 - ], - [ - 76, - -35 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 2, - -391 - ], - [ - 1, - -391 - ] - ], - [ - [ - 22379, - 9703 - ], - [ - -62, - 99 - ], - [ - -71, - 24 - ], - [ - -17, - -34 - ], - [ - -89, - -4 - ], - [ - 30, - 98 - ], - [ - 44, - 33 - ], - [ - -18, - 130 - ], - [ - -34, - 101 - ], - [ - -135, - 102 - ], - [ - -57, - 10 - ], - [ - -105, - 111 - ], - [ - -21, - -59 - ], - [ - -26, - -10 - ], - [ - -16, - 44 - ], - [ - 0, - 52 - ], - [ - -54, - 59 - ], - [ - 75, - 43 - ], - [ - 50, - -2 - ], - [ - -6, - 32 - ], - [ - -102, - 0 - ], - [ - -27, - 71 - ], - [ - -63, - 22 - ], - [ - -29, - 60 - ], - [ - 94, - 29 - ], - [ - 35, - 39 - ], - [ - 112, - -49 - ], - [ - 11, - -45 - ], - [ - 20, - -194 - ], - [ - 72, - -72 - ], - [ - 58, - 127 - ], - [ - 80, - 73 - ], - [ - 62, - 0 - ], - [ - 60, - -42 - ], - [ - 52, - -43 - ], - [ - 74, - -23 - ] - ], - [ - [ - 21278, - 10968 - ], - [ - -56, - -119 - ], - [ - -53, - -24 - ], - [ - -67, - 24 - ], - [ - -116, - -6 - ], - [ - -61, - -17 - ], - [ - -10, - -91 - ], - [ - 63, - -107 - ], - [ - 37, - 55 - ], - [ - 130, - 40 - ], - [ - -5, - -55 - ], - [ - -31, - 18 - ], - [ - -30, - -71 - ], - [ - -61, - -46 - ], - [ - 66, - -154 - ], - [ - -13, - -41 - ], - [ - 63, - -139 - ], - [ - -1, - -79 - ], - [ - -37, - -35 - ], - [ - -28, - 42 - ], - [ - 34, - 99 - ], - [ - -68, - -47 - ], - [ - -18, - 33 - ], - [ - 9, - 47 - ], - [ - -50, - 70 - ], - [ - 5, - 117 - ], - [ - -46, - -37 - ], - [ - 6, - -139 - ], - [ - 3, - -172 - ], - [ - -45, - -17 - ], - [ - -30, - 35 - ], - [ - 20, - 110 - ], - [ - -10, - 116 - ], - [ - -30, - 1 - ], - [ - -21, - 82 - ], - [ - 28, - 79 - ], - [ - 10, - 95 - ], - [ - 35, - 181 - ], - [ - 15, - 49 - ], - [ - 59, - 89 - ], - [ - 55, - -35 - ], - [ - 88, - -17 - ], - [ - 80, - 5 - ], - [ - 69, - 87 - ], - [ - 12, - -26 - ] - ], - [ - [ - 21518, - 10933 - ], - [ - -4, - -105 - ], - [ - -35, - 12 - ], - [ - -11, - -73 - ], - [ - 29, - -63 - ], - [ - -20, - -15 - ], - [ - -28, - 76 - ], - [ - -21, - 154 - ], - [ - 14, - 95 - ], - [ - 23, - 44 - ], - [ - 5, - -65 - ], - [ - 42, - -11 - ], - [ - 6, - -49 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 12, - -80 - ], - [ - 47, - -68 - ], - [ - 45, - 24 - ], - [ - 45, - -8 - ], - [ - 40, - 60 - ], - [ - 34, - 11 - ], - [ - 66, - -34 - ], - [ - 57, - 26 - ], - [ - 35, - 167 - ], - [ - 27, - 41 - ], - [ - 24, - 137 - ], - [ - 80, - 0 - ], - [ - 61, - -20 - ] - ], - [ - [ - 20765, - 11294 - ], - [ - -40, - -109 - ], - [ - 51, - -113 - ], - [ - -12, - -56 - ], - [ - 79, - -111 - ], - [ - -83, - -14 - ], - [ - -23, - -82 - ], - [ - 3, - -108 - ], - [ - -67, - -82 - ], - [ - -2, - -120 - ], - [ - -27, - -183 - ], - [ - -10, - 42 - ], - [ - -79, - -54 - ], - [ - -28, - 74 - ], - [ - -50, - 7 - ], - [ - -35, - 38 - ], - [ - -82, - -43 - ], - [ - -26, - 58 - ], - [ - -46, - -7 - ], - [ - -57, - 14 - ], - [ - -11, - 161 - ], - [ - -34, - 33 - ], - [ - -34, - 103 - ], - [ - -10, - 105 - ], - [ - 9, - 111 - ], - [ - 41, - 80 - ] - ], - [ - [ - 19924, - 10095 - ], - [ - -77, - -2 - ], - [ - -59, - 100 - ], - [ - -90, - 98 - ], - [ - -29, - 73 - ], - [ - -53, - 97 - ], - [ - -35, - 90 - ], - [ - -53, - 168 - ], - [ - -61, - 100 - ], - [ - -20, - 103 - ], - [ - -26, - 94 - ], - [ - -63, - 75 - ], - [ - -36, - 103 - ], - [ - -53, - 67 - ], - [ - -73, - 133 - ], - [ - -6, - 61 - ], - [ - 45, - -5 - ], - [ - 108, - -23 - ], - [ - 62, - -118 - ], - [ - 54, - -81 - ], - [ - 38, - -50 - ], - [ - 66, - -129 - ], - [ - 71, - -2 - ], - [ - 58, - -82 - ], - [ - 41, - -100 - ], - [ - 53, - -55 - ], - [ - -28, - -98 - ], - [ - 40, - -42 - ], - [ - 25, - -3 - ], - [ - 12, - -84 - ], - [ - 24, - -67 - ], - [ - 51, - -10 - ], - [ - 34, - -76 - ], - [ - -17, - -149 - ], - [ - -3, - -186 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -10, - -44 - ], - [ - -48, - 2 - ], - [ - -86, - -25 - ], - [ - 4, - -90 - ], - [ - -37, - -71 - ], - [ - -100, - -81 - ], - [ - -78, - -141 - ], - [ - -53, - -76 - ], - [ - -69, - -78 - ], - [ - 0, - -56 - ], - [ - -35, - -29 - ], - [ - -63, - -43 - ], - [ - -32, - -6 - ], - [ - -21, - -92 - ], - [ - 14, - -156 - ], - [ - 4, - -99 - ], - [ - -29, - -114 - ], - [ - -1, - -204 - ], - [ - -36, - -6 - ], - [ - -32, - -92 - ], - [ - 22, - -39 - ], - [ - -64, - -34 - ], - [ - -23, - -82 - ], - [ - -28, - -34 - ], - [ - -66, - 112 - ], - [ - -33, - 168 - ], - [ - -26, - 121 - ], - [ - -25, - 57 - ], - [ - -37, - 115 - ], - [ - -17, - 150 - ], - [ - -12, - 75 - ], - [ - -64, - 165 - ], - [ - -28, - 232 - ], - [ - -21, - 154 - ], - [ - 0, - 145 - ], - [ - -14, - 112 - ], - [ - -101, - -72 - ], - [ - -49, - 15 - ], - [ - -91, - 145 - ], - [ - 33, - 44 - ], - [ - -20, - 47 - ], - [ - -82, - 101 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - 46, - 81 - ], - [ - 154, - -1 - ], - [ - -14, - 103 - ], - [ - -39, - 61 - ], - [ - -8, - 92 - ], - [ - -46, - 54 - ], - [ - 77, - 126 - ], - [ - 81, - -9 - ], - [ - 73, - 126 - ], - [ - 44, - 121 - ], - [ - 67, - 121 - ], - [ - -1, - 85 - ], - [ - 60, - 70 - ], - [ - -57, - 59 - ], - [ - -24, - 81 - ], - [ - -25, - 105 - ], - [ - 35, - 52 - ], - [ - 105, - -29 - ], - [ - 78, - 18 - ], - [ - 67, - 100 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -45, - -54 - ], - [ - -27, - -112 - ], - [ - 68, - -46 - ], - [ - 66, - -59 - ], - [ - 91, - -67 - ], - [ - 95, - -15 - ], - [ - 40, - -61 - ], - [ - 54, - -12 - ], - [ - 84, - -28 - ], - [ - 58, - 2 - ], - [ - 8, - 48 - ], - [ - -9, - 76 - ], - [ - 5, - 52 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - 5, - -46 - ], - [ - -24, - -22 - ], - [ - 6, - -74 - ], - [ - -50, - 22 - ], - [ - -91, - -83 - ], - [ - 3, - -68 - ], - [ - -39, - -101 - ], - [ - -3, - -59 - ], - [ - -31, - -98 - ], - [ - -55, - 27 - ], - [ - -3, - -124 - ], - [ - -15, - -41 - ], - [ - 7, - -51 - ], - [ - -34, - -29 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - 12, - -86 - ], - [ - -53, - -107 - ], - [ - -123, - -71 - ], - [ - -99, - 18 - ], - [ - 57, - 125 - ], - [ - -37, - 122 - ], - [ - 95, - 94 - ], - [ - 53, - 56 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 34, - -63 - ], - [ - 29, - -73 - ], - [ - 66, - -53 - ], - [ - 2, - -105 - ], - [ - 33, - -20 - ], - [ - 6, - -55 - ], - [ - -100, - -62 - ], - [ - -27, - -139 - ] - ], - [ - [ - 16834, - 13806 - ], - [ - -131, - 36 - ], - [ - -76, - 28 - ], - [ - -78, - 15 - ], - [ - -30, - 147 - ], - [ - -34, - 22 - ], - [ - -53, - -22 - ], - [ - -70, - -58 - ], - [ - -86, - 40 - ], - [ - -70, - 92 - ], - [ - -67, - 34 - ], - [ - -47, - 114 - ], - [ - -51, - 160 - ], - [ - -38, - -19 - ], - [ - -44, - 39 - ], - [ - -26, - -47 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -38, - 64 - ], - [ - -1, - 63 - ], - [ - -22, - 0 - ], - [ - 11, - 87 - ], - [ - -36, - 91 - ], - [ - -85, - 66 - ], - [ - -49, - 114 - ], - [ - 17, - 94 - ], - [ - 35, - 41 - ], - [ - -6, - 70 - ], - [ - -45, - 36 - ], - [ - -45, - 143 - ] - ], - [ - [ - 15669, - 15256 - ], - [ - -39, - 97 - ], - [ - 14, - 37 - ], - [ - -22, - 137 - ], - [ - 48, - 35 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - 22, - -88 - ], - [ - 66, - -25 - ], - [ - 49, - -60 - ], - [ - 99, - -21 - ], - [ - 109, - 32 - ], - [ - 6, - 28 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - 62, - 23 - ], - [ - 49, - 69 - ], - [ - 47, - -4 - ], - [ - 30, - 23 - ], - [ - 50, - -11 - ], - [ - 77, - -61 - ], - [ - 56, - -13 - ], - [ - 79, - -107 - ], - [ - 52, - -4 - ], - [ - 6, - -101 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -41, - 6 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - -47, - 10 - ], - [ - -51, - -115 - ] - ], - [ - [ - 15794, - 14288 - ], - [ - -130, - 10 - ], - [ - -196, - 241 - ], - [ - -104, - 84 - ], - [ - -84, - 33 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -28, - 146 - ] - ], - [ - [ - 15252, - 14802 - ], - [ - 154, - 124 - ], - [ - 26, - 145 - ], - [ - -6, - 88 - ], - [ - 38, - 30 - ], - [ - 36, - 75 - ] - ], - [ - [ - 15500, - 15264 - ], - [ - 30, - 18 - ], - [ - 81, - -15 - ], - [ - 24, - -31 - ], - [ - 34, - 20 - ] - ], - [ - [ - 11536, - 18770 - ], - [ - -16, - -78 - ], - [ - 79, - -82 - ], - [ - -91, - -91 - ], - [ - -201, - -82 - ], - [ - -60, - -22 - ], - [ - -92, - 17 - ], - [ - -194, - 38 - ], - [ - 68, - 53 - ], - [ - -151, - 59 - ], - [ - 123, - 23 - ], - [ - -3, - 36 - ], - [ - -146, - 27 - ], - [ - 47, - 79 - ], - [ - 106, - 17 - ], - [ - 108, - -81 - ], - [ - 106, - 65 - ], - [ - 88, - -34 - ], - [ - 113, - 64 - ], - [ - 116, - -8 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 20, - 39 - ], - [ - -4, - 7 - ], - [ - 18, - 56 - ], - [ - 14, - 90 - ], - [ - 10, - 31 - ], - [ - 2, - 1 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 23, - 0 - ], - [ - 7, - 21 - ], - [ - 19, - 1 - ] - ], - [ - [ - 15045, - 14789 - ], - [ - 1, - -49 - ], - [ - -10, - -18 - ], - [ - 1, - -1 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - -12, - -38 - ] - ], - [ - [ - 15025, - 14683 - ], - [ - -25, - 17 - ], - [ - -14, - -80 - ], - [ - 17, - -13 - ], - [ - -18, - -17 - ], - [ - -3, - -31 - ], - [ - 33, - 16 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 2, - -47 - ], - [ - -35, - -192 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -8, - -59 - ], - [ - 17, - -51 - ] - ], - [ - [ - 13519, - 16267 - ], - [ - -55, - 17 - ], - [ - -57, - -42 - ], - [ - 4, - -60 - ], - [ - -9, - -34 - ], - [ - 23, - -61 - ], - [ - 65, - -61 - ], - [ - 35, - -99 - ], - [ - 78, - -96 - ], - [ - 55, - 0 - ], - [ - 17, - -26 - ], - [ - -20, - -24 - ], - [ - 63, - -44 - ], - [ - 51, - -36 - ], - [ - 60, - -62 - ], - [ - 7, - -23 - ], - [ - -13, - -43 - ], - [ - -39, - 56 - ], - [ - -61, - 20 - ], - [ - -29, - -78 - ], - [ - 50, - -44 - ], - [ - -8, - -63 - ], - [ - -29, - -7 - ], - [ - -37, - -103 - ], - [ - -29, - -9 - ], - [ - 0, - 37 - ], - [ - 14, - 64 - ], - [ - 15, - 26 - ], - [ - -27, - 69 - ], - [ - -21, - 61 - ], - [ - -29, - 15 - ], - [ - -21, - 51 - ], - [ - -44, - 22 - ], - [ - -31, - 49 - ], - [ - -51, - 7 - ], - [ - -55, - 54 - ], - [ - -63, - 79 - ], - [ - -48, - 69 - ], - [ - -21, - 118 - ], - [ - -35, - 14 - ], - [ - -57, - 40 - ], - [ - -32, - -16 - ], - [ - -40, - -56 - ], - [ - -29, - -9 - ] - ], - [ - [ - 13629, - 15384 - ], - [ - -25, - -95 - ], - [ - 11, - -37 - ], - [ - -15, - -62 - ], - [ - -53, - 46 - ], - [ - -36, - 13 - ], - [ - -97, - 61 - ], - [ - 10, - 61 - ], - [ - 81, - -11 - ], - [ - 71, - 13 - ], - [ - 53, - 11 - ] - ], - [ - [ - 13190, - 15741 - ], - [ - 41, - -85 - ], - [ - -9, - -159 - ], - [ - -32, - 8 - ], - [ - -29, - -40 - ], - [ - -26, - 32 - ], - [ - -3, - 144 - ], - [ - -16, - 69 - ], - [ - 39, - -6 - ], - [ - 35, - 37 - ] - ], - [ - [ - 7140, - 13015 - ], - [ - 47, - -10 - ], - [ - 37, - -29 - ], - [ - 12, - -33 - ], - [ - -49, - -2 - ], - [ - -21, - -20 - ], - [ - -39, - 19 - ], - [ - -40, - 44 - ], - [ - 8, - 27 - ], - [ - 29, - 9 - ], - [ - 16, - -5 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -14, - -19 - ], - [ - -139, - -60 - ], - [ - 69, - -120 - ], - [ - -23, - -20 - ], - [ - -11, - -40 - ], - [ - -53, - -17 - ], - [ - -17, - -43 - ], - [ - -30, - -37 - ], - [ - -78, - 19 - ] - ], - [ - [ - 14984, - 14319 - ], - [ - -2, - 17 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 10, - 35 - ], - [ - 0, - 73 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - 78, - -47 - ], - [ - 137, - 128 - ] - ], - [ - [ - 21933, - 14894 - ], - [ - 9, - -41 - ], - [ - -39, - -73 - ], - [ - -29, - 39 - ], - [ - -36, - -28 - ], - [ - -18, - -70 - ], - [ - -46, - 34 - ], - [ - 1, - 57 - ], - [ - 38, - 71 - ], - [ - 40, - -14 - ], - [ - 29, - 51 - ], - [ - 51, - -26 - ] - ], - [ - [ - 22375, - 15253 - ], - [ - -27, - -96 - ], - [ - 13, - -60 - ], - [ - -37, - -84 - ], - [ - -89, - -57 - ], - [ - -122, - -7 - ], - [ - -100, - -137 - ], - [ - -46, - 46 - ], - [ - -3, - 90 - ], - [ - -122, - -27 - ], - [ - -82, - -56 - ], - [ - -82, - -3 - ], - [ - 71, - -88 - ], - [ - -47, - -204 - ], - [ - -45, - -50 - ], - [ - -33, - 46 - ], - [ - 17, - 109 - ], - [ - -44, - 34 - ], - [ - -29, - 83 - ], - [ - 66, - 37 - ], - [ - 37, - 75 - ], - [ - 70, - 62 - ], - [ - 51, - 82 - ], - [ - 139, - 36 - ], - [ - 74, - -25 - ], - [ - 73, - 214 - ], - [ - 47, - -58 - ], - [ - 102, - 120 - ], - [ - 40, - 47 - ], - [ - 43, - 147 - ], - [ - -11, - 135 - ], - [ - 29, - 75 - ], - [ - 74, - 22 - ], - [ - 38, - -166 - ], - [ - -2, - -97 - ], - [ - -64, - -121 - ], - [ - 1, - -124 - ] - ], - [ - [ - 22579, - 16097 - ], - [ - 49, - -26 - ], - [ - 50, - 51 - ], - [ - 15, - -135 - ], - [ - -103, - -33 - ], - [ - -61, - -119 - ], - [ - -110, - 82 - ], - [ - -38, - -131 - ], - [ - -77, - -2 - ], - [ - -10, - 120 - ], - [ - 34, - 92 - ], - [ - 75, - 6 - ], - [ - 20, - 166 - ], - [ - 21, - 94 - ], - [ - 82, - -125 - ], - [ - 53, - -40 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -43, - 17 - ], - [ - -35, - 44 - ], - [ - -103, - 12 - ], - [ - -116, - 3 - ], - [ - -25, - -13 - ], - [ - -99, - 51 - ], - [ - -40, - -25 - ], - [ - -11, - -71 - ], - [ - -114, - 41 - ], - [ - -46, - -17 - ], - [ - -16, - -52 - ] - ], - [ - [ - 17494, - 15868 - ], - [ - -40, - -22 - ], - [ - -92, - -84 - ], - [ - -30, - -86 - ], - [ - -26, - -1 - ], - [ - -19, - 57 - ], - [ - -89, - 4 - ], - [ - -14, - 98 - ], - [ - -34, - 1 - ], - [ - 5, - 121 - ], - [ - -83, - 87 - ], - [ - -120, - -9 - ], - [ - -82, - -18 - ], - [ - -66, - 109 - ], - [ - -57, - 45 - ], - [ - -108, - 86 - ], - [ - -13, - 10 - ], - [ - -180, - -71 - ], - [ - 3, - -442 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - -36, - -6 - ], - [ - -49, - 94 - ], - [ - -47, - 34 - ], - [ - -79, - -25 - ], - [ - -31, - -40 - ] - ], - [ - [ - 16207, - 15810 - ], - [ - -4, - 29 - ], - [ - 18, - 50 - ], - [ - -14, - 42 - ], - [ - -81, - 41 - ], - [ - -31, - 108 - ], - [ - -38, - 30 - ], - [ - -3, - 39 - ], - [ - 68, - -11 - ], - [ - 3, - 87 - ], - [ - 59, - 20 - ], - [ - 61, - -18 - ], - [ - 12, - 117 - ], - [ - -12, - 74 - ], - [ - -70, - -6 - ], - [ - -59, - 30 - ], - [ - -81, - -53 - ], - [ - -65, - -25 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -35, - 19 - ], - [ - 7, - 62 - ], - [ - -45, - 80 - ], - [ - -51, - -3 - ], - [ - -59, - 81 - ], - [ - 40, - 91 - ], - [ - -21, - 24 - ], - [ - 56, - 132 - ], - [ - 72, - -69 - ], - [ - 8, - 87 - ], - [ - 144, - 131 - ], - [ - 109, - 3 - ], - [ - 154, - -83 - ], - [ - 82, - -49 - ], - [ - 74, - 51 - ], - [ - 111, - 2 - ], - [ - 89, - -62 - ], - [ - 20, - 36 - ], - [ - 98, - -6 - ], - [ - 18, - 57 - ], - [ - -113, - 83 - ], - [ - 67, - 58 - ], - [ - -13, - 33 - ], - [ - 67, - 31 - ], - [ - -51, - 82 - ], - [ - 32, - 41 - ], - [ - 261, - 42 - ], - [ - 34, - 30 - ], - [ - 174, - 44 - ], - [ - 63, - 50 - ], - [ - 125, - -26 - ], - [ - 22, - -125 - ], - [ - 73, - 30 - ], - [ - 90, - -41 - ], - [ - -6, - -66 - ], - [ - 67, - 7 - ], - [ - 174, - 113 - ], - [ - -25, - -37 - ], - [ - 89, - -93 - ], - [ - 156, - -305 - ], - [ - 37, - 63 - ], - [ - 96, - -69 - ], - [ - 100, - 31 - ], - [ - 38, - -22 - ], - [ - 34, - -69 - ], - [ - 49, - -23 - ], - [ - 29, - -51 - ], - [ - 90, - 16 - ], - [ - 37, - -74 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -61, - -136 - ], - [ - 1, - -437 - ], - [ - 41, - -99 - ] - ], - [ - [ - 15446, - 10595 - ], - [ - -48, - -48 - ], - [ - -18, - -50 - ], - [ - -26, - -8 - ], - [ - -10, - -85 - ], - [ - -22, - -48 - ], - [ - -14, - -80 - ], - [ - -28, - -40 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -100, - 120 - ], - [ - -5, - 70 - ], - [ - -252, - 244 - ], - [ - -12, - 13 - ] - ], - [ - [ - 14911, - 10683 - ], - [ - -1, - 127 - ], - [ - 20, - 49 - ], - [ - 34, - 79 - ], - [ - 26, - 88 - ], - [ - -31, - 138 - ], - [ - -8, - 60 - ], - [ - -33, - 83 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - 43, - 72 - ], - [ - 47, - 79 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - -132, - -18 - ], - [ - -86, - 38 - ], - [ - -75, - -9 - ], - [ - 6, - 69 - ], - [ - 76, - -20 - ], - [ - 26, - 37 - ] - ], - [ - [ - 17498, - 15625 - ], - [ - 53, - -12 - ], - [ - 89, - 87 - ], - [ - -83, - 63 - ], - [ - -49, - -30 - ], - [ - -52, - 45 - ], - [ - 59, - 78 - ], - [ - -21, - 12 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -17, - 145 - ], - [ - 45, - 100 - ], - [ - 90, - 23 - ], - [ - 65, - -17 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 58, - -48 - ], - [ - 31, - 83 - ], - [ - 62, - -44 - ] - ], - [ - [ - 20033, - 12501 - ], - [ - 16, - -80 - ], - [ - -8, - -144 - ], - [ - -118, - -92 - ], - [ - 31, - -73 - ], - [ - -73, - -8 - ], - [ - -61, - -49 - ] - ], - [ - [ - 19820, - 12055 - ], - [ - -58, - 18 - ], - [ - -28, - 62 - ], - [ - -35, - 124 - ] - ], - [ - [ - 21495, - 15429 - ], - [ - 60, - -141 - ], - [ - 17, - -78 - ], - [ - 1, - -138 - ], - [ - -27, - -66 - ], - [ - -63, - -23 - ], - [ - -56, - -50 - ], - [ - -62, - -10 - ], - [ - -8, - 65 - ], - [ - 13, - 90 - ], - [ - -31, - 125 - ], - [ - 52, - 20 - ], - [ - -48, - 103 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - 4, - 11 - ], - [ - 31, - -4 - ], - [ - 28, - 54 - ], - [ - 49, - 6 - ], - [ - 30, - 7 - ], - [ - 10, - 29 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 13, - 26 - ] - ], - [ - [ - 13960, - 15933 - ], - [ - 16, - 9 - ], - [ - 10, - 40 - ], - [ - 12, - 6 - ], - [ - 10, - -16 - ], - [ - 13, - -8 - ], - [ - 9, - -19 - ], - [ - 12, - -6 - ], - [ - 14, - -22 - ], - [ - 9, - 1 - ], - [ - -7, - -29 - ], - [ - -9, - -15 - ], - [ - 3, - -9 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - -16, - -4 - ], - [ - -41, - -19 - ], - [ - -3, - -24 - ], - [ - -9, - 1 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - 14, - -53 - ], - [ - -6, - -27 - ], - [ - 23, - -90 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - -50, - -4 - ], - [ - -17, - 58 - ], - [ - -62, - 11 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - 40, - -94 - ], - [ - 32, - -109 - ], - [ - 85, - -1 - ], - [ - 28, - -105 - ], - [ - -45, - -31 - ], - [ - -20, - -44 - ], - [ - 83, - -71 - ], - [ - 58, - -142 - ], - [ - 44, - -106 - ], - [ - 53, - -83 - ], - [ - 18, - -85 - ], - [ - -13, - -120 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 23, - 54 - ], - [ - 3, - 101 - ], - [ - -57, - 105 - ], - [ - -4, - 118 - ], - [ - -53, - 98 - ], - [ - -53, - 8 - ], - [ - -14, - -42 - ], - [ - -40, - -3 - ], - [ - -21, - 21 - ], - [ - -74, - -72 - ], - [ - -1, - 108 - ], - [ - 17, - 126 - ], - [ - -47, - 6 - ], - [ - -4, - 72 - ], - [ - -31, - 37 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - 15, - 44 - ], - [ - 60, - 78 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 25, - 98 - ], - [ - 35, - 84 - ], - [ - 1, - 5 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - 31, - -7 - ], - [ - 12, - -47 - ], - [ - -38, - -45 - ], - [ - -17, - -66 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - -18, - -1 - ], - [ - -72, - 57 - ], - [ - -64, - 91 - ], - [ - -59, - 66 - ], - [ - -47, - 77 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - 17, - 39 - ], - [ - 3, - 35 - ], - [ - 32, - 65 - ], - [ - 32, - 56 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 34, - 28 - ], - [ - 7, - 51 - ], - [ - -8, - 49 - ], - [ - 48, - 47 - ], - [ - 21, - 38 - ], - [ - 34, - 34 - ], - [ - 4, - 93 - ] - ], - [ - [ - 13348, - 14773 - ], - [ - 82, - -42 - ], - [ - 30, - 11 - ], - [ - 58, - -20 - ], - [ - 92, - -54 - ], - [ - 33, - -107 - ], - [ - 62, - -23 - ], - [ - 99, - -50 - ], - [ - 74, - -60 - ], - [ - 34, - 31 - ], - [ - 33, - 56 - ], - [ - -16, - 91 - ], - [ - 22, - 59 - ], - [ - 50, - 56 - ], - [ - 48, - 16 - ], - [ - 95, - -24 - ], - [ - 23, - -54 - ], - [ - 26, - 0 - ], - [ - 22, - -21 - ], - [ - 70, - -14 - ], - [ - 17, - -39 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - -240 - ], - [ - -80, - 0 - ], - [ - -1, - -51 - ] - ], - [ - [ - 14209, - 13146 - ], - [ - -278, - 230 - ], - [ - -278, - 230 - ], - [ - -70, - -66 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - -50, - -45 - ], - [ - -39, - 66 - ], - [ - -110, - 52 - ] - ], - [ - [ - 18249, - 11700 - ], - [ - -11, - -125 - ], - [ - -29, - -34 - ], - [ - -61, - -28 - ], - [ - -33, - 96 - ], - [ - -12, - 172 - ], - [ - 31, - 195 - ], - [ - 49, - -67 - ], - [ - 32, - -84 - ], - [ - 34, - -125 - ] - ], - [ - [ - 14568, - 7323 - ], - [ - 24, - -36 - ], - [ - -22, - -58 - ], - [ - -12, - -39 - ], - [ - -38, - -19 - ], - [ - -13, - -38 - ], - [ - -25, - -12 - ], - [ - -52, - 92 - ], - [ - 37, - 76 - ], - [ - 38, - 47 - ], - [ - 32, - 24 - ], - [ - 31, - -37 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - -17, - 37 - ], - [ - -36, - 13 - ] - ], - [ - [ - 14132, - 17315 - ], - [ - -6, - 30 - ], - [ - 8, - 33 - ], - [ - -31, - 19 - ], - [ - -73, - 21 - ] - ], - [ - [ - 14030, - 17418 - ], - [ - -15, - 101 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 80, - 37 - ], - [ - 117, - -8 - ], - [ - 68, - 12 - ], - [ - 10, - -25 - ], - [ - 37, - -8 - ], - [ - 67, - -58 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 3, - 90 - ], - [ - 34, - 76 - ], - [ - 66, - 41 - ], - [ - 55, - -90 - ], - [ - 56, - 2 - ], - [ - 13, - 93 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - 33, - -27 - ], - [ - 6, - -58 - ], - [ - 23, - -71 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - -10, - 0 - ], - [ - 1, - -64 - ], - [ - -43, - -4 - ], - [ - -22, - -27 - ], - [ - -32, - 0 - ], - [ - -25, - 15 - ], - [ - -59, - -13 - ], - [ - -22, - -93 - ], - [ - -22, - -9 - ], - [ - -33, - -151 - ], - [ - -97, - -130 - ], - [ - -23, - -165 - ], - [ - -28, - -54 - ], - [ - -9, - -43 - ], - [ - -157, - -10 - ], - [ - -1, - 0 - ] - ], - [ - [ - 11361, - 13367 - ], - [ - 3, - 56 - ], - [ - 27, - 32 - ], - [ - 23, - 63 - ], - [ - -5, - 41 - ], - [ - 24, - 84 - ], - [ - 39, - 77 - ], - [ - 24, - 19 - ], - [ - 18, - 70 - ], - [ - 2, - 64 - ], - [ - 25, - 74 - ], - [ - 46, - 44 - ], - [ - 45, - 122 - ], - [ - 1, - 2 - ], - [ - 35, - 46 - ], - [ - 65, - 13 - ], - [ - 55, - 82 - ], - [ - 35, - 32 - ], - [ - 58, - 100 - ], - [ - -18, - 150 - ], - [ - 27, - 103 - ], - [ - 9, - 63 - ], - [ - 45, - 81 - ], - [ - 70, - 55 - ], - [ - 52, - 49 - ], - [ - 46, - 125 - ], - [ - 22, - 73 - ], - [ - 51, - 0 - ], - [ - 42, - -51 - ], - [ - 67, - 8 - ], - [ - 72, - -26 - ], - [ - 30, - -2 - ] - ], - [ - [ - 14403, - 16582 - ], - [ - 17, - 18 - ], - [ - 46, - 12 - ], - [ - 51, - -38 - ], - [ - 29, - -4 - ], - [ - 32, - -32 - ], - [ - -5, - -41 - ], - [ - 25, - -20 - ], - [ - 10, - -50 - ], - [ - 24, - -30 - ], - [ - -5, - -18 - ], - [ - 13, - -12 - ], - [ - -18, - -9 - ], - [ - -41, - 3 - ], - [ - -7, - 17 - ], - [ - -15, - -10 - ], - [ - 5, - -21 - ], - [ - -19, - -38 - ], - [ - -12, - -42 - ], - [ - -17, - -13 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - -13, - 55 - ], - [ - 7, - 51 - ], - [ - -2, - 53 - ], - [ - -40, - 71 - ], - [ - -22, - 51 - ], - [ - -22, - 35 - ], - [ - -21, - 12 - ] - ], - [ - [ - 16001, - 9301 - ], - [ - 19, - -51 - ], - [ - 17, - -79 - ], - [ - 11, - -144 - ], - [ - 18, - -57 - ], - [ - -7, - -57 - ], - [ - -12, - -35 - ], - [ - -24, - 70 - ], - [ - -13, - -36 - ], - [ - 13, - -88 - ], - [ - -6, - -51 - ], - [ - -19, - -28 - ], - [ - -4, - -102 - ], - [ - -28, - -139 - ], - [ - -34, - -166 - ], - [ - -43, - -227 - ], - [ - -27, - -167 - ], - [ - -32, - -139 - ], - [ - -56, - -28 - ], - [ - -61, - -51 - ], - [ - -40, - 30 - ], - [ - -56, - 43 - ], - [ - -19, - 64 - ], - [ - -4, - 106 - ], - [ - -25, - 96 - ], - [ - -6, - 86 - ], - [ - 12, - 86 - ], - [ - 32, - 21 - ], - [ - 0, - 40 - ], - [ - 34, - 91 - ], - [ - 6, - 77 - ], - [ - -16, - 56 - ], - [ - -13, - 76 - ], - [ - -6, - 111 - ], - [ - 24, - 67 - ], - [ - 10, - 76 - ], - [ - 35, - 4 - ], - [ - 38, - 25 - ], - [ - 26, - 21 - ], - [ - 31, - 2 - ], - [ - 40, - 68 - ], - [ - 57, - 74 - ], - [ - 21, - 61 - ], - [ - -10, - 51 - ], - [ - 30, - -14 - ], - [ - 38, - 83 - ], - [ - 2, - 72 - ], - [ - 23, - 54 - ], - [ - 24, - -52 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - -78, - 130 - ], - [ - -36, - 39 - ], - [ - -57, - 31 - ], - [ - -39, - -9 - ], - [ - -56, - -45 - ], - [ - -35, - -12 - ], - [ - -50, - 32 - ], - [ - -52, - 23 - ], - [ - -65, - 55 - ], - [ - -52, - 16 - ], - [ - -79, - 56 - ], - [ - -58, - 58 - ], - [ - -18, - 32 - ], - [ - -39, - 7 - ], - [ - -71, - 38 - ], - [ - -29, - 54 - ], - [ - -75, - 69 - ], - [ - -35, - 75 - ], - [ - -17, - 59 - ], - [ - 23, - 11 - ], - [ - -7, - 35 - ], - [ - 16, - 31 - ], - [ - 1, - 41 - ], - [ - -24, - 54 - ], - [ - -6, - 48 - ], - [ - -24, - 60 - ], - [ - -61, - 120 - ], - [ - -70, - 93 - ], - [ - -34, - 75 - ], - [ - -60, - 49 - ], - [ - -13, - 29 - ], - [ - 11, - 75 - ], - [ - -36, - 28 - ], - [ - -41, - 58 - ], - [ - -17, - 84 - ], - [ - -38, - 9 - ], - [ - -40, - 63 - ], - [ - -33, - 59 - ], - [ - -3, - 37 - ], - [ - -37, - 91 - ], - [ - -25, - 92 - ], - [ - 1, - 46 - ], - [ - -50, - 47 - ], - [ - -24, - -5 - ], - [ - -39, - 33 - ], - [ - -12, - -49 - ], - [ - 12, - -57 - ], - [ - 7, - -90 - ], - [ - 24, - -50 - ], - [ - 51, - -82 - ], - [ - 12, - -29 - ], - [ - 10, - -8 - ], - [ - 10, - -41 - ], - [ - 12, - 1 - ], - [ - 14, - -77 - ], - [ - 21, - -31 - ], - [ - 15, - -42 - ], - [ - 44, - -61 - ], - [ - 23, - -112 - ], - [ - 21, - -52 - ], - [ - 19, - -56 - ], - [ - 4, - -64 - ], - [ - 34, - -4 - ], - [ - 27, - -54 - ], - [ - 26, - -54 - ], - [ - -2, - -21 - ], - [ - -29, - -44 - ], - [ - -13, - 0 - ], - [ - -18, - 73 - ], - [ - -46, - 69 - ], - [ - -50, - 58 - ], - [ - -36, - 30 - ], - [ - 3, - 88 - ], - [ - -11, - 65 - ], - [ - -33, - 37 - ], - [ - -48, - 54 - ], - [ - -9, - -16 - ], - [ - -18, - 31 - ], - [ - -43, - 29 - ], - [ - -41, - 70 - ], - [ - 5, - 9 - ], - [ - 29, - -7 - ], - [ - 26, - 45 - ], - [ - 2, - 54 - ], - [ - -53, - 86 - ], - [ - -41, - 33 - ], - [ - -26, - 75 - ], - [ - -26, - 79 - ], - [ - -32, - 95 - ], - [ - -28, - 108 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - 79, - 10 - ], - [ - 88, - 13 - ], - [ - -6, - -24 - ], - [ - 105, - -58 - ], - [ - 159, - -85 - ], - [ - 139, - 1 - ], - [ - 55, - 0 - ], - [ - 0, - 50 - ], - [ - 121, - 0 - ], - [ - 25, - -43 - ], - [ - 36, - -38 - ], - [ - 42, - -52 - ], - [ - 23, - -63 - ], - [ - 17, - -66 - ], - [ - 36, - -36 - ], - [ - 58, - -36 - ], - [ - 44, - 94 - ], - [ - 57, - 3 - ], - [ - 49, - -48 - ], - [ - 35, - -82 - ], - [ - 24, - -70 - ], - [ - 41, - -69 - ], - [ - 15, - -84 - ], - [ - 20, - -56 - ], - [ - 54, - -37 - ], - [ - 50, - -27 - ], - [ - 27, - 4 - ] - ], - [ - [ - 5776, - 13901 - ], - [ - -27, - -106 - ], - [ - -12, - -86 - ], - [ - -5, - -161 - ], - [ - -7, - -58 - ], - [ - 12, - -66 - ], - [ - 22, - -58 - ], - [ - 14, - -93 - ], - [ - 46, - -90 - ], - [ - 16, - -68 - ], - [ - 27, - -59 - ], - [ - 74, - -32 - ], - [ - 29, - -50 - ], - [ - 61, - 33 - ], - [ - 54, - 13 - ], - [ - 52, - 21 - ], - [ - 44, - 21 - ], - [ - 44, - 49 - ], - [ - 17, - 70 - ], - [ - 5, - 100 - ], - [ - 12, - 36 - ], - [ - 48, - 31 - ], - [ - 73, - 28 - ], - [ - 62, - -4 - ], - [ - 42, - 10 - ], - [ - 17, - -26 - ], - [ - -2, - -57 - ], - [ - -38, - -72 - ], - [ - -16, - -73 - ], - [ - 12, - -21 - ], - [ - -10, - -52 - ], - [ - -17, - -93 - ], - [ - -18, - 31 - ], - [ - -15, - -2 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - 23, - 7 - ], - [ - 33, - 2 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 3, - 37 - ], - [ - -6, - 47 - ], - [ - -26, - 33 - ], - [ - -14, - 69 - ], - [ - -3, - 75 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - 24, - 22 - ], - [ - 11, - 70 - ], - [ - 22, - 3 - ], - [ - 49, - -33 - ], - [ - 39, - 23 - ], - [ - 27, - -8 - ], - [ - 11, - 27 - ], - [ - 279, - 2 - ], - [ - 16, - 84 - ], - [ - -12, - 15 - ], - [ - -34, - 517 - ], - [ - -33, - 518 - ], - [ - 106, - 2 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - 0, - -276 - ], - [ - -38, - -80 - ], - [ - -6, - -74 - ], - [ - -62, - -19 - ], - [ - -95, - -10 - ], - [ - -26, - -43 - ], - [ - -44, - -5 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - -40, - -28 - ], - [ - -40, - -52 - ], - [ - -49, - -5 - ], - [ - -32, - -130 - ], - [ - -30, - -22 - ], - [ - 34, - -105 - ], - [ - 44, - -88 - ], - [ - 29, - -79 - ], - [ - -26, - -104 - ], - [ - -24, - -22 - ], - [ - 17, - -61 - ], - [ - 46, - -95 - ], - [ - 8, - -67 - ], - [ - -1, - -56 - ], - [ - 28, - -109 - ], - [ - -39, - -112 - ], - [ - -33, - -123 - ] - ], - [ - [ - 19418, - 11989 - ], - [ - -7, - 89 - ], - [ - 21, - 92 - ], - [ - -23, - 71 - ], - [ - 5, - 130 - ], - [ - -28, - 63 - ], - [ - -23, - 143 - ], - [ - -12, - 152 - ], - [ - -30, - 99 - ], - [ - -46, - -60 - ], - [ - -79, - -86 - ], - [ - -40, - 11 - ], - [ - -43, - 28 - ], - [ - 24, - 149 - ], - [ - -14, - 112 - ], - [ - -55, - 139 - ], - [ - 9, - 43 - ], - [ - -41, - 15 - ], - [ - -50, - 98 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -15, - 9 - ], - [ - -19, - 40 - ], - [ - -30, - 23 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - 19, - -21 - ], - [ - 10, - -17 - ], - [ - 23, - -12 - ], - [ - 26, - -25 - ], - [ - -5, - -11 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 74, - 21 - ], - [ - 133, - 103 - ], - [ - 106, - 57 - ], - [ - 61, - -37 - ], - [ - 72, - -2 - ], - [ - 47, - -56 - ], - [ - 70, - -4 - ], - [ - 100, - -30 - ], - [ - 68, - 83 - ], - [ - -28, - 71 - ], - [ - 72, - 124 - ], - [ - 78, - -49 - ], - [ - 63, - -14 - ], - [ - 82, - -31 - ], - [ - 14, - -90 - ], - [ - 99, - -51 - ], - [ - 65, - 23 - ], - [ - 89, - 15 - ], - [ - 70, - -15 - ], - [ - 68, - -58 - ], - [ - 42, - -61 - ], - [ - 65, - 1 - ], - [ - 88, - -20 - ], - [ - 64, - 30 - ], - [ - 91, - 20 - ], - [ - 103, - 84 - ], - [ - 41, - -13 - ], - [ - 37, - -40 - ], - [ - 83, - 10 - ] - ], - [ - [ - 14957, - 9415 - ], - [ - 52, - 10 - ], - [ - 84, - -34 - ], - [ - 18, - 15 - ], - [ - 49, - 3 - ], - [ - 24, - 36 - ], - [ - 42, - -2 - ], - [ - 76, - 47 - ], - [ - 56, - 69 - ] - ], - [ - [ - 15358, - 9559 - ], - [ - 11, - -53 - ], - [ - -3, - -120 - ], - [ - 9, - -105 - ], - [ - 3, - -188 - ], - [ - 12, - -58 - ], - [ - -21, - -86 - ], - [ - -27, - -83 - ], - [ - -44, - -75 - ], - [ - -64, - -45 - ], - [ - -79, - -59 - ], - [ - -78, - -128 - ], - [ - -27, - -22 - ], - [ - -49, - -86 - ], - [ - -29, - -27 - ], - [ - -5, - -86 - ], - [ - 33, - -91 - ], - [ - 13, - -70 - ], - [ - 1, - -36 - ], - [ - 13, - 6 - ], - [ - -2, - -118 - ], - [ - -12, - -55 - ], - [ - 17, - -21 - ], - [ - -11, - -50 - ], - [ - -29, - -42 - ], - [ - -57, - -41 - ], - [ - -84, - -65 - ], - [ - -31, - -44 - ], - [ - 6, - -51 - ], - [ - 18, - -8 - ], - [ - -6, - -63 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -53, - 1 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -6, - 53 - ], - [ - -10, - 54 - ] - ], - [ - [ - 14767, - 7697 - ], - [ - -6, - 43 - ], - [ - 12, - 134 - ], - [ - -18, - 85 - ], - [ - -33, - 169 - ] - ], - [ - [ - 14722, - 8128 - ], - [ - 73, - 136 - ], - [ - 19, - 86 - ], - [ - 10, - 11 - ], - [ - 8, - 71 - ], - [ - -11, - 35 - ], - [ - 3, - 90 - ], - [ - 13, - 83 - ], - [ - 0, - 152 - ], - [ - -36, - 39 - ], - [ - -33, - 8 - ], - [ - -15, - 30 - ], - [ - -32, - 25 - ], - [ - -59, - -2 - ], - [ - -4, - 45 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -7, - 85 - ], - [ - 212, - 99 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - 40, - -58 - ], - [ - 19, - 11 - ], - [ - 28, - -30 - ], - [ - 4, - -48 - ], - [ - -15, - -56 - ], - [ - 5, - -84 - ], - [ - 46, - -74 - ], - [ - 21, - 83 - ], - [ - 30, - 25 - ], - [ - -6, - 154 - ], - [ - -29, - 87 - ], - [ - -25, - 39 - ], - [ - -24, - -2 - ], - [ - -20, - 156 - ], - [ - 20, - 91 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - -46, - 82 - ], - [ - -42, - 88 - ], - [ - -46, - 32 - ], - [ - -34, - 35 - ], - [ - -39, - -1 - ], - [ - -34, - -26 - ], - [ - -34, - 10 - ], - [ - -24, - -38 - ] - ], - [ - [ - 11400, - 12733 - ], - [ - -6, - 65 - ], - [ - 19, - 59 - ], - [ - 9, - 113 - ], - [ - -8, - 118 - ], - [ - -8, - 60 - ], - [ - 7, - 60 - ], - [ - -18, - 57 - ], - [ - -37, - 52 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 15, - 40 - ], - [ - 273, - -1 - ], - [ - -13, - 173 - ], - [ - 17, - 62 - ], - [ - 65, - 10 - ], - [ - -2, - 307 - ], - [ - 229, - -6 - ], - [ - 0, - 182 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - -37, - 31 - ], - [ - 21, - 112 - ], - [ - 22, - 41 - ], - [ - -13, - 100 - ], - [ - 14, - 97 - ], - [ - 12, - 32 - ], - [ - -18, - 102 - ], - [ - -33, - 54 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - 68, - -23 - ], - [ - 14, - -33 - ], - [ - 24, - -56 - ], - [ - 20, - -163 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - 54, - 83 - ], - [ - 35, - 94 - ], - [ - 28, - 0 - ], - [ - 36, - -60 - ], - [ - 3, - -52 - ], - [ - 46, - -34 - ], - [ - 58, - -36 - ], - [ - -4, - -47 - ], - [ - -47, - -6 - ], - [ - 12, - -59 - ], - [ - -51, - -40 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 51, - -41 - ], - [ - 54, - 22 - ], - [ - 14, - 102 - ], - [ - 30, - 22 - ], - [ - 83, - 26 - ], - [ - 50, - 95 - ], - [ - 34, - 76 - ] - ], - [ - [ - 19668, - 11544 - ], - [ - 16, - -12 - ], - [ - 41, - -72 - ], - [ - 29, - -80 - ], - [ - 4, - -81 - ], - [ - -7, - -55 - ], - [ - 6, - -41 - ], - [ - 5, - -71 - ], - [ - 25, - -33 - ], - [ - 27, - -106 - ], - [ - -1, - -41 - ], - [ - -49, - -8 - ], - [ - -66, - 89 - ], - [ - -83, - 95 - ], - [ - -8, - 62 - ], - [ - -40, - 80 - ], - [ - -10, - 99 - ], - [ - -25, - 66 - ], - [ - 8, - 87 - ], - [ - -16, - 51 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - 12, - 21 - ], - [ - 57, - -52 - ], - [ - 6, - -62 - ], - [ - 46, - 14 - ], - [ - 23, - 50 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - 57, - 27 - ], - [ - 45, - -7 - ], - [ - 28, - -27 - ], - [ - 0, - -10 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - -443 - ], - [ - -62, - -62 - ], - [ - -37, - -8 - ], - [ - -44, - 22 - ], - [ - -31, - 9 - ], - [ - -12, - 51 - ], - [ - -28, - 33 - ], - [ - -33, - -59 - ] - ], - [ - [ - 13687, - 7369 - ], - [ - -52, - 91 - ], - [ - -27, - 87 - ], - [ - -16, - 117 - ], - [ - -17, - 87 - ], - [ - -23, - 185 - ], - [ - -2, - 143 - ], - [ - -9, - 66 - ], - [ - -27, - 49 - ], - [ - -36, - 99 - ], - [ - -36, - 144 - ], - [ - -16, - 75 - ], - [ - -56, - 117 - ], - [ - -5, - 93 - ] - ], - [ - [ - 24104, - 8268 - ], - [ - 57, - -74 - ], - [ - 36, - -55 - ], - [ - -26, - -29 - ], - [ - -39, - 32 - ], - [ - -50, - 54 - ], - [ - -44, - 64 - ], - [ - -47, - 84 - ], - [ - -9, - 41 - ], - [ - 30, - -2 - ], - [ - 39, - -40 - ], - [ - 30, - -41 - ], - [ - 23, - -34 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - 17, - -186 - ], - [ - 26, - -32 - ], - [ - 1, - -38 - ], - [ - 29, - -41 - ], - [ - -15, - -52 - ], - [ - -27, - -243 - ], - [ - -4, - -156 - ], - [ - -89, - -113 - ], - [ - -30, - -158 - ], - [ - 29, - -45 - ], - [ - 0, - -77 - ], - [ - 45, - -3 - ], - [ - -7, - -56 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - -13, - -3 - ], - [ - -47, - 132 - ], - [ - -16, - 4 - ], - [ - -55, - -67 - ], - [ - -54, - 35 - ], - [ - -37, - 7 - ], - [ - -21, - -17 - ], - [ - -40, - 4 - ], - [ - -42, - -51 - ], - [ - -35, - -3 - ], - [ - -84, - 62 - ], - [ - -33, - -29 - ], - [ - -36, - 2 - ], - [ - -26, - 45 - ], - [ - -70, - 45 - ], - [ - -75, - -15 - ], - [ - -18, - -25 - ], - [ - -10, - -69 - ], - [ - -20, - -49 - ], - [ - -5, - -107 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - -72, - -43 - ], - [ - -27, - 6 - ], - [ - -27, - -27 - ], - [ - -55, - 3 - ], - [ - -38, - 75 - ], - [ - -23, - 86 - ], - [ - -49, - 79 - ], - [ - -52, - -1 - ], - [ - -62, - 0 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - -24, - 38 - ], - [ - -33, - 49 - ], - [ - -15, - 40 - ], - [ - -30, - 38 - ], - [ - -35, - 54 - ], - [ - 8, - 19 - ], - [ - 12, - -19 - ], - [ - 5, - 9 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -6, - -11 - ], - [ - -3, - -27 - ], - [ - 7, - -44 - ], - [ - -16, - -41 - ], - [ - -8, - -48 - ], - [ - -2, - -53 - ], - [ - 4, - -31 - ], - [ - 2, - -54 - ], - [ - -11, - -12 - ], - [ - -6, - -51 - ], - [ - 4, - -32 - ], - [ - -14, - -30 - ], - [ - 3, - -33 - ], - [ - 11, - -19 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 36, - 33 - ], - [ - 61, - 177 - ], - [ - 95, - 50 - ], - [ - 58, - -4 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - -44, - -5 - ], - [ - -10, - -79 - ], - [ - -131, - 19 - ], - [ - -19, - -67 - ], - [ - -67, - 1 - ], - [ - -46, - -86 - ], - [ - -69, - -133 - ], - [ - -109, - -168 - ], - [ - 26, - -41 - ], - [ - -24, - -48 - ], - [ - -70, - 2 - ], - [ - -45, - -112 - ], - [ - 4, - -160 - ], - [ - 45, - -60 - ], - [ - -23, - -142 - ], - [ - -58, - -82 - ], - [ - -31, - -69 - ] - ], - [ - [ - 13316, - 17858 - ], - [ - -47, - 74 - ], - [ - -137, - -139 - ], - [ - -93, - -28 - ], - [ - -97, - 61 - ], - [ - -24, - 129 - ], - [ - -23, - 277 - ], - [ - 65, - 77 - ], - [ - 184, - 101 - ], - [ - 137, - 124 - ], - [ - 128, - 167 - ], - [ - 167, - 231 - ], - [ - 117, - 91 - ], - [ - 192, - 150 - ], - [ - 153, - 53 - ], - [ - 114, - -7 - ], - [ - 107, - 100 - ], - [ - 127, - -6 - ], - [ - 125, - 24 - ], - [ - 218, - -88 - ], - [ - -90, - -32 - ], - [ - 77, - -75 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - -119, - -48 - ], - [ - -56, - -11 - ] - ], - [ - [ - 14271, - 20137 - ], - [ - -156, - -49 - ], - [ - -123, - 28 - ], - [ - 48, - 31 - ], - [ - -42, - 38 - ], - [ - 145, - 24 - ], - [ - 27, - -45 - ], - [ - 101, - -27 - ] - ], - [ - [ - 13820, - 20359 - ], - [ - 229, - -90 - ], - [ - -175, - -47 - ], - [ - -39, - -88 - ], - [ - -61, - -23 - ], - [ - -33, - -99 - ], - [ - -84, - -5 - ], - [ - -150, - 73 - ], - [ - 63, - 43 - ], - [ - -104, - 35 - ], - [ - -136, - 101 - ], - [ - -54, - 94 - ], - [ - 190, - 43 - ], - [ - 38, - -42 - ], - [ - 99, - 2 - ], - [ - 27, - 41 - ], - [ - 102, - 4 - ], - [ - 88, - -42 - ] - ], - [ - [ - 14321, - 20444 - ], - [ - 137, - -43 - ], - [ - -103, - -64 - ], - [ - -203, - -14 - ], - [ - -205, - 20 - ], - [ - -12, - 33 - ], - [ - -101, - 2 - ], - [ - -76, - 55 - ], - [ - 215, - 33 - ], - [ - 102, - -28 - ], - [ - 70, - 36 - ], - [ - 176, - -30 - ] - ], - [ - [ - 24608, - 5888 - ], - [ - 16, - -49 - ], - [ - 50, - 48 - ], - [ - 20, - -50 - ], - [ - 0, - -51 - ], - [ - -26, - -55 - ], - [ - -45, - -89 - ], - [ - -36, - -48 - ], - [ - 26, - -58 - ], - [ - -54, - -1 - ], - [ - -60, - -46 - ], - [ - -18, - -78 - ], - [ - -40, - -121 - ], - [ - -55, - -54 - ], - [ - -35, - -34 - ], - [ - -64, - 2 - ], - [ - -45, - 40 - ], - [ - -76, - 8 - ], - [ - -11, - 44 - ], - [ - 37, - 89 - ], - [ - 88, - 119 - ], - [ - 45, - 22 - ], - [ - 50, - 46 - ], - [ - 60, - 63 - ], - [ - 41, - 62 - ], - [ - 31, - 89 - ], - [ - 27, - 31 - ], - [ - 10, - 67 - ], - [ - 49, - 55 - ], - [ - 15, - -51 - ] - ], - [ - [ - 24719, - 6460 - ], - [ - 51, - -127 - ], - [ - 1, - 82 - ], - [ - 32, - -33 - ], - [ - 10, - -90 - ], - [ - 56, - -39 - ], - [ - 47, - -10 - ], - [ - 40, - 46 - ], - [ - 36, - -14 - ], - [ - -17, - -107 - ], - [ - -21, - -70 - ], - [ - -54, - 3 - ], - [ - -18, - -37 - ], - [ - 6, - -51 - ], - [ - -10, - -22 - ], - [ - -26, - -65 - ], - [ - -35, - -82 - ], - [ - -54, - -48 - ], - [ - -12, - 31 - ], - [ - -29, - 18 - ], - [ - 40, - 98 - ], - [ - -23, - 66 - ], - [ - -75, - 48 - ], - [ - 2, - 44 - ], - [ - 51, - 42 - ], - [ - 12, - 92 - ], - [ - -4, - 78 - ], - [ - -28, - 80 - ], - [ - 2, - 21 - ], - [ - -33, - 50 - ], - [ - -55, - 106 - ], - [ - -29, - 85 - ], - [ - 26, - 9 - ], - [ - 37, - -66 - ], - [ - 55, - -32 - ], - [ - 19, - -106 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 20, - 41 - ], - [ - 9, - -11 - ], - [ - -7, - -49 - ], - [ - -9, - -22 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - 31, - -82 - ], - [ - 39, - -43 - ], - [ - 51, - -16 - ], - [ - 41, - -22 - ], - [ - 32, - -68 - ], - [ - 19, - -40 - ], - [ - 25, - -15 - ], - [ - -1, - -27 - ], - [ - -25, - -72 - ], - [ - -11, - -33 - ], - [ - -29, - -39 - ], - [ - -26, - -82 - ], - [ - -32, - 6 - ], - [ - -15, - -28 - ], - [ - -11, - -61 - ], - [ - 9, - -80 - ], - [ - -7, - -15 - ], - [ - -32, - 0 - ], - [ - -43, - -44 - ], - [ - -7, - -59 - ], - [ - -16, - -25 - ], - [ - -43, - 1 - ], - [ - -28, - -30 - ], - [ - 1, - -49 - ], - [ - -34, - -33 - ], - [ - -39, - 11 - ], - [ - -46, - -40 - ], - [ - -32, - -7 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -23, - 84 - ], - [ - -55, - 198 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - 209, - 120 - ], - [ - 47, - 240 - ], - [ - -32, - 84 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - -51, - 31 - ], - [ - -21, - 86 - ], - [ - -54, - 91 - ], - [ - -128, - -22 - ], - [ - -113, - -2 - ], - [ - -99, - -17 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - -24, - 34 - ], - [ - -15, - 65 - ], - [ - 18, - 32 - ], - [ - -18, - 8 - ], - [ - -13, - 40 - ], - [ - -35, - 33 - ], - [ - -30, - -7 - ], - [ - -14, - -42 - ], - [ - -29, - -30 - ], - [ - -15, - -4 - ], - [ - -7, - -25 - ], - [ - 34, - -65 - ], - [ - -19, - -16 - ], - [ - -11, - -17 - ], - [ - -32, - -7 - ], - [ - -12, - 72 - ], - [ - -9, - -20 - ], - [ - -23, - 7 - ], - [ - -14, - 48 - ], - [ - -29, - 8 - ], - [ - -18, - 14 - ], - [ - -30, - 0 - ], - [ - -2, - -26 - ], - [ - -8, - 18 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - 25, - -43 - ], - [ - -1, - -26 - ], - [ - 28, - -5 - ], - [ - 6, - 10 - ], - [ - 20, - -30 - ], - [ - 34, - 9 - ], - [ - 29, - 30 - ], - [ - 43, - 24 - ], - [ - 24, - 36 - ], - [ - 38, - -7 - ], - [ - -3, - -12 - ], - [ - 39, - -4 - ], - [ - 31, - -20 - ], - [ - 23, - -36 - ], - [ - 26, - -34 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - -70, - 69 - ], - [ - -6, - 49 - ], - [ - -138, - 121 - ], - [ - -125, - 131 - ], - [ - -54, - 74 - ], - [ - -29, - 99 - ], - [ - 12, - 34 - ], - [ - -59, - 158 - ], - [ - -69, - 221 - ], - [ - -66, - 239 - ], - [ - -29, - 55 - ], - [ - -21, - 88 - ], - [ - -55, - 78 - ], - [ - -49, - 49 - ], - [ - 22, - 54 - ], - [ - -34, - 114 - ], - [ - 22, - 84 - ], - [ - 56, - 76 - ] - ], - [ - [ - 21357, - 11807 - ], - [ - 7, - -80 - ], - [ - 4, - -67 - ], - [ - -24, - -110 - ], - [ - -25, - 122 - ], - [ - -33, - -61 - ], - [ - 23, - -88 - ], - [ - -20, - -56 - ], - [ - -82, - 69 - ], - [ - -20, - 87 - ], - [ - 21, - 57 - ], - [ - -44, - 57 - ], - [ - -22, - -50 - ], - [ - -33, - 5 - ], - [ - -51, - -67 - ], - [ - -12, - 35 - ], - [ - 28, - 101 - ], - [ - 44, - 34 - ], - [ - 38, - 45 - ], - [ - 24, - -54 - ], - [ - 53, - 33 - ], - [ - 12, - 53 - ], - [ - 49, - 3 - ], - [ - -4, - 93 - ], - [ - 56, - -57 - ], - [ - 6, - -60 - ], - [ - 5, - -44 - ] - ], - [ - [ - 21190, - 12030 - ], - [ - -25, - -39 - ], - [ - -22, - -76 - ], - [ - -22, - -35 - ], - [ - -43, - 82 - ], - [ - 15, - 33 - ], - [ - 17, - 33 - ], - [ - 8, - 75 - ], - [ - 38, - 7 - ], - [ - -11, - -81 - ], - [ - 52, - 116 - ], - [ - -7, - -115 - ] - ], - [ - [ - 20808, - 11915 - ], - [ - -92, - -114 - ], - [ - 34, - 84 - ], - [ - 50, - 74 - ], - [ - 42, - 83 - ], - [ - 36, - 119 - ], - [ - 13, - -98 - ], - [ - -46, - -66 - ], - [ - -37, - -82 - ] - ], - [ - [ - 21044, - 12224 - ], - [ - 42, - -37 - ], - [ - 44, - 0 - ], - [ - -1, - -50 - ], - [ - -33, - -51 - ], - [ - -44, - -36 - ], - [ - -2, - 56 - ], - [ - 5, - 61 - ], - [ - -11, - 57 - ] - ], - [ - [ - 21296, - 12256 - ], - [ - 20, - -134 - ], - [ - -54, - 32 - ], - [ - 1, - -40 - ], - [ - 17, - -74 - ], - [ - -33, - -27 - ], - [ - -3, - 84 - ], - [ - -21, - 7 - ], - [ - -11, - 72 - ], - [ - 41, - -9 - ], - [ - 0, - 45 - ], - [ - -43, - 92 - ], - [ - 67, - -3 - ], - [ - 19, - -45 - ] - ], - [ - [ - 21019, - 12365 - ], - [ - -19, - -104 - ], - [ - -29, - 60 - ], - [ - -36, - 92 - ], - [ - 60, - -5 - ], - [ - 24, - -43 - ] - ], - [ - [ - 21005, - 13017 - ], - [ - 43, - -34 - ], - [ - 21, - 31 - ], - [ - 6, - -30 - ], - [ - -11, - -50 - ], - [ - 24, - -86 - ], - [ - -18, - -100 - ], - [ - -42, - -40 - ], - [ - -11, - -96 - ], - [ - 16, - -96 - ], - [ - 37, - -13 - ], - [ - 31, - 14 - ], - [ - 87, - -66 - ], - [ - -7, - -66 - ], - [ - 23, - -29 - ], - [ - -7, - -55 - ], - [ - -55, - 59 - ], - [ - -25, - 63 - ], - [ - -18, - -44 - ], - [ - -45, - 72 - ], - [ - -63, - -18 - ], - [ - -35, - 27 - ], - [ - 4, - 49 - ], - [ - 22, - 31 - ], - [ - -21, - 28 - ], - [ - -9, - -44 - ], - [ - -35, - 69 - ], - [ - -10, - 52 - ], - [ - -3, - 115 - ], - [ - 28, - -39 - ], - [ - 8, - 188 - ], - [ - 22, - 108 - ], - [ - 43, - 0 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 121, - -82 - ], - [ - 129, - -69 - ], - [ - 48, - -62 - ], - [ - 39, - -60 - ], - [ - 11, - -71 - ], - [ - 116, - -74 - ], - [ - 17, - -63 - ], - [ - -64, - -13 - ], - [ - 15, - -80 - ], - [ - 62, - -79 - ], - [ - 46, - -127 - ], - [ - 39, - 4 - ], - [ - -2, - -53 - ], - [ - 53, - -21 - ], - [ - -20, - -22 - ], - [ - 74, - -51 - ], - [ - -8, - -34 - ], - [ - -46, - -9 - ], - [ - -17, - 31 - ], - [ - -60, - 14 - ], - [ - -71, - 18 - ], - [ - -54, - 76 - ], - [ - -39, - 66 - ], - [ - -37, - 105 - ], - [ - -91, - 53 - ], - [ - -59, - -34 - ], - [ - -42, - -40 - ], - [ - 9, - -88 - ], - [ - -55, - -42 - ], - [ - -39, - 20 - ], - [ - -72, - 5 - ] - ], - [ - [ - 23414, - 9979 - ], - [ - -20, - -12 - ], - [ - -30, - 46 - ], - [ - -31, - 76 - ], - [ - -15, - 92 - ], - [ - 10, - 11 - ], - [ - 8, - -35 - ], - [ - 21, - -28 - ], - [ - 33, - -76 - ], - [ - 33, - -40 - ], - [ - -9, - -34 - ] - ], - [ - [ - 23142, - 10140 - ], - [ - -37, - -10 - ], - [ - -11, - -34 - ], - [ - -38, - -29 - ], - [ - -35, - -28 - ], - [ - -37, - 0 - ], - [ - -58, - 35 - ], - [ - -39, - 34 - ], - [ - 5, - 37 - ], - [ - 63, - -18 - ], - [ - 38, - 10 - ], - [ - 10, - 57 - ], - [ - 10, - 3 - ], - [ - 7, - -64 - ], - [ - 40, - 10 - ], - [ - 20, - 41 - ], - [ - 39, - 42 - ], - [ - -8, - 71 - ], - [ - 42, - 2 - ], - [ - 14, - -19 - ], - [ - -2, - -67 - ], - [ - -23, - -73 - ] - ], - [ - [ - 23223, - 10257 - ], - [ - -22, - -32 - ], - [ - -13, - 71 - ], - [ - -17, - 47 - ], - [ - -31, - 39 - ], - [ - -40, - 51 - ], - [ - -50, - 35 - ], - [ - 19, - 29 - ], - [ - 38, - -33 - ], - [ - 24, - -27 - ], - [ - 29, - -29 - ], - [ - 28, - -50 - ], - [ - 26, - -38 - ], - [ - 9, - -63 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - 35, - -105 - ], - [ - -8, - -33 - ], - [ - -34, - -14 - ], - [ - -64, - -100 - ], - [ - 18, - -54 - ], - [ - -15, - 7 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -66, - 46 - ], - [ - -50, - -17 - ], - [ - -33, - 12 - ], - [ - -42, - -25 - ], - [ - -35, - 42 - ], - [ - -28, - -16 - ], - [ - -4, - 7 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 47, - 36 - ], - [ - 109, - 55 - ], - [ - 88, - 41 - ], - [ - 70, - -21 - ], - [ - 5, - -29 - ], - [ - 67, - -1 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 86, - -14 - ], - [ - 128, - 2 - ] - ], - [ - [ - 7927, - 13018 - ], - [ - 36, - -10 - ], - [ - 12, - -24 - ], - [ - -18, - -30 - ], - [ - -52, - 0 - ], - [ - -41, - -4 - ], - [ - -4, - 52 - ], - [ - 10, - 17 - ], - [ - 57, - -1 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - 10, - -21 - ] - ], - [ - [ - 21664, - 15862 - ], - [ - -27, - 7 - ], - [ - -30, - -40 - ], - [ - -21, - -41 - ], - [ - 3, - -86 - ], - [ - -36, - -27 - ], - [ - -12, - -21 - ], - [ - -27, - -35 - ], - [ - -46, - -20 - ], - [ - -30, - -32 - ], - [ - -3, - -52 - ], - [ - -8, - -13 - ], - [ - 28, - -20 - ], - [ - 40, - -53 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - -34, - 23 - ], - [ - -8, - -23 - ], - [ - -21, - -10 - ], - [ - -2, - 23 - ], - [ - -18, - 11 - ], - [ - -19, - 19 - ], - [ - 19, - 53 - ], - [ - 17, - 14 - ], - [ - -7, - 22 - ], - [ - 18, - 65 - ], - [ - -5, - 19 - ], - [ - -40, - 13 - ], - [ - -33, - 32 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -28, - -31 - ], - [ - -37, - 17 - ], - [ - -36, - -14 - ], - [ - 11, - 94 - ], - [ - -7, - 74 - ], - [ - -31, - 11 - ], - [ - -17, - 45 - ], - [ - 6, - 79 - ], - [ - 28, - 44 - ], - [ - 5, - 48 - ], - [ - 14, - 72 - ], - [ - -1, - 51 - ], - [ - -14, - 43 - ], - [ - -3, - 41 - ] - ], - [ - [ - 16089, - 13767 - ], - [ - -4, - 87 - ], - [ - 19, - 63 - ], - [ - 19, - 13 - ], - [ - 21, - -37 - ], - [ - 1, - -71 - ], - [ - -15, - -70 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - -20, - -9 - ], - [ - -21, - 24 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - -13, - 21 - ], - [ - 16, - 20 - ], - [ - -17, - 15 - ], - [ - -22, - -27 - ], - [ - -40, - 35 - ], - [ - -6, - 50 - ], - [ - -42, - 28 - ], - [ - -8, - 38 - ], - [ - -38, - 47 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - 30, - 25 - ], - [ - 43, - -13 - ], - [ - 45, - 0 - ], - [ - 32, - -30 - ], - [ - 24, - 19 - ], - [ - 51, - 11 - ], - [ - 18, - 28 - ], - [ - 29, - 0 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - 31, - -22 - ], - [ - 32, - 20 - ], - [ - 32, - -21 - ] - ], - [ - [ - 14611, - 16231 - ], - [ - 2, - -31 - ], - [ - -34, - -26 - ], - [ - -21, - 11 - ], - [ - -20, - -144 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - -89, - 101 - ], - [ - -80, - 46 - ], - [ - -60, - 70 - ], - [ - 51, - 19 - ], - [ - 58, - 101 - ], - [ - -39, - 47 - ], - [ - 102, - 49 - ], - [ - -1, - 26 - ], - [ - -63, - -19 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - 2, - 53 - ], - [ - 36, - 34 - ], - [ - 68, - 9 - ], - [ - 11, - 40 - ], - [ - -16, - 66 - ], - [ - 28, - 63 - ], - [ - 0, - 35 - ], - [ - -103, - 39 - ], - [ - -41, - -1 - ], - [ - -43, - 56 - ], - [ - -53, - -19 - ], - [ - -89, - 42 - ], - [ - 2, - 23 - ], - [ - -25, - 53 - ], - [ - -56, - 5 - ], - [ - -6, - 38 - ], - [ - 18, - 24 - ], - [ - -45, - 68 - ], - [ - -72, - -12 - ], - [ - -21, - 6 - ], - [ - -18, - -27 - ], - [ - -26, - 5 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 79, - 67 - ], - [ - -73, - 57 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - 71, - 42 - ], - [ - 115, - -73 - ], - [ - 191, - -28 - ], - [ - 263, - -136 - ], - [ - 54, - -57 - ], - [ - 4, - -80 - ], - [ - -77, - -63 - ], - [ - -114, - -32 - ], - [ - -311, - 91 - ], - [ - -51, - -15 - ], - [ - 113, - -88 - ], - [ - 5, - -56 - ], - [ - 4, - -122 - ], - [ - 90, - -37 - ], - [ - 55, - -31 - ], - [ - 9, - 58 - ], - [ - -42, - 52 - ], - [ - 44, - 45 - ], - [ - 168, - -74 - ], - [ - 59, - 29 - ], - [ - -47, - 88 - ], - [ - 163, - 117 - ], - [ - 64, - -7 - ], - [ - 65, - -42 - ], - [ - 41, - 83 - ], - [ - -58, - 71 - ], - [ - 34, - 72 - ], - [ - -51, - 75 - ], - [ - 195, - -39 - ], - [ - 39, - -67 - ], - [ - -88, - -15 - ], - [ - 1, - -67 - ], - [ - 54, - -41 - ], - [ - 108, - 26 - ], - [ - 17, - 77 - ], - [ - 146, - 57 - ], - [ - 243, - 103 - ], - [ - 53, - -6 - ], - [ - -69, - -73 - ], - [ - 86, - -12 - ], - [ - 50, - 41 - ], - [ - 131, - 3 - ], - [ - 103, - 50 - ], - [ - 80, - -73 - ], - [ - 79, - 80 - ], - [ - -73, - 69 - ], - [ - 36, - 40 - ], - [ - 206, - -36 - ], - [ - 97, - -38 - ], - [ - 252, - -137 - ], - [ - 47, - 63 - ], - [ - -71, - 63 - ], - [ - -2, - 26 - ], - [ - -84, - 12 - ], - [ - 23, - 56 - ], - [ - -37, - 94 - ], - [ - -2, - 38 - ], - [ - 128, - 109 - ], - [ - 46, - 109 - ], - [ - 52, - 24 - ], - [ - 184, - -32 - ], - [ - 15, - -67 - ], - [ - -66, - -97 - ], - [ - 43, - -38 - ], - [ - 23, - -84 - ], - [ - -16, - -164 - ], - [ - 77, - -74 - ], - [ - -30, - -80 - ], - [ - -137, - -170 - ], - [ - 80, - -18 - ], - [ - 28, - 43 - ], - [ - 76, - 31 - ], - [ - 19, - 59 - ], - [ - 60, - 57 - ], - [ - -40, - 69 - ], - [ - 32, - 79 - ], - [ - -76, - 10 - ], - [ - -17, - 66 - ], - [ - 56, - 121 - ], - [ - -91, - 98 - ], - [ - 125, - 80 - ], - [ - -16, - 86 - ], - [ - 35, - 3 - ], - [ - 36, - -67 - ], - [ - -27, - -116 - ], - [ - 74, - -22 - ], - [ - -31, - 87 - ], - [ - 116, - 47 - ], - [ - 145, - 6 - ], - [ - 129, - -68 - ], - [ - -62, - 100 - ], - [ - -7, - 128 - ], - [ - 121, - 24 - ], - [ - 168, - -5 - ], - [ - 151, - 15 - ], - [ - -57, - 63 - ], - [ - 81, - 79 - ], - [ - 80, - 3 - ], - [ - 135, - 60 - ], - [ - 184, - 16 - ], - [ - 24, - 32 - ], - [ - 183, - 12 - ], - [ - 57, - -27 - ], - [ - 156, - 63 - ], - [ - 128, - -2 - ], - [ - 20, - 52 - ], - [ - 66, - 51 - ], - [ - 165, - 50 - ], - [ - 119, - -39 - ], - [ - -95, - -30 - ], - [ - 158, - -18 - ], - [ - 19, - -60 - ], - [ - 64, - 30 - ], - [ - 204, - -2 - ], - [ - 157, - -59 - ], - [ - 56, - -44 - ], - [ - -18, - -63 - ], - [ - -77, - -35 - ], - [ - -183, - -67 - ], - [ - -52, - -36 - ], - [ - 86, - -16 - ], - [ - 103, - -31 - ], - [ - 63, - 23 - ], - [ - 35, - -77 - ], - [ - 31, - 31 - ], - [ - 112, - 19 - ], - [ - 223, - -20 - ], - [ - 17, - -56 - ], - [ - 292, - -18 - ], - [ - 4, - 92 - ], - [ - 148, - -21 - ], - [ - 111, - 1 - ], - [ - 112, - -63 - ], - [ - 32, - -77 - ], - [ - -41, - -50 - ], - [ - 88, - -95 - ], - [ - 109, - -49 - ], - [ - 68, - 126 - ], - [ - 111, - -54 - ], - [ - 119, - 33 - ], - [ - 135, - -37 - ], - [ - 52, - 33 - ], - [ - 114, - -16 - ], - [ - -51, - 111 - ], - [ - 92, - 52 - ], - [ - 630, - -78 - ], - [ - 59, - -71 - ], - [ - 183, - -92 - ], - [ - 281, - 23 - ], - [ - 139, - -20 - ], - [ - 58, - -50 - ], - [ - -8, - -87 - ], - [ - 85, - -34 - ], - [ - 94, - 24 - ], - [ - 123, - 3 - ], - [ - 132, - -23 - ], - [ - 132, - 13 - ], - [ - 121, - -107 - ], - [ - 87, - 39 - ], - [ - -57, - 76 - ], - [ - 32, - 54 - ], - [ - 222, - -34 - ], - [ - 145, - 7 - ], - [ - 200, - -57 - ], - [ - 98, - -52 - ], - [ - 0, - -478 - ], - [ - -1, - -1 - ], - [ - -89, - -53 - ], - [ - -90, - 9 - ], - [ - 62, - -64 - ], - [ - 42, - -99 - ], - [ - 32, - -32 - ], - [ - 8, - -49 - ], - [ - -18, - -32 - ], - [ - -130, - 26 - ], - [ - -195, - -90 - ], - [ - -62, - -14 - ], - [ - -106, - -85 - ], - [ - -101, - -73 - ], - [ - -26, - -55 - ], - [ - -100, - 83 - ], - [ - -181, - -94 - ], - [ - -32, - 45 - ], - [ - -67, - -52 - ], - [ - -93, - 17 - ], - [ - -23, - -79 - ], - [ - -84, - -116 - ], - [ - 3, - -49 - ], - [ - 79, - -27 - ], - [ - -9, - -174 - ], - [ - -65, - -5 - ], - [ - -30, - -100 - ], - [ - 29, - -52 - ], - [ - -121, - -61 - ], - [ - -25, - -137 - ], - [ - -104, - -29 - ], - [ - -20, - -122 - ], - [ - -101, - -112 - ], - [ - -26, - 83 - ], - [ - -30, - 175 - ], - [ - -38, - 266 - ], - [ - 33, - 167 - ], - [ - 59, - 71 - ], - [ - 3, - 56 - ], - [ - 109, - 27 - ], - [ - 124, - 151 - ], - [ - 120, - 123 - ], - [ - 126, - 96 - ], - [ - 56, - 169 - ], - [ - -85, - -10 - ], - [ - -42, - -99 - ], - [ - -177, - -131 - ], - [ - -57, - 147 - ], - [ - -180, - -41 - ], - [ - -174, - -201 - ], - [ - 57, - -73 - ], - [ - -155, - -32 - ], - [ - -108, - -12 - ], - [ - 5, - 87 - ], - [ - -108, - 18 - ], - [ - -87, - -59 - ], - [ - -213, - 21 - ], - [ - -229, - -36 - ], - [ - -226, - -234 - ], - [ - -267, - -283 - ], - [ - 110, - -15 - ], - [ - 34, - -75 - ], - [ - 68, - -27 - ], - [ - 44, - 60 - ], - [ - 77, - -8 - ], - [ - 100, - -132 - ], - [ - 3, - -102 - ], - [ - -55, - -120 - ], - [ - -6, - -143 - ], - [ - -31, - -192 - ], - [ - -105, - -173 - ], - [ - -23, - -83 - ], - [ - -95, - -140 - ], - [ - -94, - -138 - ], - [ - -45, - -71 - ], - [ - -93, - -71 - ], - [ - -44, - -1 - ], - [ - -44, - 58 - ], - [ - -93, - -88 - ], - [ - -11, - -40 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -32, - -71 - ], - [ - -67, - -20 - ], - [ - -69, - -124 - ], - [ - 63, - -114 - ], - [ - -7, - -81 - ], - [ - 76, - -141 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 16, - 52 - ], - [ - 96, - 39 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - 19, - 30 - ], - [ - 49, - -26 - ], - [ - 23, - -4 - ], - [ - 9, - -24 - ], - [ - 10, - -4 - ] - ], - [ - [ - 14988, - 16284 - ], - [ - 1, - -10 - ], - [ - 34, - -29 - ], - [ - 71, - 7 - ], - [ - -14, - -43 - ], - [ - -76, - -20 - ], - [ - -95, - -70 - ], - [ - -38, - 25 - ], - [ - 15, - 56 - ], - [ - -76, - 35 - ], - [ - 12, - 23 - ], - [ - 67, - 40 - ], - [ - -11, - 14 - ] - ], - [ - [ - 22561, - 16885 - ], - [ - 70, - -212 - ], - [ - -103, - 39 - ], - [ - -43, - -173 - ], - [ - 68, - -123 - ], - [ - -2, - -84 - ], - [ - -53, - 73 - ], - [ - -46, - -93 - ], - [ - -12, - 100 - ], - [ - 7, - 117 - ], - [ - -8, - 130 - ], - [ - 17, - 90 - ], - [ - 3, - 161 - ], - [ - -41, - 118 - ], - [ - 6, - 164 - ], - [ - 64, - 55 - ], - [ - -27, - 56 - ], - [ - 31, - 16 - ], - [ - 18, - -79 - ], - [ - 24, - -116 - ], - [ - -2, - -118 - ], - [ - 29, - -121 - ] - ], - [ - [ - 348, - 18785 - ], - [ - 47, - -30 - ], - [ - -17, - 88 - ], - [ - 190, - -18 - ], - [ - 136, - -113 - ], - [ - -69, - -52 - ], - [ - -114, - -12 - ], - [ - -2, - -118 - ], - [ - -28, - -24 - ], - [ - -65, - 3 - ], - [ - -53, - 42 - ], - [ - -93, - 35 - ], - [ - -16, - 52 - ], - [ - -70, - 20 - ], - [ - -80, - -16 - ], - [ - -38, - 42 - ], - [ - 16, - 45 - ], - [ - -84, - -29 - ], - [ - 32, - -56 - ], - [ - -40, - -51 - ], - [ - 0, - 478 - ], - [ - 171, - -92 - ], - [ - 183, - -119 - ], - [ - -6, - -75 - ] - ], - [ - [ - 25095, - 19295 - ], - [ - -76, - -6 - ], - [ - -13, - 38 - ], - [ - 89, - 50 - ], - [ - 0, - -82 - ] - ], - [ - [ - 91, - 19302 - ], - [ - -91, - -7 - ], - [ - 0, - 82 - ], - [ - 9, - 5 - ], - [ - 59, - 0 - ], - [ - 101, - -35 - ], - [ - -6, - -16 - ], - [ - -72, - -29 - ] - ], - [ - [ - 22558, - 19580 - ], - [ - -106, - 0 - ], - [ - -143, - 13 - ], - [ - -12, - 6 - ], - [ - 66, - 48 - ], - [ - 87, - 11 - ], - [ - 99, - -46 - ], - [ - 9, - -32 - ] - ], - [ - [ - 23055, - 19805 - ], - [ - -81, - -47 - ], - [ - -111, - 10 - ], - [ - -130, - 48 - ], - [ - 17, - 38 - ], - [ - 130, - -18 - ], - [ - 175, - -31 - ] - ], - [ - [ - 22661, - 19862 - ], - [ - -55, - -89 - ], - [ - -257, - 4 - ], - [ - -115, - -29 - ], - [ - -138, - 78 - ], - [ - 37, - 83 - ], - [ - 92, - 22 - ], - [ - 184, - -5 - ], - [ - 252, - -64 - ] - ], - [ - [ - 16558, - 19281 - ], - [ - -41, - -10 - ], - [ - -228, - 16 - ], - [ - -18, - 53 - ], - [ - -126, - 32 - ], - [ - -11, - 65 - ], - [ - 72, - 25 - ], - [ - -3, - 66 - ], - [ - 139, - 102 - ], - [ - -65, - 15 - ], - [ - 167, - 105 - ], - [ - -18, - 55 - ], - [ - 155, - 63 - ], - [ - 231, - 77 - ], - [ - 232, - 22 - ], - [ - 119, - 45 - ], - [ - 136, - 16 - ], - [ - 48, - -48 - ], - [ - -47, - -37 - ], - [ - -247, - -60 - ], - [ - -213, - -57 - ], - [ - -216, - -114 - ], - [ - -104, - -117 - ], - [ - -109, - -116 - ], - [ - 14, - -99 - ], - [ - 133, - -99 - ] - ], - [ - [ - 19872, - 20192 - ], - [ - -393, - -47 - ], - [ - 128, - 158 - ], - [ - 57, - 13 - ], - [ - 52, - -8 - ], - [ - 177, - -68 - ], - [ - -21, - -48 - ] - ], - [ - [ - 16112, - 20460 - ], - [ - -93, - -15 - ], - [ - -63, - -10 - ], - [ - -10, - -19 - ], - [ - -81, - -20 - ], - [ - -76, - 28 - ], - [ - 40, - 38 - ], - [ - -155, - 3 - ], - [ - 136, - 22 - ], - [ - 106, - 2 - ], - [ - 14, - -33 - ], - [ - 40, - 29 - ], - [ - 66, - 20 - ], - [ - 103, - -26 - ], - [ - -27, - -19 - ] - ], - [ - [ - 19514, - 20260 - ], - [ - -152, - -15 - ], - [ - -194, - 35 - ], - [ - -116, - 46 - ], - [ - -53, - 86 - ], - [ - -95, - 24 - ], - [ - 181, - 82 - ], - [ - 150, - 27 - ], - [ - 136, - -61 - ], - [ - 160, - -116 - ], - [ - -17, - -108 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - 17, - -12 - ], - [ - 42, - 37 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 28, - -68 - ], - [ - -4, - -70 - ], - [ - -21, - -15 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 3, - 50 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - -201, - -46 - ], - [ - -65, - -54 - ], - [ - -50, - -126 - ], - [ - -32, - -20 - ], - [ - -18, - 40 - ], - [ - -26, - -6 - ], - [ - -68, - 12 - ], - [ - -13, - 12 - ], - [ - -80, - -3 - ], - [ - -19, - -11 - ], - [ - -28, - 31 - ], - [ - -19, - -59 - ], - [ - 7, - -50 - ], - [ - -30, - -39 - ] - ], - [ - [ - 15530, - 12758 - ], - [ - -9, - 52 - ], - [ - -21, - 36 - ], - [ - -6, - 48 - ], - [ - -36, - 43 - ], - [ - -37, - 100 - ], - [ - -20, - 98 - ], - [ - -48, - 83 - ], - [ - -31, - 19 - ], - [ - -46, - 115 - ], - [ - -8, - 83 - ], - [ - 3, - 71 - ], - [ - -40, - 133 - ], - [ - -33, - 47 - ], - [ - -38, - 25 - ], - [ - -22, - 68 - ], - [ - 3, - 28 - ], - [ - -19, - 62 - ], - [ - -20, - 27 - ], - [ - -28, - 89 - ], - [ - -42, - 97 - ], - [ - -36, - 82 - ], - [ - -34, - -1 - ], - [ - 10, - 66 - ], - [ - 4, - 42 - ], - [ - 8, - 48 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - 27, - -104 - ], - [ - 34, - -27 - ], - [ - 12, - -42 - ], - [ - 48, - -51 - ], - [ - 4, - -49 - ], - [ - -7, - -40 - ], - [ - 9, - -41 - ], - [ - 20, - -33 - ], - [ - 9, - -40 - ], - [ - 10, - -29 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - 13, - -46 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 1, - 29 - ], - [ - -25, - 35 - ], - [ - -1, - 70 - ], - [ - -15, - 46 - ], - [ - -24, - -7 - ], - [ - 7, - 44 - ], - [ - 18, - 50 - ], - [ - -8, - 50 - ], - [ - 23, - 37 - ], - [ - -15, - 28 - ], - [ - 19, - 74 - ], - [ - 32, - 88 - ], - [ - 60, - -8 - ], - [ - -4, - 476 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - 23, - -118 - ], - [ - -15, - -22 - ], - [ - 10, - -123 - ], - [ - 25, - -144 - ], - [ - 27, - -29 - ], - [ - 38, - -45 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 94 - ], - [ - -10, - 2 - ], - [ - 2, - 60 - ], - [ - -9, - 41 - ], - [ - -36, - 47 - ], - [ - -8, - 87 - ], - [ - 8, - 88 - ], - [ - -32, - 9 - ], - [ - -5, - -27 - ], - [ - -42, - -6 - ], - [ - 17, - -35 - ], - [ - 6, - -72 - ], - [ - -38, - -66 - ], - [ - -35, - -87 - ], - [ - -36, - -12 - ], - [ - -58, - 70 - ], - [ - -27, - -25 - ], - [ - -7, - -35 - ], - [ - -36, - -23 - ], - [ - -2, - -24 - ], - [ - -70, - 0 - ], - [ - -9, - 24 - ], - [ - -51, - 5 - ], - [ - -25, - -21 - ], - [ - -19, - 10 - ], - [ - -36, - 70 - ], - [ - -12, - 33 - ], - [ - -50, - -16 - ], - [ - -19, - -56 - ], - [ - -18, - -107 - ], - [ - -24, - -23 - ], - [ - -21, - -13 - ], - [ - 47, - -47 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - -43, - -55 - ], - [ - -49, - 0 - ], - [ - -56, - -28 - ], - [ - -44, - 27 - ], - [ - -29, - -33 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - -11, - 92 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - -28, - 94 - ], - [ - -35, - 42 - ], - [ - 31, - 23 - ], - [ - 33, - 84 - ], - [ - 17, - 62 - ] - ], - [ - [ - 23849, - 9540 - ], - [ - 19, - -42 - ], - [ - -49, - 1 - ], - [ - -26, - 74 - ], - [ - 41, - -29 - ], - [ - 15, - -4 - ] - ], - [ - [ - 23760, - 9613 - ], - [ - -27, - -3 - ], - [ - -43, - 12 - ], - [ - -14, - 19 - ], - [ - 4, - 47 - ], - [ - 46, - -19 - ], - [ - 23, - -25 - ], - [ - 11, - -31 - ] - ], - [ - [ - 23818, - 9645 - ], - [ - -11, - -22 - ], - [ - -51, - 104 - ], - [ - -15, - 72 - ], - [ - 24, - 0 - ], - [ - 25, - -96 - ], - [ - 28, - -58 - ] - ], - [ - [ - 23692, - 9797 - ], - [ - 3, - -24 - ], - [ - -55, - 51 - ], - [ - -38, - 43 - ], - [ - -26, - 40 - ], - [ - 11, - 12 - ], - [ - 32, - -29 - ], - [ - 57, - -55 - ], - [ - 16, - -38 - ] - ], - [ - [ - 23529, - 9916 - ], - [ - -14, - -7 - ], - [ - -30, - 27 - ], - [ - -29, - 49 - ], - [ - 4, - 20 - ], - [ - 41, - -50 - ], - [ - 28, - -39 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - -19, - 9 - ], - [ - -50, - 49 - ], - [ - -36, - 64 - ], - [ - -12, - 44 - ], - [ - -9, - 88 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - -8, - -28 - ], - [ - -41, - 1 - ], - [ - -25, - 12 - ], - [ - -28, - 24 - ], - [ - -39, - 7 - ], - [ - -20, - 26 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - 23, - -22 - ], - [ - 13, - -49 - ], - [ - 32, - -51 - ], - [ - 34, - 0 - ], - [ - 66, - 31 - ], - [ - 76, - 14 - ], - [ - 61, - 37 - ], - [ - 35, - 8 - ], - [ - 25, - 22 - ], - [ - 40, - 4 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - -1, - -2 - ], - [ - 0, - -49 - ], - [ - 0, - -121 - ], - [ - 0, - -63 - ], - [ - -32, - -74 - ], - [ - -48, - -100 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - 22, - 2 - ], - [ - 32, - 18 - ], - [ - 37, - 12 - ], - [ - 33, - 41 - ], - [ - 26, - 1 - ], - [ - 2, - -33 - ], - [ - -6, - -70 - ], - [ - 0, - -63 - ], - [ - -15, - -44 - ], - [ - -20, - -129 - ], - [ - -33, - -134 - ], - [ - -43, - -153 - ], - [ - -60, - -176 - ], - [ - -60, - -135 - ], - [ - -82, - -163 - ], - [ - -69, - -97 - ], - [ - -105, - -120 - ], - [ - -65, - -91 - ], - [ - -76, - -145 - ], - [ - -16, - -63 - ], - [ - -16, - -29 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - 83, - -24 - ], - [ - 8, - 21 - ], - [ - 56, - 9 - ], - [ - 75, - -32 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -19, - -31 - ], - [ - -14, - -49 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 15, - 11 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -120, - -34 - ], - [ - -68, - -84 - ], - [ - 11, - -73 - ], - [ - -111, - -97 - ], - [ - -134, - -103 - ], - [ - -51, - -169 - ], - [ - 49, - -84 - ], - [ - 67, - -67 - ], - [ - -64, - -135 - ], - [ - -72, - -28 - ], - [ - -27, - -202 - ], - [ - -40, - -112 - ], - [ - -84, - 12 - ], - [ - -40, - -96 - ], - [ - -80, - -5 - ], - [ - -22, - 113 - ], - [ - -59, - 136 - ], - [ - -53, - 170 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -14, - -53 - ], - [ - -41, - -13 - ], - [ - -41, - 65 - ], - [ - -1, - 41 - ], - [ - 19, - 45 - ], - [ - 7, - 35 - ], - [ - 20, - 9 - ], - [ - 35, - -22 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - -7, - 91 - ], - [ - 17, - 50 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - 19, - 26 - ], - [ - 19, - 26 - ], - [ - 4, - 67 - ], - [ - 22, - -23 - ], - [ - 77, - 33 - ], - [ - 37, - -22 - ], - [ - 58, - 0 - ], - [ - 80, - 45 - ], - [ - 37, - -2 - ], - [ - 80, - 19 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -57, - -26 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -63, - 55 - ], - [ - -60, - -2 - ], - [ - 11, - 94 - ], - [ - -62, - 0 - ], - [ - -5, - -132 - ], - [ - -38, - -176 - ], - [ - -23, - -106 - ], - [ - 5, - -86 - ], - [ - 46, - -4 - ], - [ - 28, - -110 - ], - [ - 12, - -103 - ], - [ - 39, - -69 - ], - [ - 42, - -14 - ], - [ - 37, - -62 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - -27, - 46 - ], - [ - -12, - 59 - ], - [ - -37, - 68 - ], - [ - -34, - 57 - ], - [ - -11, - -71 - ], - [ - -14, - 67 - ], - [ - 8, - 75 - ], - [ - 21, - 115 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 39, - 122 - ], - [ - -15, - 89 - ], - [ - -51, - 29 - ], - [ - 18, - 53 - ], - [ - 58, - -6 - ], - [ - 33, - 66 - ], - [ - 22, - 77 - ], - [ - 94, - 28 - ], - [ - -15, - -55 - ], - [ - 10, - -34 - ], - [ - 29, - 3 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - -13, - 85 - ], - [ - 10, - 125 - ], - [ - -54, - 41 - ], - [ - 18, - 82 - ], - [ - -46, - 7 - ], - [ - 15, - 101 - ], - [ - 66, - -29 - ], - [ - 61, - 38 - ], - [ - -51, - 72 - ], - [ - -20, - 69 - ], - [ - -56, - -31 - ], - [ - -7, - -88 - ], - [ - -22, - 78 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - 79, - 2 - ], - [ - -12, - 60 - ], - [ - 60, - 41 - ], - [ - 58, - 70 - ], - [ - 94, - -63 - ], - [ - 8, - -96 - ], - [ - 26, - -25 - ], - [ - 76, - 6 - ], - [ - 23, - -22 - ], - [ - 35, - -124 - ], - [ - 79, - -82 - ], - [ - 46, - -57 - ], - [ - 73, - -59 - ], - [ - 92, - -51 - ], - [ - -2, - -73 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 8, - 29 - ], - [ - 60, - 27 - ], - [ - 49, - 4 - ], - [ - 21, - 15 - ], - [ - 27, - -15 - ], - [ - -26, - -33 - ], - [ - -72, - -52 - ], - [ - -59, - -35 - ] - ], - [ - [ - 8248, - 12088 - ], - [ - 40, - 16 - ], - [ - 15, - -5 - ], - [ - -3, - -89 - ], - [ - -58, - -13 - ], - [ - -13, - 11 - ], - [ - 20, - 33 - ], - [ - -1, - 47 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - 75, - 48 - ], - [ - 49, - -14 - ], - [ - -2, - -61 - ], - [ - 59, - 44 - ], - [ - 5, - -23 - ], - [ - -35, - -59 - ], - [ - 0, - -55 - ], - [ - 24, - -30 - ], - [ - -9, - -104 - ], - [ - -46, - -60 - ], - [ - 13, - -66 - ], - [ - 36, - -2 - ], - [ - 18, - -57 - ], - [ - 26, - -18 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - -25, - 54 - ], - [ - 26, - 45 - ], - [ - -42, - -10 - ], - [ - -59, - 28 - ], - [ - -48, - -70 - ], - [ - -105, - -13 - ], - [ - -57, - 64 - ], - [ - -75, - 4 - ], - [ - -16, - -49 - ], - [ - -48, - -15 - ], - [ - -68, - 64 - ], - [ - -76, - -2 - ], - [ - -41, - 119 - ], - [ - -51, - 67 - ], - [ - 34, - 93 - ], - [ - -44, - 58 - ], - [ - 77, - 114 - ], - [ - 107, - 5 - ], - [ - 30, - 91 - ], - [ - 133, - -16 - ], - [ - 83, - 78 - ], - [ - 82, - 34 - ], - [ - 115, - 3 - ], - [ - 122, - -85 - ], - [ - 100, - -46 - ], - [ - 81, - 18 - ], - [ - 60, - -10 - ], - [ - 82, - 62 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - 8, - -46 - ], - [ - 61, - -39 - ], - [ - -12, - -29 - ], - [ - -83, - -7 - ], - [ - -30, - -37 - ], - [ - -58, - -65 - ], - [ - -22, - 56 - ], - [ - 1, - 25 - ] - ], - [ - [ - 21036, - 13724 - ], - [ - -42, - -193 - ], - [ - -29, - -98 - ], - [ - -37, - 101 - ], - [ - -8, - 89 - ], - [ - 41, - 118 - ], - [ - 56, - 91 - ], - [ - 32, - -36 - ], - [ - -13, - -72 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 24, - 14 - ], - [ - 77, - -1 - ], - [ - 142, - 9 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -32, - -148 - ], - [ - 4, - -68 - ], - [ - 45, - -43 - ], - [ - 2, - -32 - ], - [ - -19, - -72 - ], - [ - 4, - -36 - ], - [ - -5, - -58 - ], - [ - 24, - -75 - ], - [ - 29, - -118 - ], - [ - 26, - -27 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - -39, - 36 - ], - [ - -45, - 20 - ], - [ - -28, - 20 - ], - [ - -29, - 31 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - -56, - -10 - ], - [ - -46, - -38 - ], - [ - -65, - -7 - ], - [ - -60, - -44 - ], - [ - 3, - -65 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - -9, - 13 - ], - [ - -109, - 31 - ], - [ - -4, - 44 - ], - [ - -65, - -14 - ], - [ - -26, - -66 - ], - [ - -54, - -89 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -30, - -75 - ], - [ - -79, - -67 - ], - [ - -51, - 24 - ], - [ - -38, - -13 - ], - [ - -65, - 52 - ], - [ - -47, - -4 - ], - [ - -42, - 66 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - 13, - -39 - ], - [ - -75, - -58 - ], - [ - -72, - -42 - ], - [ - -73, - -35 - ], - [ - -37, - -71 - ], - [ - -12, - -27 - ], - [ - -1, - -64 - ], - [ - 23, - -64 - ], - [ - 29, - -3 - ], - [ - -7, - 44 - ], - [ - 21, - -26 - ], - [ - -6, - -35 - ], - [ - -47, - -19 - ], - [ - -33, - 2 - ], - [ - -52, - -21 - ], - [ - -30, - -6 - ], - [ - -41, - -6 - ], - [ - -58, - -34 - ], - [ - 103, - 22 - ], - [ - 20, - -22 - ], - [ - -97, - -36 - ], - [ - -45, - -1 - ], - [ - 2, - 15 - ], - [ - -21, - -33 - ], - [ - 21, - -6 - ], - [ - -15, - -86 - ], - [ - -51, - -92 - ], - [ - -5, - 31 - ], - [ - -16, - 6 - ], - [ - -22, - 30 - ], - [ - 14, - -65 - ], - [ - 17, - -21 - ], - [ - 1, - -46 - ], - [ - -22, - -46 - ], - [ - -39, - -96 - ], - [ - -7, - 5 - ], - [ - 22, - 81 - ], - [ - -36, - 46 - ], - [ - -8, - 100 - ], - [ - -13, - -52 - ], - [ - 15, - -76 - ], - [ - -46, - 19 - ], - [ - 48, - -39 - ], - [ - 3, - -114 - ], - [ - 20, - -8 - ], - [ - 7, - -42 - ], - [ - 10, - -120 - ], - [ - -45, - -89 - ], - [ - -72, - -35 - ], - [ - -46, - -71 - ], - [ - -34, - -8 - ], - [ - -36, - -44 - ], - [ - -10, - -40 - ], - [ - -76, - -78 - ], - [ - -39, - -57 - ], - [ - -33, - -71 - ], - [ - -11, - -85 - ], - [ - 12, - -83 - ], - [ - 24, - -103 - ], - [ - 30, - -85 - ], - [ - 1, - -52 - ], - [ - 33, - -139 - ], - [ - -2, - -81 - ], - [ - -3, - -47 - ], - [ - -18, - -73 - ], - [ - -21, - -15 - ], - [ - -34, - 15 - ], - [ - -11, - 52 - ], - [ - -26, - 28 - ], - [ - -37, - 103 - ], - [ - -33, - 92 - ], - [ - -10, - 47 - ], - [ - 14, - 79 - ], - [ - -19, - 66 - ], - [ - -55, - 101 - ], - [ - -27, - 18 - ], - [ - -70, - -54 - ], - [ - -13, - 6 - ], - [ - -34, - 56 - ], - [ - -43, - 29 - ], - [ - -79, - -15 - ], - [ - -62, - 13 - ], - [ - -53, - -8 - ], - [ - -29, - -19 - ], - [ - 13, - -31 - ], - [ - -2, - -49 - ], - [ - 15, - -24 - ], - [ - -13, - -16 - ], - [ - -26, - 18 - ], - [ - -26, - -23 - ], - [ - -51, - 4 - ], - [ - -52, - 64 - ], - [ - -60, - -15 - ], - [ - -51, - 27 - ], - [ - -44, - -8 - ], - [ - -58, - -28 - ], - [ - -64, - -89 - ], - [ - -69, - -52 - ], - [ - -38, - -57 - ], - [ - -16, - -54 - ], - [ - -1, - -83 - ], - [ - 4, - -57 - ], - [ - 13, - -41 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - -12, - 62 - ], - [ - -45, - 69 - ], - [ - -33, - 14 - ], - [ - -7, - 34 - ], - [ - -39, - 6 - ], - [ - -25, - 33 - ], - [ - -65, - 12 - ], - [ - -18, - 19 - ], - [ - -8, - 66 - ], - [ - -68, - 120 - ], - [ - -58, - 167 - ], - [ - 2, - 28 - ], - [ - -30, - 40 - ], - [ - -54, - 100 - ], - [ - -10, - 98 - ], - [ - -37, - 66 - ], - [ - 15, - 99 - ], - [ - -2, - 103 - ], - [ - -22, - 92 - ], - [ - 27, - 113 - ], - [ - 8, - 109 - ], - [ - 9, - 109 - ], - [ - -13, - 161 - ], - [ - -22, - 102 - ], - [ - -20, - 56 - ], - [ - 8, - 23 - ], - [ - 101, - -41 - ], - [ - 37, - -113 - ], - [ - 17, - 32 - ], - [ - -11, - 98 - ], - [ - -23, - 99 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - -38, - 45 - ], - [ - -62, - 38 - ], - [ - -19, - 105 - ], - [ - -90, - 97 - ], - [ - -38, - 113 - ], - [ - -67, - 8 - ], - [ - -111, - 3 - ], - [ - -81, - 34 - ], - [ - -144, - 125 - ], - [ - -67, - 23 - ], - [ - -122, - 42 - ], - [ - -97, - -10 - ], - [ - -137, - 55 - ], - [ - -83, - 51 - ], - [ - -77, - -25 - ], - [ - 14, - -83 - ], - [ - -38, - -8 - ], - [ - -81, - -25 - ], - [ - -61, - -40 - ], - [ - -77, - -26 - ], - [ - -10, - 71 - ], - [ - 31, - 117 - ], - [ - 74, - 37 - ], - [ - -19, - 30 - ], - [ - -89, - -66 - ], - [ - -47, - -80 - ], - [ - -101, - -86 - ], - [ - 51, - -58 - ], - [ - -66, - -86 - ], - [ - -75, - -50 - ], - [ - -69, - -37 - ], - [ - -18, - -53 - ], - [ - -109, - -62 - ], - [ - -22, - -56 - ], - [ - -81, - -52 - ], - [ - -48, - 10 - ], - [ - -65, - -34 - ], - [ - -71, - -41 - ], - [ - -58, - -40 - ], - [ - -119, - -34 - ], - [ - -11, - 20 - ], - [ - 76, - 56 - ], - [ - 68, - 37 - ], - [ - 74, - 66 - ], - [ - 87, - 13 - ], - [ - 34, - 50 - ], - [ - 97, - 71 - ], - [ - 15, - 24 - ], - [ - 52, - 43 - ], - [ - 12, - 91 - ], - [ - 35, - 71 - ], - [ - -80, - -37 - ], - [ - -22, - 21 - ], - [ - -38, - -44 - ], - [ - -46, - 61 - ], - [ - -19, - -43 - ], - [ - -26, - 60 - ], - [ - -69, - -48 - ], - [ - -43, - 0 - ], - [ - -6, - 71 - ], - [ - 13, - 44 - ], - [ - -45, - 43 - ], - [ - -91, - -23 - ], - [ - -59, - 56 - ], - [ - -48, - 29 - ], - [ - 0, - 68 - ], - [ - -54, - 51 - ], - [ - 27, - 69 - ], - [ - 57, - 67 - ], - [ - 25, - 62 - ], - [ - 57, - 9 - ], - [ - 47, - -20 - ], - [ - 57, - 58 - ], - [ - 50, - -10 - ], - [ - 53, - 37 - ], - [ - -13, - 55 - ], - [ - -39, - 22 - ], - [ - 52, - 46 - ], - [ - -43, - -2 - ], - [ - -74, - -26 - ], - [ - -21, - -26 - ], - [ - -55, - 26 - ], - [ - -99, - -13 - ], - [ - -102, - 29 - ], - [ - -29, - 48 - ], - [ - -88, - 70 - ], - [ - 98, - 50 - ], - [ - 155, - 58 - ], - [ - 58, - 0 - ], - [ - -10, - -60 - ], - [ - 147, - 5 - ], - [ - -56, - 74 - ], - [ - -86, - 46 - ], - [ - -50, - 60 - ], - [ - -67, - 51 - ], - [ - -95, - 38 - ], - [ - 39, - 63 - ], - [ - 123, - 4 - ], - [ - 88, - 55 - ], - [ - 17, - 58 - ], - [ - 71, - 57 - ], - [ - 68, - 14 - ], - [ - 132, - 53 - ], - [ - 64, - -8 - ], - [ - 108, - 64 - ], - [ - 105, - -25 - ], - [ - 50, - -54 - ], - [ - 31, - 23 - ], - [ - 118, - -7 - ], - [ - -4, - -28 - ], - [ - 107, - -20 - ], - [ - 71, - 12 - ], - [ - 147, - -38 - ], - [ - 134, - -12 - ], - [ - 53, - -15 - ], - [ - 93, - 19 - ], - [ - 106, - -36 - ], - [ - 76, - -17 - ] - ], - [ - [ - 1705, - 13087 - ], - [ - -10, - -20 - ], - [ - -18, - 17 - ], - [ - 2, - 33 - ], - [ - -11, - 44 - ], - [ - 3, - 13 - ], - [ - 12, - 20 - ], - [ - -4, - 23 - ], - [ - 4, - 12 - ], - [ - 5, - -3 - ], - [ - 27, - -20 - ], - [ - 12, - -10 - ], - [ - 11, - -16 - ], - [ - 18, - -42 - ], - [ - -2, - -7 - ], - [ - -27, - -26 - ], - [ - -22, - -18 - ] - ], - [ - [ - 1667, - 13274 - ], - [ - -23, - -9 - ], - [ - -12, - 26 - ], - [ - -8, - 9 - ], - [ - -1, - 8 - ], - [ - 7, - 10 - ], - [ - 25, - -11 - ], - [ - 18, - -19 - ], - [ - -6, - -14 - ] - ], - [ - [ - 1620, - 13338 - ], - [ - -2, - -13 - ], - [ - -37, - 3 - ], - [ - 5, - 15 - ], - [ - 34, - -5 - ] - ], - [ - [ - 1558, - 13355 - ], - [ - -4, - -7 - ], - [ - -5, - 2 - ], - [ - -24, - 4 - ], - [ - -9, - 27 - ], - [ - -3, - 5 - ], - [ - 19, - 17 - ], - [ - 6, - -8 - ], - [ - 20, - -40 - ] - ], - [ - [ - 1440, - 13434 - ], - [ - -8, - -12 - ], - [ - -24, - 22 - ], - [ - 4, - 9 - ], - [ - 10, - 12 - ], - [ - 16, - -3 - ], - [ - 2, - -28 - ] - ], - [ - [ - 1882, - 17649 - ], - [ - -70, - -45 - ], - [ - -36, - 31 - ], - [ - -10, - 56 - ], - [ - 63, - 42 - ], - [ - 37, - 19 - ], - [ - 46, - -8 - ], - [ - 30, - -38 - ], - [ - -60, - -57 - ] - ], - [ - [ - 1005, - 17985 - ], - [ - -43, - -19 - ], - [ - -45, - 22 - ], - [ - -43, - 33 - ], - [ - 69, - 20 - ], - [ - 56, - -10 - ], - [ - 6, - -46 - ] - ], - [ - [ - 576, - 18449 - ], - [ - 43, - -23 - ], - [ - 44, - 13 - ], - [ - 56, - -32 - ], - [ - 69, - -16 - ], - [ - -5, - -13 - ], - [ - -53, - -26 - ], - [ - -53, - 27 - ], - [ - -27, - 21 - ], - [ - -61, - -7 - ], - [ - -17, - 11 - ], - [ - 4, - 45 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -2, - -28 - ], - [ - -41, - -14 - ], - [ - 23, - -55 - ], - [ - -1, - -63 - ], - [ - -31, - -69 - ], - [ - 27, - -95 - ], - [ - 30, - 7 - ], - [ - 15, - 87 - ], - [ - -21, - 42 - ], - [ - -4, - 91 - ], - [ - 87, - 49 - ], - [ - -10, - 56 - ], - [ - 25, - 38 - ], - [ - 25, - -84 - ], - [ - 49, - -2 - ], - [ - 45, - -67 - ], - [ - 3, - -40 - ], - [ - 62, - -1 - ], - [ - 75, - 13 - ], - [ - 40, - -54 - ], - [ - 53, - -15 - ], - [ - 39, - 38 - ], - [ - 1, - 30 - ], - [ - 86, - 7 - ], - [ - 84, - 2 - ], - [ - -59, - -36 - ], - [ - 24, - -56 - ], - [ - 55, - -9 - ], - [ - 53, - -59 - ], - [ - 11, - -96 - ], - [ - 37, - 2 - ], - [ - 27, - -28 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -93, - -103 - ], - [ - -58, - -113 - ], - [ - -15, - -83 - ], - [ - 53, - -127 - ], - [ - 66, - -157 - ], - [ - 63, - -74 - ], - [ - 42, - -96 - ], - [ - 32, - -222 - ], - [ - -9, - -211 - ], - [ - -58, - -79 - ], - [ - -80, - -77 - ], - [ - -57, - -100 - ], - [ - -87, - -112 - ], - [ - -25, - 77 - ], - [ - 19, - 81 - ], - [ - -52, - 68 - ] - ], - [ - [ - 24248, - 8822 - ], - [ - -23, - -16 - ], - [ - -24, - 52 - ], - [ - 3, - 33 - ], - [ - 44, - -69 - ] - ], - [ - [ - 24196, - 9006 - ], - [ - 12, - -97 - ], - [ - -19, - 15 - ], - [ - -15, - -7 - ], - [ - -10, - 34 - ], - [ - -1, - 91 - ], - [ - 33, - -36 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -51, - -32 - ], - [ - -13, - -54 - ], - [ - -2, - -41 - ], - [ - -69, - -50 - ], - [ - -112, - -56 - ], - [ - -62, - -85 - ], - [ - -31, - -6 - ], - [ - -21, - 7 - ], - [ - -40, - -50 - ], - [ - -45, - -23 - ], - [ - -58, - -6 - ], - [ - -18, - -7 - ], - [ - -15, - -32 - ], - [ - -19, - -9 - ], - [ - -10, - -30 - ], - [ - -35, - 2 - ], - [ - -22, - -16 - ], - [ - -48, - 6 - ], - [ - -19, - 70 - ], - [ - 2, - 66 - ], - [ - -11, - 35 - ], - [ - -14, - 89 - ], - [ - -20, - 49 - ], - [ - 14, - 6 - ], - [ - -7, - 55 - ], - [ - 9, - 23 - ], - [ - -3, - 52 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - 29, - -1 - ], - [ - 33, - -21 - ], - [ - 24, - 15 - ], - [ - 37, - -12 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -17, - -87 - ], - [ - -9, - -100 - ], - [ - -18, - -54 - ], - [ - -47, - -61 - ], - [ - -14, - -17 - ], - [ - -29, - -61 - ], - [ - -20, - -62 - ], - [ - -39, - -86 - ], - [ - -79, - -123 - ], - [ - -49, - -72 - ], - [ - -53, - -55 - ], - [ - -73, - -47 - ], - [ - -35, - -6 - ], - [ - -9, - -33 - ], - [ - -43, - 18 - ], - [ - -34, - -23 - ], - [ - -76, - 23 - ], - [ - -42, - -15 - ], - [ - -29, - 7 - ], - [ - -72, - -48 - ], - [ - -59, - -19 - ], - [ - -43, - -45 - ], - [ - -32, - -3 - ], - [ - -30, - 43 - ], - [ - -23, - 2 - ], - [ - -30, - 54 - ], - [ - -3, - -17 - ], - [ - -10, - 32 - ], - [ - 1, - 70 - ], - [ - -23, - 81 - ], - [ - 23, - 22 - ], - [ - -2, - 92 - ], - [ - -46, - 112 - ], - [ - -35, - 102 - ], - [ - 0, - 0 - ], - [ - -50, - 156 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -53, - -17 - ], - [ - -40, - -47 - ], - [ - -8, - -42 - ], - [ - -25, - -10 - ], - [ - -61, - -98 - ], - [ - -38, - -78 - ], - [ - -24, - -3 - ], - [ - -22, - 14 - ], - [ - -78, - 13 - ] - ] - ], - "transform": { - "scale": [ - 0.01434548714883443, - 0.008335499711981569 - ], - "translate": [ - -180, - -90 - ] - }, - "objects": { - "ne_110m_admin_0_countries": { - "type": "GeometryCollection", - "geometries": [ - { - "arcs": [ - [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Afghanistan", - "NAME_LONG": "Afghanistan", - "ABBREV": "Afg.", - "FORMAL_EN": "Islamic State of Afghanistan", - "POP_EST": 34124811, - "POP_RANK": 15, - "GDP_MD_EST": 64080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AF", - "ISO_A3": "AFG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 6, - 7, - 8, - 9 - ] - ], - [ - [ - 10, - 11, - 12 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Angola", - "NAME_LONG": "Angola", - "ABBREV": "Ang.", - "FORMAL_EN": "People's Republic of Angola", - "POP_EST": 29310273, - "POP_RANK": 15, - "GDP_MD_EST": 189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AO", - "ISO_A3": "AGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - 13, - 14, - 15, - 16, - 17 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Albania", - "NAME_LONG": "Albania", - "ABBREV": "Alb.", - "FORMAL_EN": "Republic of Albania", - "POP_EST": 3047987, - "POP_RANK": 12, - "GDP_MD_EST": 33900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AL", - "ISO_A3": "ALB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 18, - 19, - 20, - 21, - 22 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "United Arab Emirates", - "NAME_LONG": "United Arab Emirates", - "ABBREV": "U.A.E.", - "FORMAL_EN": "United Arab Emirates", - "POP_EST": 6072475, - "POP_RANK": 13, - "GDP_MD_EST": 667200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AE", - "ISO_A3": "ARE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 23, - 24 - ] - ], - [ - [ - 25, - 26, - 27, - 28, - 29, - 30 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Argentina", - "NAME_LONG": "Argentina", - "ABBREV": "Arg.", - "FORMAL_EN": "Argentine Republic", - "POP_EST": 44293293, - "POP_RANK": 15, - "GDP_MD_EST": 879400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AR", - "ISO_A3": "ARG", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 31, - 32, - 33, - 34, - 35 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Armenia", - "NAME_LONG": "Armenia", - "ABBREV": "Arm.", - "FORMAL_EN": "Republic of Armenia", - "POP_EST": 3045191, - "POP_RANK": 12, - "GDP_MD_EST": 26300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AM", - "ISO_A3": "ARM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 36 - ] - ], - [ - [ - 37 - ] - ], - [ - [ - 38 - ] - ], - [ - [ - 39 - ] - ], - [ - [ - 40 - ] - ], - [ - [ - 41 - ] - ], - [ - [ - 42 - ] - ], - [ - [ - 43 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Antarctica", - "NAME_LONG": "Antarctica", - "ABBREV": "Ant.", - "FORMAL_EN": "", - "POP_EST": 4050, - "POP_RANK": 4, - "GDP_MD_EST": 810, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "AQ", - "ISO_A3": "ATA", - "CONTINENT": "Antarctica", - "REGION_UN": "Antarctica", - "SUBREGION": "Antarctica" - } - }, - { - "arcs": [ - [ - 44 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Fr. S. Antarctic Lands", - "NAME_LONG": "French Southern and Antarctic Lands", - "ABBREV": "Fr. S.A.L.", - "FORMAL_EN": "Territory of the French Southern and Antarctic Lands", - "POP_EST": 140, - "POP_RANK": 1, - "GDP_MD_EST": 16, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TF", - "ISO_A3": "ATF", - "CONTINENT": "Seven seas (open ocean)", - "REGION_UN": "Seven seas (open ocean)", - "SUBREGION": "Seven seas (open ocean)" - } - }, - { - "arcs": [ - [ - [ - 45 - ] - ], - [ - [ - 46 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Australia", - "NAME_LONG": "Australia", - "ABBREV": "Auz.", - "FORMAL_EN": "Commonwealth of Australia", - "POP_EST": 23232413, - "POP_RANK": 15, - "GDP_MD_EST": 1189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AU", - "ISO_A3": "AUS", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - 47, - 48, - 49, - 50, - 51, - 52, - 53 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Austria", - "NAME_LONG": "Austria", - "ABBREV": "Aust.", - "FORMAL_EN": "Republic of Austria", - "POP_EST": 8754413, - "POP_RANK": 13, - "GDP_MD_EST": 416600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AT", - "ISO_A3": "AUT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -33, - 54, - 55, - 56, - 57 - ] - ], - [ - [ - -35, - 58 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Azerbaijan", - "NAME_LONG": "Azerbaijan", - "ABBREV": "Aze.", - "FORMAL_EN": "Republic of Azerbaijan", - "POP_EST": 9961396, - "POP_RANK": 13, - "GDP_MD_EST": 167900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AZ", - "ISO_A3": "AZE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 59, - 60, - 61 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burundi", - "NAME_LONG": "Burundi", - "ABBREV": "Bur.", - "FORMAL_EN": "Republic of Burundi", - "POP_EST": 11466756, - "POP_RANK": 14, - "GDP_MD_EST": 7892, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BI", - "ISO_A3": "BDI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 62, - 63, - 64, - 65, - 66 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belgium", - "NAME_LONG": "Belgium", - "ABBREV": "Belg.", - "FORMAL_EN": "Kingdom of Belgium", - "POP_EST": 11491346, - "POP_RANK": 14, - "GDP_MD_EST": 508600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BE", - "ISO_A3": "BEL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 67, - 68, - 69, - 70, - 71 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Benin", - "NAME_LONG": "Benin", - "ABBREV": "Benin", - "FORMAL_EN": "Republic of Benin", - "POP_EST": 11038805, - "POP_RANK": 14, - "GDP_MD_EST": 24310, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BJ", - "ISO_A3": "BEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -70, - 72, - 73, - 74, - 75, - 76 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burkina Faso", - "NAME_LONG": "Burkina Faso", - "ABBREV": "B.F.", - "FORMAL_EN": "Burkina Faso", - "POP_EST": 20107509, - "POP_RANK": 15, - "GDP_MD_EST": 32990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BF", - "ISO_A3": "BFA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 77, - 78, - 79 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bangladesh", - "NAME_LONG": "Bangladesh", - "ABBREV": "Bang.", - "FORMAL_EN": "People's Republic of Bangladesh", - "POP_EST": 157826578, - "POP_RANK": 17, - "GDP_MD_EST": 628400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BD", - "ISO_A3": "BGD", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 80, - 81, - 82, - 83, - 84, - 85 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bulgaria", - "NAME_LONG": "Bulgaria", - "ABBREV": "Bulg.", - "FORMAL_EN": "Republic of Bulgaria", - "POP_EST": 7101510, - "POP_RANK": 13, - "GDP_MD_EST": 143100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BG", - "ISO_A3": "BGR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 86 - ] - ], - [ - [ - 87 - ] - ], - [ - [ - 88 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Bahamas", - "NAME_LONG": "Bahamas", - "ABBREV": "Bhs.", - "FORMAL_EN": "Commonwealth of the Bahamas", - "POP_EST": 329988, - "POP_RANK": 10, - "GDP_MD_EST": 9066, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BS", - "ISO_A3": "BHS", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 89, - 90, - 91 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bosnia and Herz.", - "NAME_LONG": "Bosnia and Herzegovina", - "ABBREV": "B.H.", - "FORMAL_EN": "Bosnia and Herzegovina", - "POP_EST": 3856181, - "POP_RANK": 12, - "GDP_MD_EST": 42530, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BA", - "ISO_A3": "BIH", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 92, - 93, - 94, - 95, - 96 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belarus", - "NAME_LONG": "Belarus", - "ABBREV": "Bela.", - "FORMAL_EN": "Republic of Belarus", - "POP_EST": 9549747, - "POP_RANK": 13, - "GDP_MD_EST": 165400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BY", - "ISO_A3": "BLR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 97, - 98, - 99 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belize", - "NAME_LONG": "Belize", - "ABBREV": "Belize", - "FORMAL_EN": "Belize", - "POP_EST": 360346, - "POP_RANK": 10, - "GDP_MD_EST": 3088, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BZ", - "ISO_A3": "BLZ", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -27, - 100, - 101, - 102, - 103 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bolivia", - "NAME_LONG": "Bolivia", - "ABBREV": "Bolivia", - "FORMAL_EN": "Plurinational State of Bolivia", - "POP_EST": 11138234, - "POP_RANK": 14, - "GDP_MD_EST": 78350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BO", - "ISO_A3": "BOL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -29, - 104, - -103, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brazil", - "NAME_LONG": "Brazil", - "ABBREV": "Brazil", - "FORMAL_EN": "Federative Republic of Brazil", - "POP_EST": 207353391, - "POP_RANK": 17, - "GDP_MD_EST": 3081000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BR", - "ISO_A3": "BRA", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 113, - 114 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brunei", - "NAME_LONG": "Brunei Darussalam", - "ABBREV": "Brunei", - "FORMAL_EN": "Negara Brunei Darussalam", - "POP_EST": 443593, - "POP_RANK": 10, - "GDP_MD_EST": 33730, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BN", - "ISO_A3": "BRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 115, - 116 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bhutan", - "NAME_LONG": "Bhutan", - "ABBREV": "Bhutan", - "FORMAL_EN": "Kingdom of Bhutan", - "POP_EST": 758288, - "POP_RANK": 11, - "GDP_MD_EST": 6432, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BT", - "ISO_A3": "BTN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 117, - 118, - 119, - 120 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Botswana", - "NAME_LONG": "Botswana", - "ABBREV": "Bwa.", - "FORMAL_EN": "Republic of Botswana", - "POP_EST": 2214858, - "POP_RANK": 12, - "GDP_MD_EST": 35900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BW", - "ISO_A3": "BWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 121, - 122, - 123, - 124, - 125, - 126 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Central African Rep.", - "NAME_LONG": "Central African Republic", - "ABBREV": "C.A.R.", - "FORMAL_EN": "Central African Republic", - "POP_EST": 5625118, - "POP_RANK": 13, - "GDP_MD_EST": 3206, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CF", - "ISO_A3": "CAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 127 - ] - ], - [ - [ - 128 - ] - ], - [ - [ - 129 - ] - ], - [ - [ - 130 - ] - ], - [ - [ - 131 - ] - ], - [ - [ - 132 - ] - ], - [ - [ - 133 - ] - ], - [ - [ - 134 - ] - ], - [ - [ - 135 - ] - ], - [ - [ - 136 - ] - ], - [ - [ - 137, - 138, - 139, - 140 - ] - ], - [ - [ - 141 - ] - ], - [ - [ - 142 - ] - ], - [ - [ - 143 - ] - ], - [ - [ - 144 - ] - ], - [ - [ - 145 - ] - ], - [ - [ - 146 - ] - ], - [ - [ - 147 - ] - ], - [ - [ - 148 - ] - ], - [ - [ - 149 - ] - ], - [ - [ - 150 - ] - ], - [ - [ - 151 - ] - ], - [ - [ - 152 - ] - ], - [ - [ - 153 - ] - ], - [ - [ - 154 - ] - ], - [ - [ - 155 - ] - ], - [ - [ - 156 - ] - ], - [ - [ - 157 - ] - ], - [ - [ - 158 - ] - ], - [ - [ - 159 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Canada", - "NAME_LONG": "Canada", - "ABBREV": "Can.", - "FORMAL_EN": "Canada", - "POP_EST": 35623680, - "POP_RANK": 15, - "GDP_MD_EST": 1674000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CA", - "ISO_A3": "CAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -51, - 160, - 161, - 162 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Switzerland", - "NAME_LONG": "Switzerland", - "ABBREV": "Switz.", - "FORMAL_EN": "Swiss Confederation", - "POP_EST": 8236303, - "POP_RANK": 13, - "GDP_MD_EST": 496300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CH", - "ISO_A3": "CHE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -24, - 163 - ] - ], - [ - [ - -26, - 164, - 165, - -101 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Chile", - "NAME_LONG": "Chile", - "ABBREV": "Chile", - "FORMAL_EN": "Republic of Chile", - "POP_EST": 17789267, - "POP_RANK": 14, - "GDP_MD_EST": 436100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CL", - "ISO_A3": "CHL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -4, - 166, - 167, - 168, - 169, - 170, - 171, - 172, - 173, - 174, - 175, - 176, - 177, - -117, - 178, - 179, - 180, - 181 - ] - ], - [ - [ - 182 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "China", - "NAME_LONG": "China", - "ABBREV": "China", - "FORMAL_EN": "People's Republic of China", - "POP_EST": 1379302771, - "POP_RANK": 18, - "GDP_MD_EST": 21140000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CN", - "ISO_A3": "CHN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -75, - 183, - 184, - 185, - 186, - 187 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Côte d'Ivoire", - "NAME_LONG": "Côte d'Ivoire", - "ABBREV": "I.C.", - "FORMAL_EN": "Republic of Ivory Coast", - "POP_EST": 24184810, - "POP_RANK": 15, - "GDP_MD_EST": 87120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CI", - "ISO_A3": "CIV", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -127, - 188, - 189, - 190, - 191, - 192, - 193, - 194 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cameroon", - "NAME_LONG": "Cameroon", - "ABBREV": "Cam.", - "FORMAL_EN": "Republic of Cameroon", - "POP_EST": 24994885, - "POP_RANK": 15, - "GDP_MD_EST": 77240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CM", - "ISO_A3": "CMR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -9, - 195, - -13, - 196, - -125, - 197, - 198, - 199, - -60, - 200, - 201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dem. Rep. Congo", - "NAME_LONG": "Democratic Republic of the Congo", - "ABBREV": "D.R.C.", - "FORMAL_EN": "Democratic Republic of the Congo", - "POP_EST": 83301151, - "POP_RANK": 16, - "GDP_MD_EST": 66010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CD", - "ISO_A3": "COD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -12, - 202, - 203, - -189, - -126, - -197 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Congo", - "NAME_LONG": "Republic of the Congo", - "ABBREV": "Rep. Congo", - "FORMAL_EN": "Republic of the Congo", - "POP_EST": 4954674, - "POP_RANK": 12, - "GDP_MD_EST": 30270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CG", - "ISO_A3": "COG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -107, - 204, - 205, - 206, - 207, - 208, - 209 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Colombia", - "NAME_LONG": "Colombia", - "ABBREV": "Col.", - "FORMAL_EN": "Republic of Colombia", - "POP_EST": 47698524, - "POP_RANK": 15, - "GDP_MD_EST": 688000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CO", - "ISO_A3": "COL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 210, - 211, - 212, - 213 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Costa Rica", - "NAME_LONG": "Costa Rica", - "ABBREV": "C.R.", - "FORMAL_EN": "Republic of Costa Rica", - "POP_EST": 4930258, - "POP_RANK": 12, - "GDP_MD_EST": 79260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CR", - "ISO_A3": "CRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - 214 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cuba", - "NAME_LONG": "Cuba", - "ABBREV": "Cuba", - "FORMAL_EN": "Republic of Cuba", - "POP_EST": 11147407, - "POP_RANK": 14, - "GDP_MD_EST": 132900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CU", - "ISO_A3": "CUB", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 215, - 216 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "N. Cyprus", - "NAME_LONG": "Northern Cyprus", - "ABBREV": "N. Cy.", - "FORMAL_EN": "Turkish Republic of Northern Cyprus", - "POP_EST": 265100, - "POP_RANK": 10, - "GDP_MD_EST": 3600, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -217, - 217 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cyprus", - "NAME_LONG": "Cyprus", - "ABBREV": "Cyp.", - "FORMAL_EN": "Republic of Cyprus", - "POP_EST": 1221549, - "POP_RANK": 12, - "GDP_MD_EST": 29260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CY", - "ISO_A3": "CYP", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -53, - 218, - 219, - 220 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Czechia", - "NAME_LONG": "Czech Republic", - "ABBREV": "Cz.", - "FORMAL_EN": "Czech Republic", - "POP_EST": 10674723, - "POP_RANK": 14, - "GDP_MD_EST": 350900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CZ", - "ISO_A3": "CZE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -52, - -163, - 221, - 222, - -63, - 223, - 224, - 225, - 226, - 227, - -219 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Germany", - "NAME_LONG": "Germany", - "ABBREV": "Ger.", - "FORMAL_EN": "Federal Republic of Germany", - "POP_EST": 80594017, - "POP_RANK": 16, - "GDP_MD_EST": 3979000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DE", - "ISO_A3": "DEU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 228, - 229, - 230, - 231 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Djibouti", - "NAME_LONG": "Djibouti", - "ABBREV": "Dji.", - "FORMAL_EN": "Republic of Djibouti", - "POP_EST": 865267, - "POP_RANK": 11, - "GDP_MD_EST": 3345, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DJ", - "ISO_A3": "DJI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -226, - 232 - ] - ], - [ - [ - 233 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Denmark", - "NAME_LONG": "Denmark", - "ABBREV": "Den.", - "FORMAL_EN": "Kingdom of Denmark", - "POP_EST": 5605948, - "POP_RANK": 13, - "GDP_MD_EST": 264800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DK", - "ISO_A3": "DNK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 234, - 235 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dominican Rep.", - "NAME_LONG": "Dominican Republic", - "ABBREV": "Dom. Rep.", - "FORMAL_EN": "Dominican Republic", - "POP_EST": 10734247, - "POP_RANK": 14, - "GDP_MD_EST": 161900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DO", - "ISO_A3": "DOM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 236, - 237, - 238, - 239, - 240, - 241, - 242, - 243 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Algeria", - "NAME_LONG": "Algeria", - "ABBREV": "Alg.", - "FORMAL_EN": "People's Democratic Republic of Algeria", - "POP_EST": 40969443, - "POP_RANK": 15, - "GDP_MD_EST": 609400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DZ", - "ISO_A3": "DZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -206, - 244, - 245 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ecuador", - "NAME_LONG": "Ecuador", - "ABBREV": "Ecu.", - "FORMAL_EN": "Republic of Ecuador", - "POP_EST": 16290913, - "POP_RANK": 14, - "GDP_MD_EST": 182400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EC", - "ISO_A3": "ECU", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 246, - 247, - 248, - 249, - 250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Egypt", - "NAME_LONG": "Egypt", - "ABBREV": "Egypt", - "FORMAL_EN": "Arab Republic of Egypt", - "POP_EST": 97041072, - "POP_RANK": 16, - "GDP_MD_EST": 1105000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EG", - "ISO_A3": "EGY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -232, - 251, - 252, - 253 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eritrea", - "NAME_LONG": "Eritrea", - "ABBREV": "Erit.", - "FORMAL_EN": "State of Eritrea", - "POP_EST": 5918919, - "POP_RANK": 13, - "GDP_MD_EST": 9169, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ER", - "ISO_A3": "ERI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 254, - 255, - 256, - 257 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Spain", - "NAME_LONG": "Spain", - "ABBREV": "Sp.", - "FORMAL_EN": "Kingdom of Spain", - "POP_EST": 48958159, - "POP_RANK": 15, - "GDP_MD_EST": 1690000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ES", - "ISO_A3": "ESP", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 258, - 259, - 260 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Estonia", - "NAME_LONG": "Estonia", - "ABBREV": "Est.", - "FORMAL_EN": "Republic of Estonia", - "POP_EST": 1251581, - "POP_RANK": 12, - "GDP_MD_EST": 38700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EE", - "ISO_A3": "EST", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -231, - 261, - 262, - 263, - 264, - 265, - -252 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ethiopia", - "NAME_LONG": "Ethiopia", - "ABBREV": "Eth.", - "FORMAL_EN": "Federal Democratic Republic of Ethiopia", - "POP_EST": 105350020, - "POP_RANK": 17, - "GDP_MD_EST": 174700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ET", - "ISO_A3": "ETH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 266, - 267, - 268, - 269 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Finland", - "NAME_LONG": "Finland", - "ABBREV": "Fin.", - "FORMAL_EN": "Republic of Finland", - "POP_EST": 5491218, - "POP_RANK": 13, - "GDP_MD_EST": 224137, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FI", - "ISO_A3": "FIN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - [ - 270 - ] - ], - [ - [ - 271 - ] - ], - [ - [ - 272 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Fiji", - "NAME_LONG": "Fiji", - "ABBREV": "Fiji", - "FORMAL_EN": "Republic of Fiji", - "POP_EST": 920938, - "POP_RANK": 11, - "GDP_MD_EST": 8374, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FJ", - "ISO_A3": "FJI", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - 273 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Falkland Is.", - "NAME_LONG": "Falkland Islands", - "ABBREV": "Flk. Is.", - "FORMAL_EN": "Falkland Islands", - "POP_EST": 2931, - "POP_RANK": 4, - "GDP_MD_EST": 281.8, - "POP_YEAR": 2014, - "GDP_YEAR": 2012, - "ISO_A2": "FK", - "ISO_A3": "FLK", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -65, - 274, - -222, - -162, - 275, - 276, - -256, - 277 - ] - ], - [ - [ - -111, - 278, - 279 - ] - ], - [ - [ - 280 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "France", - "NAME_LONG": "France", - "ABBREV": "Fr.", - "FORMAL_EN": "French Republic", - "POP_EST": 67106161, - "POP_RANK": 16, - "GDP_MD_EST": 2699000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FR", - "ISO_A3": "FRA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -190, - -204, - 281, - 282 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gabon", - "NAME_LONG": "Gabon", - "ABBREV": "Gabon", - "FORMAL_EN": "Gabonese Republic", - "POP_EST": 1772255, - "POP_RANK": 12, - "GDP_MD_EST": 35980, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GA", - "ISO_A3": "GAB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 283, - 284 - ] - ], - [ - [ - 285 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United Kingdom", - "NAME_LONG": "United Kingdom", - "ABBREV": "U.K.", - "FORMAL_EN": "United Kingdom of Great Britain and Northern Ireland", - "POP_EST": 64769452, - "POP_RANK": 16, - "GDP_MD_EST": 2788000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GB", - "ISO_A3": "GBR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -32, - 286, - 287, - 288, - -55 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Georgia", - "NAME_LONG": "Georgia", - "ABBREV": "Geo.", - "FORMAL_EN": "Georgia", - "POP_EST": 4926330, - "POP_RANK": 12, - "GDP_MD_EST": 37270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GE", - "ISO_A3": "GEO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -74, - 289, - 290, - -184 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ghana", - "NAME_LONG": "Ghana", - "ABBREV": "Ghana", - "FORMAL_EN": "Republic of Ghana", - "POP_EST": 27499924, - "POP_RANK": 15, - "GDP_MD_EST": 120800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GH", - "ISO_A3": "GHA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -187, - 291, - 292, - 293, - 294, - 295, - 296 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea", - "NAME_LONG": "Guinea", - "ABBREV": "Gin.", - "FORMAL_EN": "Republic of Guinea", - "POP_EST": 12413867, - "POP_RANK": 14, - "GDP_MD_EST": 16080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GN", - "ISO_A3": "GIN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 297, - 298 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gambia", - "NAME_LONG": "The Gambia", - "ABBREV": "Gambia", - "FORMAL_EN": "Republic of the Gambia", - "POP_EST": 2051363, - "POP_RANK": 12, - "GDP_MD_EST": 3387, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GM", - "ISO_A3": "GMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -295, - 299, - 300 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea-Bissau", - "NAME_LONG": "Guinea-Bissau", - "ABBREV": "GnB.", - "FORMAL_EN": "Republic of Guinea-Bissau", - "POP_EST": 1792338, - "POP_RANK": 12, - "GDP_MD_EST": 2851, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GW", - "ISO_A3": "GNB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -191, - -283, - 301 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eq. Guinea", - "NAME_LONG": "Equatorial Guinea", - "ABBREV": "Eq. G.", - "FORMAL_EN": "Republic of Equatorial Guinea", - "POP_EST": 778358, - "POP_RANK": 11, - "GDP_MD_EST": 31770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GQ", - "ISO_A3": "GNQ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - -14, - 302, - -84, - 303, - 304 - ] - ], - [ - [ - 305 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Greece", - "NAME_LONG": "Greece", - "ABBREV": "Greece", - "FORMAL_EN": "Hellenic Republic", - "POP_EST": 10768477, - "POP_RANK": 14, - "GDP_MD_EST": 290500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GR", - "ISO_A3": "GRC", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 306 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Greenland", - "NAME_LONG": "Greenland", - "ABBREV": "Grlnd.", - "FORMAL_EN": "Greenland", - "POP_EST": 57713, - "POP_RANK": 8, - "GDP_MD_EST": 2173, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "GL", - "ISO_A3": "GRL", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -100, - 307, - 308, - 309, - 310, - 311 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guatemala", - "NAME_LONG": "Guatemala", - "ABBREV": "Guat.", - "FORMAL_EN": "Republic of Guatemala", - "POP_EST": 15460732, - "POP_RANK": 14, - "GDP_MD_EST": 131800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GT", - "ISO_A3": "GTM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -109, - 312, - 313, - 314 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guyana", - "NAME_LONG": "Guyana", - "ABBREV": "Guy.", - "FORMAL_EN": "Co-operative Republic of Guyana", - "POP_EST": 737718, - "POP_RANK": 11, - "GDP_MD_EST": 6093, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GY", - "ISO_A3": "GUY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -309, - 315, - 316, - 317, - 318 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Honduras", - "NAME_LONG": "Honduras", - "ABBREV": "Hond.", - "FORMAL_EN": "Republic of Honduras", - "POP_EST": 9038741, - "POP_RANK": 13, - "GDP_MD_EST": 43190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HN", - "ISO_A3": "HND", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -91, - 319, - 320, - 321, - 322, - 323 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Croatia", - "NAME_LONG": "Croatia", - "ABBREV": "Cro.", - "FORMAL_EN": "Republic of Croatia", - "POP_EST": 4292095, - "POP_RANK": 12, - "GDP_MD_EST": 94240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HR", - "ISO_A3": "HRV", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -236, - 324 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Haiti", - "NAME_LONG": "Haiti", - "ABBREV": "Haiti", - "FORMAL_EN": "Republic of Haiti", - "POP_EST": 10646714, - "POP_RANK": 14, - "GDP_MD_EST": 19340, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HT", - "ISO_A3": "HTI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -48, - 325, - 326, - 327, - 328, - -323, - 329 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Hungary", - "NAME_LONG": "Hungary", - "ABBREV": "Hun.", - "FORMAL_EN": "Republic of Hungary", - "POP_EST": 9850845, - "POP_RANK": 13, - "GDP_MD_EST": 267600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HU", - "ISO_A3": "HUN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 330 - ] - ], - [ - [ - 331, - 332 - ] - ], - [ - [ - 333 - ] - ], - [ - [ - 334 - ] - ], - [ - [ - 335 - ] - ], - [ - [ - 336 - ] - ], - [ - [ - 337 - ] - ], - [ - [ - 338 - ] - ], - [ - [ - 339, - 340 - ] - ], - [ - [ - 341 - ] - ], - [ - [ - 342 - ] - ], - [ - [ - 343, - 344 - ] - ], - [ - [ - 345 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Indonesia", - "NAME_LONG": "Indonesia", - "ABBREV": "Indo.", - "FORMAL_EN": "Republic of Indonesia", - "POP_EST": 260580739, - "POP_RANK": 17, - "GDP_MD_EST": 3028000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ID", - "ISO_A3": "IDN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -80, - 346, - 347, - -181, - 348, - -179, - -116, - -178, - 349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "India", - "NAME_LONG": "India", - "ABBREV": "India", - "FORMAL_EN": "Republic of India", - "POP_EST": 1281935911, - "POP_RANK": 18, - "GDP_MD_EST": 8721000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IN", - "ISO_A3": "IND", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -284, - 350 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ireland", - "NAME_LONG": "Ireland", - "ABBREV": "Ire.", - "FORMAL_EN": "Ireland", - "POP_EST": 5011102, - "POP_RANK": 13, - "GDP_MD_EST": 322000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IE", - "ISO_A3": "IRL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -6, - 351, - 352, - 353, - 354, - -59, - -34, - -58, - 355, - 356 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iran", - "NAME_LONG": "Iran", - "ABBREV": "Iran", - "FORMAL_EN": "Islamic Republic of Iran", - "POP_EST": 82021564, - "POP_RANK": 16, - "GDP_MD_EST": 1459000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IR", - "ISO_A3": "IRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -354, - 357, - 358, - 359, - 360, - 361, - 362 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iraq", - "NAME_LONG": "Iraq", - "ABBREV": "Iraq", - "FORMAL_EN": "Republic of Iraq", - "POP_EST": 39192111, - "POP_RANK": 15, - "GDP_MD_EST": 596700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IQ", - "ISO_A3": "IRQ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 363 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iceland", - "NAME_LONG": "Iceland", - "ABBREV": "Iceland", - "FORMAL_EN": "Republic of Iceland", - "POP_EST": 339747, - "POP_RANK": 10, - "GDP_MD_EST": 16150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IS", - "ISO_A3": "ISL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 364, - 365, - 366, - 367, - 368, - 369, - -250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Israel", - "NAME_LONG": "Israel", - "ABBREV": "Isr.", - "FORMAL_EN": "State of Israel", - "POP_EST": 8299706, - "POP_RANK": 13, - "GDP_MD_EST": 297000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IL", - "ISO_A3": "ISR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - -50, - 370, - 371, - -276, - -161 - ] - ], - [ - [ - 372 - ] - ], - [ - [ - 373 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Italy", - "NAME_LONG": "Italy", - "ABBREV": "Italy", - "FORMAL_EN": "Italian Republic", - "POP_EST": 62137802, - "POP_RANK": 16, - "GDP_MD_EST": 2221000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IT", - "ISO_A3": "ITA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 374 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jamaica", - "NAME_LONG": "Jamaica", - "ABBREV": "Jam.", - "FORMAL_EN": "Jamaica", - "POP_EST": 2990561, - "POP_RANK": 12, - "GDP_MD_EST": 25390, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JM", - "ISO_A3": "JAM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -361, - 375, - 376, - -370, - 377, - -368, - 378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jordan", - "NAME_LONG": "Jordan", - "ABBREV": "Jord.", - "FORMAL_EN": "Hashemite Kingdom of Jordan", - "POP_EST": 10248069, - "POP_RANK": 14, - "GDP_MD_EST": 86190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JO", - "ISO_A3": "JOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 379 - ] - ], - [ - [ - 380 - ] - ], - [ - [ - 381 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Japan", - "NAME_LONG": "Japan", - "ABBREV": "Japan", - "FORMAL_EN": "Japan", - "POP_EST": 126451398, - "POP_RANK": 17, - "GDP_MD_EST": 4932000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JP", - "ISO_A3": "JPN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -169, - 382, - 383, - 384, - 385, - 386 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kazakhstan", - "NAME_LONG": "Kazakhstan", - "ABBREV": "Kaz.", - "FORMAL_EN": "Republic of Kazakhstan", - "POP_EST": 18556698, - "POP_RANK": 14, - "GDP_MD_EST": 460700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KZ", - "ISO_A3": "KAZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -264, - 387, - 388, - 389, - 390, - 391 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kenya", - "NAME_LONG": "Kenya", - "ABBREV": "Ken.", - "FORMAL_EN": "Republic of Kenya", - "POP_EST": 47615739, - "POP_RANK": 15, - "GDP_MD_EST": 152700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KE", - "ISO_A3": "KEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -168, - 392, - 393, - -383 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kyrgyzstan", - "NAME_LONG": "Kyrgyzstan", - "ABBREV": "Kgz.", - "FORMAL_EN": "Kyrgyz Republic", - "POP_EST": 5789122, - "POP_RANK": 13, - "GDP_MD_EST": 21010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KG", - "ISO_A3": "KGZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - 394, - 395, - 396, - 397 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cambodia", - "NAME_LONG": "Cambodia", - "ABBREV": "Camb.", - "FORMAL_EN": "Kingdom of Cambodia", - "POP_EST": 16204486, - "POP_RANK": 14, - "GDP_MD_EST": 58940, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KH", - "ISO_A3": "KHM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 398, - 399 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Korea", - "NAME_LONG": "Republic of Korea", - "ABBREV": "S.K.", - "FORMAL_EN": "Republic of Korea", - "POP_EST": 51181299, - "POP_RANK": 16, - "GDP_MD_EST": 1929000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KR", - "ISO_A3": "KOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -17, - 400, - 401, - 402 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kosovo", - "NAME_LONG": "Kosovo", - "ABBREV": "Kos.", - "FORMAL_EN": "Republic of Kosovo", - "POP_EST": 1895250, - "POP_RANK": 12, - "GDP_MD_EST": 18490, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "XK", - "ISO_A3": "-99", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -359, - 403, - 404 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kuwait", - "NAME_LONG": "Kuwait", - "ABBREV": "Kwt.", - "FORMAL_EN": "State of Kuwait", - "POP_EST": 2875422, - "POP_RANK": 12, - "GDP_MD_EST": 301100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KW", - "ISO_A3": "KWT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -176, - 405, - -396, - 406, - 407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Laos", - "NAME_LONG": "Lao PDR", - "ABBREV": "Laos", - "FORMAL_EN": "Lao People's Democratic Republic", - "POP_EST": 7126706, - "POP_RANK": 13, - "GDP_MD_EST": 40960, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LA", - "ISO_A3": "LAO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -366, - 408, - 409 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lebanon", - "NAME_LONG": "Lebanon", - "ABBREV": "Leb.", - "FORMAL_EN": "Lebanese Republic", - "POP_EST": 6229794, - "POP_RANK": 13, - "GDP_MD_EST": 85160, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LB", - "ISO_A3": "LBN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -186, - 410, - 411, - -292 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Liberia", - "NAME_LONG": "Liberia", - "ABBREV": "Liberia", - "FORMAL_EN": "Republic of Liberia", - "POP_EST": 4689021, - "POP_RANK": 12, - "GDP_MD_EST": 3881, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LR", - "ISO_A3": "LBR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -243, - 412, - 413, - -248, - 414, - 415, - 416 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Libya", - "NAME_LONG": "Libya", - "ABBREV": "Libya", - "FORMAL_EN": "Libya", - "POP_EST": 6653210, - "POP_RANK": 13, - "GDP_MD_EST": 90890, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LY", - "ISO_A3": "LBY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 417 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sri Lanka", - "NAME_LONG": "Sri Lanka", - "ABBREV": "Sri L.", - "FORMAL_EN": "Democratic Socialist Republic of Sri Lanka", - "POP_EST": 22409381, - "POP_RANK": 15, - "GDP_MD_EST": 236700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LK", - "ISO_A3": "LKA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 418 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lesotho", - "NAME_LONG": "Lesotho", - "ABBREV": "Les.", - "FORMAL_EN": "Kingdom of Lesotho", - "POP_EST": 1958042, - "POP_RANK": 12, - "GDP_MD_EST": 6019, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LS", - "ISO_A3": "LSO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -93, - 419, - 420, - 421, - 422 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lithuania", - "NAME_LONG": "Lithuania", - "ABBREV": "Lith.", - "FORMAL_EN": "Republic of Lithuania", - "POP_EST": 2823859, - "POP_RANK": 12, - "GDP_MD_EST": 85620, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LT", - "ISO_A3": "LTU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -64, - -223, - -275 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Luxembourg", - "NAME_LONG": "Luxembourg", - "ABBREV": "Lux.", - "FORMAL_EN": "Grand Duchy of Luxembourg", - "POP_EST": 594130, - "POP_RANK": 11, - "GDP_MD_EST": 58740, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LU", - "ISO_A3": "LUX", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -94, - -423, - 423, - -261, - 424 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Latvia", - "NAME_LONG": "Latvia", - "ABBREV": "Lat.", - "FORMAL_EN": "Republic of Latvia", - "POP_EST": 1944643, - "POP_RANK": 12, - "GDP_MD_EST": 50650, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LV", - "ISO_A3": "LVA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -240, - 425, - 426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Morocco", - "NAME_LONG": "Morocco", - "ABBREV": "Mor.", - "FORMAL_EN": "Kingdom of Morocco", - "POP_EST": 33986655, - "POP_RANK": 15, - "GDP_MD_EST": 282800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MA", - "ISO_A3": "MAR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 427, - 428 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Moldova", - "NAME_LONG": "Moldova", - "ABBREV": "Mda.", - "FORMAL_EN": "Republic of Moldova", - "POP_EST": 3474121, - "POP_RANK": 12, - "GDP_MD_EST": 18540, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MD", - "ISO_A3": "MDA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 429 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Madagascar", - "NAME_LONG": "Madagascar", - "ABBREV": "Mad.", - "FORMAL_EN": "Republic of Madagascar", - "POP_EST": 25054161, - "POP_RANK": 15, - "GDP_MD_EST": 36860, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MG", - "ISO_A3": "MDG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -98, - -312, - 430, - 431, - 432 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mexico", - "NAME_LONG": "Mexico", - "ABBREV": "Mex.", - "FORMAL_EN": "United Mexican States", - "POP_EST": 124574795, - "POP_RANK": 17, - "GDP_MD_EST": 2307000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MX", - "ISO_A3": "MEX", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -18, - -403, - 433, - -85, - -303 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Macedonia", - "NAME_LONG": "Macedonia", - "ABBREV": "Mkd.", - "FORMAL_EN": "Former Yugoslav Republic of Macedonia", - "POP_EST": 2103721, - "POP_RANK": 12, - "GDP_MD_EST": 29520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MK", - "ISO_A3": "MKD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -76, - -188, - -297, - 434, - 435, - -237, - 436 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mali", - "NAME_LONG": "Mali", - "ABBREV": "Mali", - "FORMAL_EN": "Republic of Mali", - "POP_EST": 17885245, - "POP_RANK": 14, - "GDP_MD_EST": 38090, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ML", - "ISO_A3": "MLI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -78, - -350, - -177, - -408, - 437, - 438 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Myanmar", - "NAME_LONG": "Myanmar", - "ABBREV": "Myan.", - "FORMAL_EN": "Republic of the Union of Myanmar", - "POP_EST": 55123814, - "POP_RANK": 16, - "GDP_MD_EST": 311100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MM", - "ISO_A3": "MMR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -16, - 439, - -320, - -90, - 440, - -401 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Montenegro", - "NAME_LONG": "Montenegro", - "ABBREV": "Mont.", - "FORMAL_EN": "Montenegro", - "POP_EST": 642550, - "POP_RANK": 11, - "GDP_MD_EST": 10610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ME", - "ISO_A3": "MNE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -171, - 441 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mongolia", - "NAME_LONG": "Mongolia", - "ABBREV": "Mong.", - "FORMAL_EN": "Mongolia", - "POP_EST": 3068243, - "POP_RANK": 12, - "GDP_MD_EST": 37000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MN", - "ISO_A3": "MNG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - 442, - 443, - 444, - 445, - 446, - 447, - 448, - 449 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mozambique", - "NAME_LONG": "Mozambique", - "ABBREV": "Moz.", - "FORMAL_EN": "Republic of Mozambique", - "POP_EST": 26573706, - "POP_RANK": 15, - "GDP_MD_EST": 35010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MZ", - "ISO_A3": "MOZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -238, - -436, - 450, - 451, - 452 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mauritania", - "NAME_LONG": "Mauritania", - "ABBREV": "Mrt.", - "FORMAL_EN": "Islamic Republic of Mauritania", - "POP_EST": 3758571, - "POP_RANK": 12, - "GDP_MD_EST": 16710, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MR", - "ISO_A3": "MRT", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -450, - 453, - 454 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Malawi", - "NAME_LONG": "Malawi", - "ABBREV": "Mal.", - "FORMAL_EN": "Republic of Malawi", - "POP_EST": 19196246, - "POP_RANK": 14, - "GDP_MD_EST": 21200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MW", - "ISO_A3": "MWI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -115, - 455, - -344, - 456 - ] - ], - [ - [ - 457, - 458 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Malaysia", - "NAME_LONG": "Malaysia", - "ABBREV": "Malay.", - "FORMAL_EN": "Malaysia", - "POP_EST": 31381992, - "POP_RANK": 15, - "GDP_MD_EST": 863000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MY", - "ISO_A3": "MYS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -7, - 459, - -119, - 460, - 461 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Namibia", - "NAME_LONG": "Namibia", - "ABBREV": "Nam.", - "FORMAL_EN": "Republic of Namibia", - "POP_EST": 2484780, - "POP_RANK": 12, - "GDP_MD_EST": 25990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NA", - "ISO_A3": "NAM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 462 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "New Caledonia", - "NAME_LONG": "New Caledonia", - "ABBREV": "New C.", - "FORMAL_EN": "New Caledonia", - "POP_EST": 279070, - "POP_RANK": 10, - "GDP_MD_EST": 10770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NC", - "ISO_A3": "NCL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -71, - -77, - -437, - -244, - -417, - 463, - -194, - 464 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Niger", - "NAME_LONG": "Niger", - "ABBREV": "Niger", - "FORMAL_EN": "Republic of Niger", - "POP_EST": 19245344, - "POP_RANK": 14, - "GDP_MD_EST": 20150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NE", - "ISO_A3": "NER", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -72, - -465, - -193, - 465 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nigeria", - "NAME_LONG": "Nigeria", - "ABBREV": "Nigeria", - "FORMAL_EN": "Federal Republic of Nigeria", - "POP_EST": 190632261, - "POP_RANK": 17, - "GDP_MD_EST": 1089000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NG", - "ISO_A3": "NGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -212, - 466, - -317, - 467 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nicaragua", - "NAME_LONG": "Nicaragua", - "ABBREV": "Nic.", - "FORMAL_EN": "Republic of Nicaragua", - "POP_EST": 6025951, - "POP_RANK": 13, - "GDP_MD_EST": 33550, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NI", - "ISO_A3": "NIC", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -67, - 468, - -224 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Netherlands", - "NAME_LONG": "Netherlands", - "ABBREV": "Neth.", - "FORMAL_EN": "Kingdom of the Netherlands", - "POP_EST": 17084719, - "POP_RANK": 14, - "GDP_MD_EST": 870800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NL", - "ISO_A3": "NLD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -268, - 469, - 470, - 471 - ] - ], - [ - [ - 472 - ] - ], - [ - [ - 473 - ] - ], - [ - [ - 474 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Norway", - "NAME_LONG": "Norway", - "ABBREV": "Nor.", - "FORMAL_EN": "Kingdom of Norway", - "POP_EST": 5320045, - "POP_RANK": 13, - "GDP_MD_EST": 364700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NO", - "ISO_A3": "NOR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -180, - -349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nepal", - "NAME_LONG": "Nepal", - "ABBREV": "Nepal", - "FORMAL_EN": "Nepal", - "POP_EST": 29384297, - "POP_RANK": 15, - "GDP_MD_EST": 71520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NP", - "ISO_A3": "NPL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 475 - ] - ], - [ - [ - 476 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "New Zealand", - "NAME_LONG": "New Zealand", - "ABBREV": "N.Z.", - "FORMAL_EN": "New Zealand", - "POP_EST": 4510327, - "POP_RANK": 12, - "GDP_MD_EST": 174800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NZ", - "ISO_A3": "NZL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - [ - -20, - 477 - ] - ], - [ - [ - -22, - 478, - 479, - 480 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Oman", - "NAME_LONG": "Oman", - "ABBREV": "Oman", - "FORMAL_EN": "Sultanate of Oman", - "POP_EST": 3424386, - "POP_RANK": 12, - "GDP_MD_EST": 173100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "OM", - "ISO_A3": "OMN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -5, - -182, - -348, - 481, - -352 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Pakistan", - "NAME_LONG": "Pakistan", - "ABBREV": "Pak.", - "FORMAL_EN": "Islamic Republic of Pakistan", - "POP_EST": 204924861, - "POP_RANK": 17, - "GDP_MD_EST": 988200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PK", - "ISO_A3": "PAK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -208, - 482, - -214, - 483 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Panama", - "NAME_LONG": "Panama", - "ABBREV": "Pan.", - "FORMAL_EN": "Republic of Panama", - "POP_EST": 3753142, - "POP_RANK": 12, - "GDP_MD_EST": 93120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PA", - "ISO_A3": "PAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -102, - -166, - 484, - -245, - -205, - -106 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Peru", - "NAME_LONG": "Peru", - "ABBREV": "Peru", - "FORMAL_EN": "Republic of Peru", - "POP_EST": 31036656, - "POP_RANK": 15, - "GDP_MD_EST": 410400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PE", - "ISO_A3": "PER", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - 485 - ] - ], - [ - [ - 486 - ] - ], - [ - [ - 487 - ] - ], - [ - [ - 488 - ] - ], - [ - [ - 489 - ] - ], - [ - [ - 490 - ] - ], - [ - [ - 491 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Philippines", - "NAME_LONG": "Philippines", - "ABBREV": "Phil.", - "FORMAL_EN": "Republic of the Philippines", - "POP_EST": 104256076, - "POP_RANK": 17, - "GDP_MD_EST": 801900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PH", - "ISO_A3": "PHL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - -340, - 492 - ] - ], - [ - [ - 493 - ] - ], - [ - [ - 494 - ] - ], - [ - [ - 495 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Papua New Guinea", - "NAME_LONG": "Papua New Guinea", - "ABBREV": "P.N.G.", - "FORMAL_EN": "Independent State of Papua New Guinea", - "POP_EST": 6909701, - "POP_RANK": 13, - "GDP_MD_EST": 28020, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PG", - "ISO_A3": "PNG", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -97, - 496, - 497, - -220, - -228, - 498, - 499, - -420 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Poland", - "NAME_LONG": "Poland", - "ABBREV": "Pol.", - "FORMAL_EN": "Republic of Poland", - "POP_EST": 38476269, - "POP_RANK": 15, - "GDP_MD_EST": 1052000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PL", - "ISO_A3": "POL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 500 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Puerto Rico", - "NAME_LONG": "Puerto Rico", - "ABBREV": "P.R.", - "FORMAL_EN": "Commonwealth of Puerto Rico", - "POP_EST": 3351827, - "POP_RANK": 12, - "GDP_MD_EST": 131000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PR", - "ISO_A3": "PRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -173, - 501, - 502, - -400, - 503 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "North Korea", - "NAME_LONG": "Dem. Rep. Korea", - "ABBREV": "N.K.", - "FORMAL_EN": "Democratic People's Republic of Korea", - "POP_EST": 25248140, - "POP_RANK": 15, - "GDP_MD_EST": 40000, - "POP_YEAR": 2013, - "GDP_YEAR": 2016, - "ISO_A2": "KP", - "ISO_A3": "PRK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -258, - 504 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Portugal", - "NAME_LONG": "Portugal", - "ABBREV": "Port.", - "FORMAL_EN": "Portuguese Republic", - "POP_EST": 10839514, - "POP_RANK": 14, - "GDP_MD_EST": 297100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PT", - "ISO_A3": "PRT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -28, - -104, - -105 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Paraguay", - "NAME_LONG": "Paraguay", - "ABBREV": "Para.", - "FORMAL_EN": "Republic of Paraguay", - "POP_EST": 6943739, - "POP_RANK": 13, - "GDP_MD_EST": 64670, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PY", - "ISO_A3": "PRY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -369, - -378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Palestine", - "NAME_LONG": "Palestine", - "ABBREV": "Pal.", - "FORMAL_EN": "West Bank and Gaza", - "POP_EST": 4543126, - "POP_RANK": 12, - "GDP_MD_EST": 21220.77, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PS", - "ISO_A3": "PSE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 505, - 506 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Qatar", - "NAME_LONG": "Qatar", - "ABBREV": "Qatar", - "FORMAL_EN": "State of Qatar", - "POP_EST": 2314307, - "POP_RANK": 12, - "GDP_MD_EST": 334500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "QA", - "ISO_A3": "QAT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -81, - 507, - -328, - 508, - -429, - 509, - 510 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Romania", - "NAME_LONG": "Romania", - "ABBREV": "Rom.", - "FORMAL_EN": "Romania", - "POP_EST": 21529967, - "POP_RANK": 15, - "GDP_MD_EST": 441000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RO", - "ISO_A3": "ROU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - -56, - -289, - 511, - 512, - -95, - -425, - -260, - 513, - -269, - -472, - 514, - -502, - -172, - -442, - -170, - -387, - 515 - ] - ], - [ - [ - -421, - -500, - 516 - ] - ], - [ - [ - 519 - ] - ], - [ - [ - 520 - ] - ], - [ - [ - 521 - ] - ], - [ - [ - 522 - ] - ], - [ - [ - 523 - ] - ], - [ - [ - 524 - ] - ], - [ - [ - 525 - ] - ], - [ - [ - 526 - ] - ], - [ - [ - 527 - ] - ], - [ - [ - 528 - ] - ], - [ - [ - 529 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Russia", - "NAME_LONG": "Russian Federation", - "ABBREV": "Rus.", - "FORMAL_EN": "Russian Federation", - "POP_EST": 142257519, - "POP_RANK": 17, - "GDP_MD_EST": 3745000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RU", - "ISO_A3": "RUS", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -61, - -200, - 530, - 531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Rwanda", - "NAME_LONG": "Rwanda", - "ABBREV": "Rwa.", - "FORMAL_EN": "Republic of Rwanda", - "POP_EST": 11901484, - "POP_RANK": 14, - "GDP_MD_EST": 21970, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RW", - "ISO_A3": "RWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -239, - -453, - 532, - -426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "W. Sahara", - "NAME_LONG": "Western Sahara", - "ABBREV": "W. Sah.", - "FORMAL_EN": "Sahrawi Arab Democratic Republic", - "POP_EST": 603253, - "POP_RANK": 11, - "GDP_MD_EST": 906.5, - "POP_YEAR": 2017, - "GDP_YEAR": 2007, - "ISO_A2": "EH", - "ISO_A3": "ESH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -23, - -481, - 533, - 534, - -376, - -360, - -405, - 535, - -507, - 536 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Saudi Arabia", - "NAME_LONG": "Saudi Arabia", - "ABBREV": "Saud.", - "FORMAL_EN": "Kingdom of Saudi Arabia", - "POP_EST": 28571770, - "POP_RANK": 15, - "GDP_MD_EST": 1731000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SA", - "ISO_A3": "SAU", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -123, - 537, - -415, - -247, - 538, - -253, - -266, - 539 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sudan", - "NAME_LONG": "Sudan", - "ABBREV": "Sudan", - "FORMAL_EN": "Republic of the Sudan", - "POP_EST": 37345935, - "POP_RANK": 15, - "GDP_MD_EST": 176300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SD", - "ISO_A3": "SDN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -124, - -540, - -265, - -392, - 540, - -198 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "S. Sudan", - "NAME_LONG": "South Sudan", - "ABBREV": "S. Sud.", - "FORMAL_EN": "Republic of South Sudan", - "POP_EST": 13026129, - "POP_RANK": 14, - "GDP_MD_EST": 20880, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SS", - "ISO_A3": "SSD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -296, - -301, - 541, - -299, - 542, - -451, - -435 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Senegal", - "NAME_LONG": "Senegal", - "ABBREV": "Sen.", - "FORMAL_EN": "Republic of Senegal", - "POP_EST": 14668522, - "POP_RANK": 14, - "GDP_MD_EST": 39720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SN", - "ISO_A3": "SEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - [ - 543 - ] - ], - [ - [ - 544 - ] - ], - [ - [ - 545 - ] - ], - [ - [ - 546 - ] - ], - [ - [ - 547 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Solomon Is.", - "NAME_LONG": "Solomon Islands", - "ABBREV": "S. Is.", - "FORMAL_EN": "", - "POP_EST": 647581, - "POP_RANK": 11, - "GDP_MD_EST": 1198, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SB", - "ISO_A3": "SLB", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -293, - -412, - 548 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sierra Leone", - "NAME_LONG": "Sierra Leone", - "ABBREV": "S.L.", - "FORMAL_EN": "Republic of Sierra Leone", - "POP_EST": 6163195, - "POP_RANK": 13, - "GDP_MD_EST": 10640, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SL", - "ISO_A3": "SLE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -310, - -319, - 549 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "El Salvador", - "NAME_LONG": "El Salvador", - "ABBREV": "El. S.", - "FORMAL_EN": "Republic of El Salvador", - "POP_EST": 6172011, - "POP_RANK": 13, - "GDP_MD_EST": 54790, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SV", - "ISO_A3": "SLV", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -230, - 550, - 551, - -262 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somaliland", - "NAME_LONG": "Somaliland", - "ABBREV": "Solnd.", - "FORMAL_EN": "Republic of Somaliland", - "POP_EST": 3500000, - "POP_RANK": 12, - "GDP_MD_EST": 12250, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -263, - -552, - 552, - -388 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somalia", - "NAME_LONG": "Somalia", - "ABBREV": "Som.", - "FORMAL_EN": "Federal Republic of Somalia", - "POP_EST": 7531386, - "POP_RANK": 13, - "GDP_MD_EST": 4719, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SO", - "ISO_A3": "SOM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -86, - -434, - -402, - -441, - -92, - -324, - -329, - -508 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Serbia", - "NAME_LONG": "Serbia", - "ABBREV": "Serb.", - "FORMAL_EN": "Republic of Serbia", - "POP_EST": 7111024, - "POP_RANK": 13, - "GDP_MD_EST": 101800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RS", - "ISO_A3": "SRB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -110, - -315, - 553, - -279 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Suriname", - "NAME_LONG": "Suriname", - "ABBREV": "Sur.", - "FORMAL_EN": "Republic of Suriname", - "POP_EST": 591919, - "POP_RANK": 11, - "GDP_MD_EST": 8547, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SR", - "ISO_A3": "SUR", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -54, - -221, - -498, - 554, - -326 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovakia", - "NAME_LONG": "Slovakia", - "ABBREV": "Svk.", - "FORMAL_EN": "Slovak Republic", - "POP_EST": 5445829, - "POP_RANK": 13, - "GDP_MD_EST": 168800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SK", - "ISO_A3": "SVK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -49, - -330, - -322, - 555, - -371 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovenia", - "NAME_LONG": "Slovenia", - "ABBREV": "Slo.", - "FORMAL_EN": "Republic of Slovenia", - "POP_EST": 1972126, - "POP_RANK": 12, - "GDP_MD_EST": 68350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SI", - "ISO_A3": "SVN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -267, - 556, - -470 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sweden", - "NAME_LONG": "Sweden", - "ABBREV": "Swe.", - "FORMAL_EN": "Kingdom of Sweden", - "POP_EST": 9960487, - "POP_RANK": 13, - "GDP_MD_EST": 498100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SE", - "ISO_A3": "SWE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -446, - 557 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Swaziland", - "NAME_LONG": "Swaziland", - "ABBREV": "Swz.", - "FORMAL_EN": "Kingdom of Swaziland", - "POP_EST": 1467152, - "POP_RANK": 12, - "GDP_MD_EST": 11060, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SZ", - "ISO_A3": "SWZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -362, - -379, - -367, - -410, - 558, - 559 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Syria", - "NAME_LONG": "Syria", - "ABBREV": "Syria", - "FORMAL_EN": "Syrian Arab Republic", - "POP_EST": 18028549, - "POP_RANK": 14, - "GDP_MD_EST": 50280, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "SY", - "ISO_A3": "SYR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -122, - -195, - -464, - -416, - -538 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Chad", - "NAME_LONG": "Chad", - "ABBREV": "Chad", - "FORMAL_EN": "Republic of Chad", - "POP_EST": 12075985, - "POP_RANK": 14, - "GDP_MD_EST": 30590, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TD", - "ISO_A3": "TCD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -69, - 560, - -290, - -73 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Togo", - "NAME_LONG": "Togo", - "ABBREV": "Togo", - "FORMAL_EN": "Togolese Republic", - "POP_EST": 7965055, - "POP_RANK": 13, - "GDP_MD_EST": 11610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TG", - "ISO_A3": "TGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -395, - 561, - -459, - 562, - -438, - -407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Thailand", - "NAME_LONG": "Thailand", - "ABBREV": "Thai.", - "FORMAL_EN": "Kingdom of Thailand", - "POP_EST": 68414135, - "POP_RANK": 16, - "GDP_MD_EST": 1161000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TH", - "ISO_A3": "THA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -3, - 563, - -393, - -167 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tajikistan", - "NAME_LONG": "Tajikistan", - "ABBREV": "Tjk.", - "FORMAL_EN": "Republic of Tajikistan", - "POP_EST": 8468555, - "POP_RANK": 13, - "GDP_MD_EST": 25810, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TJ", - "ISO_A3": "TJK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -1, - -357, - 564, - -385, - 565 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Turkmenistan", - "NAME_LONG": "Turkmenistan", - "ABBREV": "Turkm.", - "FORMAL_EN": "Turkmenistan", - "POP_EST": 5351277, - "POP_RANK": 13, - "GDP_MD_EST": 94720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TM", - "ISO_A3": "TKM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -332, - 566 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Timor-Leste", - "NAME_LONG": "Timor-Leste", - "ABBREV": "T.L.", - "FORMAL_EN": "Democratic Republic of Timor-Leste", - "POP_EST": 1291358, - "POP_RANK": 12, - "GDP_MD_EST": 4975, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TL", - "ISO_A3": "TLS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 567 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Trinidad and Tobago", - "NAME_LONG": "Trinidad and Tobago", - "ABBREV": "Tr.T.", - "FORMAL_EN": "Republic of Trinidad and Tobago", - "POP_EST": 1218208, - "POP_RANK": 12, - "GDP_MD_EST": 43570, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TT", - "ISO_A3": "TTO", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -242, - 568, - -413 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tunisia", - "NAME_LONG": "Tunisia", - "ABBREV": "Tun.", - "FORMAL_EN": "Republic of Tunisia", - "POP_EST": 11403800, - "POP_RANK": 14, - "GDP_MD_EST": 130800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TN", - "ISO_A3": "TUN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - [ - -36, - -355, - -363, - -560, - 569, - -287 - ] - ], - [ - [ - -83, - 570, - -304 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Turkey", - "NAME_LONG": "Turkey", - "ABBREV": "Tur.", - "FORMAL_EN": "Republic of Turkey", - "POP_EST": 80845215, - "POP_RANK": 16, - "GDP_MD_EST": 1670000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TR", - "ISO_A3": "TUR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 571 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Taiwan", - "NAME_LONG": "Taiwan", - "ABBREV": "Taiwan", - "FORMAL_EN": "", - "POP_EST": 23508428, - "POP_RANK": 15, - "GDP_MD_EST": 1127000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TW", - "ISO_A3": "TWN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -62, - -532, - 572, - -390, - 573, - -443, - -455, - 574, - -201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tanzania", - "NAME_LONG": "Tanzania", - "ABBREV": "Tanz.", - "FORMAL_EN": "United Republic of Tanzania", - "POP_EST": 53950935, - "POP_RANK": 16, - "GDP_MD_EST": 150600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TZ", - "ISO_A3": "TZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -199, - -541, - -391, - -573, - -531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uganda", - "NAME_LONG": "Uganda", - "ABBREV": "Uga.", - "FORMAL_EN": "Republic of Uganda", - "POP_EST": 39570125, - "POP_RANK": 15, - "GDP_MD_EST": 84930, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UG", - "ISO_A3": "UGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -96, - -513, - 575, - -518, - 576, - -510, - -428, - -509, - -327, - -555, - -497 - ], - [ - 517, - 518 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ukraine", - "NAME_LONG": "Ukraine", - "ABBREV": "Ukr.", - "FORMAL_EN": "Ukraine", - "POP_EST": 44033874, - "POP_RANK": 15, - "GDP_MD_EST": 352600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UA", - "ISO_A3": "UKR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -30, - -113, - 577 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uruguay", - "NAME_LONG": "Uruguay", - "ABBREV": "Ury.", - "FORMAL_EN": "Oriental Republic of Uruguay", - "POP_EST": 3360148, - "POP_RANK": 12, - "GDP_MD_EST": 73250, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UY", - "ISO_A3": "URY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -141, - 578, - -432, - 579 - ] - ], - [ - [ - -139, - 580 - ] - ], - [ - [ - 581 - ] - ], - [ - [ - 582 - ] - ], - [ - [ - 583 - ] - ], - [ - [ - 584 - ] - ], - [ - [ - 585 - ] - ], - [ - [ - 586 - ] - ], - [ - [ - 587 - ] - ], - [ - [ - 588 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United States of America", - "NAME_LONG": "United States", - "ABBREV": "U.S.A.", - "FORMAL_EN": "United States of America", - "POP_EST": 326625791, - "POP_RANK": 17, - "GDP_MD_EST": 18560000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "US", - "ISO_A3": "USA", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -2, - -566, - -384, - -394, - -564 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uzbekistan", - "NAME_LONG": "Uzbekistan", - "ABBREV": "Uzb.", - "FORMAL_EN": "Republic of Uzbekistan", - "POP_EST": 29748859, - "POP_RANK": 15, - "GDP_MD_EST": 202300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UZ", - "ISO_A3": "UZB", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -108, - -210, - 589, - -313 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Venezuela", - "NAME_LONG": "Venezuela", - "ABBREV": "Ven.", - "FORMAL_EN": "Bolivarian Republic of Venezuela", - "POP_EST": 31304016, - "POP_RANK": 15, - "GDP_MD_EST": 468600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VE", - "ISO_A3": "VEN", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -175, - 590, - -397, - -406 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Vietnam", - "NAME_LONG": "Vietnam", - "ABBREV": "Viet.", - "FORMAL_EN": "Socialist Republic of Vietnam", - "POP_EST": 96160163, - "POP_RANK": 16, - "GDP_MD_EST": 594900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VN", - "ISO_A3": "VNM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - 591 - ] - ], - [ - [ - 592 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Vanuatu", - "NAME_LONG": "Vanuatu", - "ABBREV": "Van.", - "FORMAL_EN": "Republic of Vanuatu", - "POP_EST": 282814, - "POP_RANK": 10, - "GDP_MD_EST": 723, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VU", - "ISO_A3": "VUT", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -480, - 593, - -534 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Yemen", - "NAME_LONG": "Yemen", - "ABBREV": "Yem.", - "FORMAL_EN": "Republic of Yemen", - "POP_EST": 28036829, - "POP_RANK": 15, - "GDP_MD_EST": 73450, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "YE", - "ISO_A3": "YEM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -118, - 594, - -447, - -558, - -445, - 595, - -461 - ], - [ - -419 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Africa", - "NAME_LONG": "South Africa", - "ABBREV": "S.Af.", - "FORMAL_EN": "Republic of South Africa", - "POP_EST": 54841552, - "POP_RANK": 16, - "GDP_MD_EST": 739100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZA", - "ISO_A3": "ZAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -10, - -202, - -575, - -454, - -449, - 596, - -120, - -460 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zambia", - "NAME_LONG": "Zambia", - "ABBREV": "Zambia", - "FORMAL_EN": "Republic of Zambia", - "POP_EST": 15972000, - "POP_RANK": 14, - "GDP_MD_EST": 65170, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZM", - "ISO_A3": "ZMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -121, - -597, - -448, - -595 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zimbabwe", - "NAME_LONG": "Zimbabwe", - "ABBREV": "Zimb.", - "FORMAL_EN": "Republic of Zimbabwe", - "POP_EST": 13805084, - "POP_RANK": 14, - "GDP_MD_EST": 28330, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZW", - "ISO_A3": "ZWE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - } - ] - } - } -} diff --git a/explorer-nextjs/app/components/ComponentError.tsx b/explorer-nextjs/app/components/ComponentError.tsx deleted file mode 100644 index 00b448fb9e..0000000000 --- a/explorer-nextjs/app/components/ComponentError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; - -export const ComponentError: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }} - variant="body1" - data-testid="delegation-total-amount" - > - {text} - </Typography> -); diff --git a/explorer-nextjs/app/components/ContentCard.tsx b/explorer-nextjs/app/components/ContentCard.tsx deleted file mode 100644 index c83049e5c7..0000000000 --- a/explorer-nextjs/app/components/ContentCard.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Card, CardHeader, CardContent, Typography } from '@mui/material' -import React, { ReactEventHandler } from 'react' - -type ContentCardProps = { - title?: React.ReactNode - subtitle?: string - Icon?: React.ReactNode - Action?: React.ReactNode - errorMsg?: string - onClick?: ReactEventHandler -} - -export const ContentCard: FCWithChildren<ContentCardProps> = ({ - title, - Icon, - Action, - subtitle, - errorMsg, - children, - onClick, -}) => ( - <Card onClick={onClick} sx={{ height: '100%' }}> - {title && ( - <CardHeader - title={title || ''} - avatar={Icon} - action={Action} - subheader={subtitle} - /> - )} - {children && <CardContent>{children}</CardContent>} - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {errorMsg} - </Typography> - )} - </Card> -) diff --git a/explorer-nextjs/app/components/CustomColumnHeading.tsx b/explorer-nextjs/app/components/CustomColumnHeading.tsx deleted file mode 100644 index e767db34c1..0000000000 --- a/explorer-nextjs/app/components/CustomColumnHeading.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import { Box, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { Tooltip } from '@nymproject/react/tooltip/Tooltip' - -export const CustomColumnHeading: FCWithChildren<{ - headingTitle: string - tooltipInfo?: string -}> = ({ headingTitle, tooltipInfo }) => { - const theme = useTheme() - - return ( - <Box alignItems="center" display="flex"> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={headingTitle} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - )} - <Typography variant="body2" fontWeight={600} data-testid={headingTitle}> - {headingTitle} - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx b/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx deleted file mode 100644 index 8e859f5eba..0000000000 --- a/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { - Breakpoint, - Button, - Paper, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - SxProps, - Typography, -} from '@mui/material'; - -export interface ConfirmationModalProps { - open: boolean; - onConfirm: () => void; - onClose?: () => void; - children?: React.ReactNode; - title: React.ReactNode | string; - subTitle?: React.ReactNode | string; - confirmButton: React.ReactNode | string; - disabled?: boolean; - sx?: SxProps; - fullWidth?: boolean; - maxWidth?: Breakpoint; - backdropProps?: object; -} - -export const ConfirmationModal = ({ - open, - onConfirm, - onClose, - children, - title, - subTitle, - confirmButton, - disabled, - sx, - fullWidth, - maxWidth, - backdropProps, -}: ConfirmationModalProps) => { - const Title = ( - <DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}> - {title} - {subTitle && - (typeof subTitle === 'string' ? ( - <Typography fontWeight={400} variant="subtitle1" fontSize={12} color="grey"> - {subTitle} - </Typography> - ) : ( - subTitle - ))} - </DialogTitle> - ); - const ConfirmButton = - typeof confirmButton === 'string' ? ( - <Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}> - <Typography variant="button" fontSize="large"> - {confirmButton} - </Typography> - </Button> - ) : ( - confirmButton - ); - return ( - <Dialog - open={open} - onClose={onClose} - aria-labelledby="responsive-dialog-title" - maxWidth={maxWidth || 'sm'} - sx={{ textAlign: 'center', ...sx }} - fullWidth={fullWidth} - BackdropProps={backdropProps} - PaperComponent={Paper} - PaperProps={{ elevation: 0 }} - > - {Title} - <DialogContent>{children}</DialogContent> - <DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions> - </Dialog> - ); -}; diff --git a/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx b/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx deleted file mode 100644 index 9ea2f54b6b..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react' -import { Button, IconButton } from '@mui/material' -import { SxProps } from '@mui/system' -import { useIsMobile } from '@/app/hooks' -import { DelegateIcon } from '@/app/icons/DelevateSVG' - -export const DelegateIconButton: FCWithChildren<{ - size?: 'small' | 'medium' - disabled?: boolean - tooltip?: React.ReactNode - sx?: SxProps - onDelegate: () => void -}> = ({ onDelegate, sx, disabled, size = 'medium' }) => { - const isMobile = useIsMobile() - - const handleOnDelegate = () => { - onDelegate() - } - - if (isMobile) { - return ( - <IconButton size="small" disabled={disabled} onClick={handleOnDelegate}> - <DelegateIcon fontSize="small" /> - </IconButton> - ) - } - - return ( - <Button - variant="outlined" - size={size} - disabled={disabled} - onClick={handleOnDelegate} - sx={sx} - > - Delegate - </Button> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/DelegateModal.tsx b/explorer-nextjs/app/components/Delegations/DelegateModal.tsx deleted file mode 100644 index 5b453e8ad5..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegateModal.tsx +++ /dev/null @@ -1,191 +0,0 @@ -'use client' - -import React, { useState } from 'react' -import { Box, SxProps } from '@mui/material' -import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField' -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField' -import { CurrencyDenom, DecCoin } from '@nymproject/types' -import { useWalletContext } from '@/app/context/wallet' -import { urls } from '@/app/utils' -import { useDelegationsContext } from '@/app/context/delegations' -import { validateAmount } from '@/app/utils/currency' -import { SimpleModal } from './SimpleModal' -import { ModalListItem } from './ModalListItem' -import { DelegationModalProps } from './DelegationModal' - -const MIN_AMOUNT_TO_DELEGATE = 10 - -type Props = { - mixId: number - identityKey: string - header?: string - buttonText?: string - rewardInterval?: string - estimatedReward?: number - profitMarginPercentage?: string | null - nodeUptimePercentage?: number | null - denom: CurrencyDenom - sx?: SxProps - backdropProps?: object - onClose: () => void - onOk?: (delegationModalProps: DelegationModalProps) => void -} - -export const DelegateModal = ({ - mixId, - identityKey, - onClose, - onOk, - denom, - sx, -}: Props) => { - const [amount, setAmount] = useState<DecCoin | undefined>({ - amount: '10', - denom: 'nym', - }) - const [isValidated, setValidated] = useState<boolean>(false) - const [errorAmount, setErrorAmount] = useState<string | undefined>() - - const { address, balance } = useWalletContext() - const { handleDelegate } = useDelegationsContext() - - const validate = async () => { - let newValidatedValue = true - let errorAmountMessage - - if (amount && !(await validateAmount(amount.amount, '0'))) { - newValidatedValue = false - errorAmountMessage = 'Please enter a valid amount' - } - - if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) { - errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}` - newValidatedValue = false - } - - if (!amount?.amount.length) { - newValidatedValue = false - } - - if (amount && balance.data && +balance.data - +amount.amount <= 0) { - errorAmountMessage = 'Not enough funds' - newValidatedValue = false - } - - setErrorAmount(errorAmountMessage) - setValidated(newValidatedValue) - } - - const delegateToMixnode = async ({ - delegationMixId, - delegationAmount, - }: { - delegationMixId: number - delegationAmount: string - }) => { - try { - const tx = await handleDelegate(delegationMixId, delegationAmount) - return tx - } catch (e) { - console.error('Failed to delegate to mixnode', e) - throw e - } - } - - const handleConfirm = async () => { - if (mixId && amount && onOk) { - onOk({ - status: 'loading', - }) - try { - if (!address) { - throw new Error('Please connect your wallet') - } - - const tx = await delegateToMixnode({ - delegationMixId: mixId, - delegationAmount: amount.amount, - }) - - if (!tx) { - throw new Error('Failed to delegate') - } - - onOk({ - status: 'success', - message: 'Delegation can take up to one hour to process', - transactions: [ - { - url: `${urls('MAINNET').blockExplorer}/transaction/${ - tx.transactionHash - }`, - hash: tx.transactionHash, - }, - ], - }) - } catch (e) { - console.error('Failed to delegate', e) - onOk({ - status: 'error', - message: (e as Error).message, - }) - } - } - } - - const handleAmountChanged = (newAmount: DecCoin) => { - setAmount(newAmount) - } - - React.useEffect(() => { - validate() - }, [amount, identityKey, mixId]) - - return ( - <SimpleModal - open - onClose={onClose} - onOk={handleConfirm} - header="Delegate" - okLabel="Delegate" - okDisabled={!isValidated} - sx={sx} - > - <Box sx={{ mt: 3 }} gap={2}> - <IdentityKeyFormField - required - fullWidth - label="Node identity key" - onChanged={() => undefined} - initialValue={identityKey} - readOnly - showTickOnValid={false} - /> - </Box> - - <Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}> - <CurrencyFormField - showCoinMark={false} - required - fullWidth - autoFocus - label="Amount" - initialValue={amount?.amount || '10'} - onChanged={handleAmountChanged} - denom={denom} - validationError={errorAmount} - /> - </Box> - <Box sx={{ mt: 3 }}> - <ModalListItem - label="Account balance" - value={`${balance.data} NYM`} - divider - fontWeight={600} - /> - </Box> - - <ModalListItem label="Est. fee for this transaction will be calculated in your connected wallet" /> - </SimpleModal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/DelegationModal.tsx b/explorer-nextjs/app/components/Delegations/DelegationModal.tsx deleted file mode 100644 index 76c37a9e96..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegationModal.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react' -import { Typography, SxProps, Stack } from '@mui/material' -import { Link } from '@nymproject/react/link/Link' -import { LoadingModal } from './LoadingModal' -import { ConfirmationModal } from './ConfirmationModal' -import { ErrorModal } from './ErrorModal' - -export type DelegationModalProps = { - status: 'loading' | 'success' | 'error' | 'info' - message?: string - transactions?: { - url: string - hash: string - }[] -} - -export const DelegationModal: FCWithChildren< - DelegationModalProps & { - open: boolean - onClose: () => void - sx?: SxProps - backdropProps?: object - children?: React.ReactNode - } -> = ({ - status, - message, - transactions, - open, - onClose, - children, - sx, - backdropProps, -}) => { - if (status === 'loading') - return <LoadingModal sx={sx} backdropProps={backdropProps} /> - - if (status === 'error') { - return ( - <ErrorModal message={message} sx={sx} open={open} onClose={onClose}> - {children} - </ErrorModal> - ) - } - - if (status === 'info') { - return ( - <ConfirmationModal - open={open} - title="Connect wallet" - confirmButton="OK" - onConfirm={onClose} - > - <Typography>{message}</Typography> - </ConfirmationModal> - ) - } - - return ( - <ConfirmationModal - open={open} - onConfirm={onClose || (() => {})} - title="Transaction successful" - confirmButton="Done" - > - <Stack alignItems="center" spacing={2} mb={0}> - {message && <Typography>{message}</Typography>} - {transactions?.length === 1 && ( - <Link - href={transactions[0].url} - target="_blank" - sx={{ ml: 1 }} - text="View on blockchain" - noIcon - /> - )} - {transactions && transactions.length > 1 && ( - <Stack alignItems="center" spacing={1}> - <Typography>View the transactions on blockchain:</Typography> - {transactions.map(({ url, hash }) => ( - <Link - href={url} - target="_blank" - sx={{ ml: 1 }} - text={hash.slice(0, 6)} - key={hash} - noIcon - /> - ))} - </Stack> - )} - </Stack> - </ConfirmationModal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/ErrorModal.tsx b/explorer-nextjs/app/components/Delegations/ErrorModal.tsx deleted file mode 100644 index b9218d1e1e..0000000000 --- a/explorer-nextjs/app/components/Delegations/ErrorModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, SxProps, Typography } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const ErrorModal: FCWithChildren<{ - open: boolean; - title?: string; - message?: string; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - children?: React.ReactNode; -}> = ({ children, open, title, message, sx, backdropProps, onClose }) => ( - <Modal open={open} onClose={onClose} BackdropProps={backdropProps}> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Typography color={(theme) => theme.palette.error.main} mb={1}> - {title || 'Oh no! Something went wrong...'} - </Typography> - <Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}> - {message} - </Typography> - {children} - <Button variant="contained" onClick={onClose}> - Close - </Button> - </Box> - </Modal> -); diff --git a/explorer-nextjs/app/components/Delegations/LoadingModal.tsx b/explorer-nextjs/app/components/Delegations/LoadingModal.tsx deleted file mode 100644 index eda13938d4..0000000000 --- a/explorer-nextjs/app/components/Delegations/LoadingModal.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const LoadingModal: FCWithChildren<{ - text?: string; - sx?: SxProps; - backdropProps?: object; -}> = ({ sx, text = 'Please wait...' }) => ( - <Modal open> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Stack spacing={4} direction="row" alignItems="center"> - <CircularProgress /> - <Typography sx={{ color: 'text.primary' }}>{text}</Typography> - </Stack> - </Box> - </Modal> -); diff --git a/explorer-nextjs/app/components/Delegations/ModalDivider.tsx b/explorer-nextjs/app/components/Delegations/ModalDivider.tsx deleted file mode 100644 index 6258e0bfac..0000000000 --- a/explorer-nextjs/app/components/Delegations/ModalDivider.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Box, SxProps } from '@mui/material'; - -export const ModalDivider: FCWithChildren<{ - sx?: SxProps; -}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />; diff --git a/explorer-nextjs/app/components/Delegations/ModalListItem.tsx b/explorer-nextjs/app/components/Delegations/ModalListItem.tsx deleted file mode 100644 index 830c25705e..0000000000 --- a/explorer-nextjs/app/components/Delegations/ModalListItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material'; -import { ModalDivider } from './ModalDivider'; - -export const ModalListItem: FCWithChildren<{ - label: string; - divider?: boolean; - hidden?: boolean; - fontWeight?: TypographyProps['fontWeight']; - fontSize?: TypographyProps['fontSize']; - light?: boolean; - value?: React.ReactNode; - sxValue?: SxProps; -}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => ( - <Box sx={{ display: hidden ? 'none' : 'block' }}> - <Stack direction="row" justifyContent="space-between" alignItems="center"> - <Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}> - {label} - </Typography> - {value && ( - <Typography - fontSize="smaller" - fontWeight={fontWeight} - sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }} - > - {value} - </Typography> - )} - </Stack> - {divider && <ModalDivider />} - </Box> -); diff --git a/explorer-nextjs/app/components/Delegations/SimpleModal.tsx b/explorer-nextjs/app/components/Delegations/SimpleModal.tsx deleted file mode 100644 index b1909c8c5e..0000000000 --- a/explorer-nextjs/app/components/Delegations/SimpleModal.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react' -import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import ErrorOutline from '@mui/icons-material/ErrorOutline' -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export const modalStyle = (width: number | string = 600) => ({ - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - width, - transform: 'translate(-50%, -50%)', - bgcolor: 'background.paper', - boxShadow: 24, - borderRadius: '16px', - p: 4, -}) - -export const StyledBackButton = ({ - onBack, - label, - fullWidth, - sx, -}: { - onBack: () => void - label?: string - fullWidth?: boolean - sx?: SxProps -}) => ( - <Button - disableFocusRipple - size="large" - fullWidth={fullWidth} - variant="outlined" - onClick={onBack} - sx={sx} - > - {label || <ArrowBackIosNewIcon fontSize="small" />} - </Button> -) - -export const SimpleModal: FCWithChildren<{ - open: boolean - hideCloseIcon?: boolean - displayErrorIcon?: boolean - displayInfoIcon?: boolean - headerStyles?: SxProps - subHeaderStyles?: SxProps - buttonFullWidth?: boolean - onClose?: () => void - onOk?: () => Promise<void> - onBack?: () => void - header: string | React.ReactNode - subHeader?: string - okLabel: string - backLabel?: string - backButtonFullWidth?: boolean - okDisabled?: boolean - sx?: SxProps - children?: React.ReactNode -}> = ({ - open, - hideCloseIcon, - displayErrorIcon, - displayInfoIcon, - headerStyles, - buttonFullWidth, - onClose, - okDisabled, - onOk, - onBack, - header, - subHeader, - okLabel, - backLabel, - backButtonFullWidth, - sx, - children, -}) => { - const isMobile = useIsMobile() - - return ( - <Modal open={open} onClose={onClose}> - <Box sx={{ ...modalStyle(isMobile ? '90%' : 600), ...sx }}> - {displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />} - {displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />} - <Stack - direction="row" - justifyContent="space-between" - alignItems="center" - > - {typeof header === 'string' ? ( - <Typography - fontSize={20} - fontWeight={600} - sx={{ color: 'text.primary', ...headerStyles }} - > - {header} - </Typography> - ) : ( - header - )} - {!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />} - </Stack> - - <Typography - mt={subHeader ? 0.5 : 0} - mb={3} - fontSize={12} - color={(theme) => theme.palette.text.secondary} - > - {subHeader} - </Typography> - - {children} - - {(onOk || onBack) && ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: 2, - mt: 2, - width: buttonFullWidth ? '100%' : null, - }} - > - {onBack && ( - <StyledBackButton - onBack={onBack} - label={backLabel} - fullWidth={backButtonFullWidth} - /> - )} - {onOk && ( - <Button - variant="contained" - fullWidth - size="large" - onClick={onOk} - disabled={okDisabled} - > - {okLabel} - </Button> - )} - </Box> - )} - </Box> - </Modal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/index.ts b/explorer-nextjs/app/components/Delegations/index.ts deleted file mode 100644 index da51de9bd9..0000000000 --- a/explorer-nextjs/app/components/Delegations/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './ConfirmationModal'; -export * from './DelegateIconButton'; -export * from './DelegationModal'; -export * from './DelegateModal'; -export * from './ErrorModal'; -export * from './LoadingModal'; -export * from './ModalDivider'; -export * from './ModalListItem'; -export * from './SimpleModal'; -export * from './styles'; diff --git a/explorer-nextjs/app/components/Delegations/styles.ts b/explorer-nextjs/app/components/Delegations/styles.ts deleted file mode 100644 index 9b26551767..0000000000 --- a/explorer-nextjs/app/components/Delegations/styles.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Theme } from '@mui/material/styles'; - -export const backDropStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { - style: { - left: mode === 'light' ? '0' : '50%', - width: '50%', - }, - }; -}; - -export const modalStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '25%' : '75%' }; -}; - -export const dialogStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '-50%' : '50%' }; -}; diff --git a/explorer-nextjs/app/components/DetailTable.tsx b/explorer-nextjs/app/components/DetailTable.tsx deleted file mode 100644 index b068a5c852..0000000000 --- a/explorer-nextjs/app/components/DetailTable.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import * as React from 'react' -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' -import { Box } from '@mui/system' -import { unymToNym } from '@/app/utils/currency' -import { GatewayEnrichedRowType } from './Gateways/Gateways' -import { MixnodeRowType } from './MixNodes' -import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar' -import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants"; - -export type ColumnsType = { - field: string - title: string - headerAlign?: TableCellProps['align'] - width?: string | number - tooltipInfo?: string -} - -export interface UniversalTableProps<T = any> { - tableName: string - columnsData: ColumnsType[] - rows: T[] -} - -function formatCellValues(val: string | number, field: string) { - if (field === 'identity_key' && typeof val === 'string') { - return ( - <Box display="flex" justifyContent="flex-end"> - <CopyToClipboard - sx={{ mr: 1, mt: 0.5, fontSize: '18px' }} - value={val} - tooltip={`Copy identity key ${val} to clipboard`} - /> - <span>{val}</span> - </Box> - ) - } - - if (field === 'bond') { - return unymToNym(val, 6) - } - - if (field === 'owner') { - return ( - <Link - underline="none" - color="inherit" - target="_blank" - href={`${EXPLORER_FOR_ACCOUNTS}/account/${val}`} - > - {val} - </Link> - ) - } - - if (field === 'stake_saturation') { - return <StakeSaturationProgressBar value={Number(val)} threshold={100} /> - } - - return val -} - -export const DetailTable: FCWithChildren<{ - tableName: string - columnsData: ColumnsType[] - rows: MixnodeRowType[] | GatewayEnrichedRowType[] | any[] -}> = ({ tableName, columnsData, rows }: UniversalTableProps) => { - const theme = useTheme() - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 1080 }} aria-label={tableName}> - <TableHead> - <TableRow> - {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' }}> - <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> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows.map((eachRow) => ( - <TableRow - key={eachRow.id} - sx={{ '&:last-child td, &:last-child th': { border: 0 } }} - > - {columnsData?.map((data, index) => ( - <TableCell - key={data.title} - component="th" - scope="row" - variant="body" - sx={{ - padding: 2, - width: 200, - fontSize: 14, - }} - data-testid={`${data.title.replace(/ /g, '-')}-value`} - > - {formatCellValues( - eachRow[columnsData[index].field], - columnsData[index].field - )} - </TableCell> - ))} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/Filters/Filters.tsx b/explorer-nextjs/app/components/Filters/Filters.tsx deleted file mode 100644 index 3a871a5fb4..0000000000 --- a/explorer-nextjs/app/components/Filters/Filters.tsx +++ /dev/null @@ -1,193 +0,0 @@ -'use client' - -import React, { useState, useEffect, useRef, useCallback } from 'react' -import { - Button, - Dialog, - DialogContent, - DialogActions, - DialogTitle, - Slider, - Typography, - Box, - Snackbar, - Slide, - Alert, -} from '@mui/material' -import { useParams } from 'next/navigation' -import { useMainContext } from '@/app/context/main' -import { - MixnodeStatusWithAll, - toMixnodeStatus, -} from '@/app/typeDefs/explorer-api' -import { EnumFilterKey, TFilterItem, TFilters } from '@/app/typeDefs/filters' -import { Api } from '@/app/api' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { formatOnSave, generateFilterSchema } from './filterSchema' -import FiltersButton from './FiltersButton' - -const FilterItem = ({ - label, - id, - tooltipInfo, - value, - isSmooth, - marks, - scale, - min, - max, - onChange, -}: TFilterItem & { - onChange: (id: EnumFilterKey, newValue: number[]) => void -}) => ( - <Box sx={{ p: 2 }}> - <Typography gutterBottom>{label}</Typography> - <Typography fontSize={12}>{tooltipInfo}</Typography> - <Slider - value={value} - onChange={(e: Event, newValue: number | number[]) => - onChange(id, newValue as number[]) - } - valueLabelDisplay={isSmooth ? 'auto' : 'off'} - marks={marks} - step={isSmooth ? 1 : null} - scale={scale} - min={min} - max={max} - valueLabelFormat={(val: number) => - val === 100 && id === 'stakeSaturation' ? '>100' : val - } - /> - </Box> -) - -export const Filters = () => { - const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext() - const { status } = useParams<{ - status: 'active' | 'standby' | 'inactive' | 'all' - }>() - const isMobile = useIsMobile() - - const [showFilters, setShowFilters] = useState(false) - const [isFiltered, setIsFiltered] = useState(false) - const [filters, setFilters] = React.useState<TFilters>() - const [upperSaturationValue, setUpperSaturationValue] = - React.useState<number>(100) - - const baseFilters = useRef<TFilters>() - const prevFilters = useRef<TFilters>() - - const handleToggleShowFilters = () => setShowFilters(!showFilters) - - const initialiseFilters = useCallback(async () => { - const allMixnodes = await Api.fetchMixnodes() - if (allMixnodes) { - setUpperSaturationValue( - Math.round( - Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1 - ) - ) - const initFilters = generateFilterSchema() - baseFilters.current = initFilters - prevFilters.current = initFilters - setFilters(initFilters) - } - }, []) - - const handleOnChange = (id: EnumFilterKey, newValue: number[]) => { - if (id === 'stakeSaturation' && newValue[1] === 100) { - newValue.splice(1, 1, upperSaturationValue) - } - setFilters((ftrs) => { - if (ftrs) - return { - ...ftrs, - [id]: { - ...ftrs[id], - value: newValue, - }, - } - return undefined - }) - } - - const handleOnSave = async () => { - setShowFilters(false) - await filterMixnodes(formatOnSave(filters!), status) - setIsFiltered(true) - prevFilters.current = filters - } - - const handleOnCancel = () => { - setShowFilters(false) - setFilters(prevFilters.current) - } - - const resetFilters = () => { - setFilters(baseFilters.current) - setIsFiltered(false) - prevFilters.current = baseFilters.current - } - - const onClearFilters = async () => { - await fetchMixnodes(toMixnodeStatus(MixnodeStatusWithAll[status])) - resetFilters() - } - - useEffect(() => { - initialiseFilters() - }, [initialiseFilters]) - - useEffect(() => { - resetFilters() - }, [status]) - - if (!filters) return null - - return ( - <> - <Snackbar - open={isFiltered} - anchorOrigin={{ vertical: 'top', horizontal: 'center' }} - message="Filters applied" - TransitionComponent={Slide} - transitionDuration={250} - > - <Alert - severity="info" - variant={isMobile ? 'standard' : 'outlined'} - sx={{ color: (t) => t.palette.info.light }} - action={ - <Button size="small" onClick={onClearFilters}> - CLEAR FILTERS - </Button> - } - > - {mixnodes?.data?.length} mixnodes matched your criteria - </Alert> - </Snackbar> - <FiltersButton onClick={handleToggleShowFilters} fullWidth /> - <Dialog - open={showFilters} - onClose={handleToggleShowFilters} - maxWidth="md" - fullWidth - > - <DialogTitle>Mixnode filters</DialogTitle> - <DialogContent dividers> - {Object.values(filters).map((v) => ( - <FilterItem {...v} key={v.id} onChange={handleOnChange} /> - ))} - </DialogContent> - <DialogActions> - <Button size="large" onClick={handleOnCancel}> - Cancel - </Button> - <Button variant="contained" size="large" onClick={handleOnSave}> - Save - </Button> - </DialogActions> - </Dialog> - </> - ) -} diff --git a/explorer-nextjs/app/components/Filters/FiltersButton.tsx b/explorer-nextjs/app/components/Filters/FiltersButton.tsx deleted file mode 100644 index 1e6d3e2940..0000000000 --- a/explorer-nextjs/app/components/Filters/FiltersButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { Tune } from '@mui/icons-material'; - -type FiltersButtonProps = { - iconOnly?: boolean; - fullWidth?: boolean; - onClick: () => void; -}; - -const FiltersButton = ({ iconOnly, fullWidth, onClick }: FiltersButtonProps) => { - if (iconOnly) { - return ( - <IconButton onClick={onClick} color="primary"> - <Tune /> - </IconButton> - ); - } - - return ( - <Button - fullWidth={fullWidth} - size="large" - variant="contained" - endIcon={<Tune />} - onClick={onClick} - sx={{ textTransform: 'none' }} - > - Filters - </Button> - ); -}; - -export default FiltersButton; diff --git a/explorer-nextjs/app/components/Filters/filterSchema.ts b/explorer-nextjs/app/components/Filters/filterSchema.ts deleted file mode 100644 index c5011114b1..0000000000 --- a/explorer-nextjs/app/components/Filters/filterSchema.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { EnumFilterKey, TFilters } from '../../typeDefs/filters'; - -export const generateFilterSchema = () => ({ - profitMargin: { - label: 'Profit margin (%)', - id: EnumFilterKey.profitMargin, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: - 'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators', - }, - stakeSaturation: { - label: 'Stake saturation (%)', - id: EnumFilterKey.stakeSaturation, - value: [0, 100], - isSmooth: true, - marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({ - value: value < 100 ? value : 100, - label: value < 100 ? value : '>100', - })), - tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", - }, - routingScore: { - label: 'Routing score (%)', - id: EnumFilterKey.routingScore, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards', - }, -}); - -const formatStakeSaturationValues = ([value_1, value_2]: number[]) => { - const lowerValue = value_1 / 100; - const upperValue = value_2 / 100; - - return [lowerValue, upperValue]; -}; - -export const formatOnSave = (filters: TFilters) => ({ - routingScore: filters.routingScore.value, - profitMargin: filters.profitMargin.value, - stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value), -}); diff --git a/explorer-nextjs/app/components/Footer.tsx b/explorer-nextjs/app/components/Footer.tsx deleted file mode 100644 index 3e69bd3c42..0000000000 --- a/explorer-nextjs/app/components/Footer.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import Box from '@mui/material/Box' -import MuiLink from '@mui/material/Link' -import Typography from '@mui/material/Typography' -import { useIsMobile } from '../hooks/useIsMobile' -import { NymVpnIcon } from '../icons/NymVpn' -import { Socials } from './Socials' -import Link from 'next/link' - -export const Footer: FCWithChildren = () => { - const isMobile = useIsMobile() - - return ( - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - width: '100%', - height: 'auto', - mt: 3, - pt: 3, - pb: 3, - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - justifyContent: 'center', - alignItems: 'center', - mb: 2, - }} - > - <Box marginRight={1}> - <Link href="http://nymvpn.com" target="_blank"> - <NymVpnIcon /> - </Link> - </Box> - - <Socials isFooter /> - </Box> - - <Typography - sx={{ - fontSize: 12, - textAlign: isMobile ? 'center' : 'end', - color: 'nym.muted.onDarkBg', - }} - > - © {new Date().getFullYear()} Nym Technologies SA, all rights reserved - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Gateways/Gateways.ts b/explorer-nextjs/app/components/Gateways/Gateways.ts deleted file mode 100644 index f4dbf2743c..0000000000 --- a/explorer-nextjs/app/components/Gateways/Gateways.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {GatewayResponse, GatewayBond, GatewayReportResponse, LocatedGateway} from '@/app/typeDefs/explorer-api'; -import { toPercentInteger } from '@/app/utils'; - -export type GatewayRowType = { - id: string; - owner: string; - identity_key: string; - bond: number; - host: string; - location: string; - version: string; -// node_performance: number; -}; - -export type GatewayEnrichedRowType = GatewayRowType & { - routingScore: string; - avgUptime: string; - clientsPort: number; - mixPort: number; -}; - -export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] { - return !arrayOfGateways - ? [] - : arrayOfGateways.map((gw) => ({ - id: gw.owner, - owner: gw.owner, - identity_key: gw.gateway.identity_key || '', - location: gw.location?.country_name.toUpperCase() || '', - bond: gw.pledge_amount.amount || 0, - host: gw.gateway.host || '', - version: gw.gateway.version || '', -// node_performance: toPercentInteger(gw.node_performance.last_24h), - })); -} - -export function gatewayEnrichedToGridRow(gateway: LocatedGateway, report: GatewayReportResponse): GatewayEnrichedRowType { - return { - id: gateway.owner, - owner: gateway.owner, - identity_key: gateway.gateway.identity_key || '', - location: gateway.location?.country_name.toUpperCase() || '', - bond: gateway.pledge_amount.amount || 0, - host: gateway.gateway.host || '', - version: gateway.gateway.version || '', - clientsPort: gateway.gateway.clients_port || 0, - mixPort: gateway.gateway.mix_port || 0, - routingScore: `${report.most_recent}%`, - avgUptime: `${report.last_day || report.last_hour}%`, -// node_performance: toPercentInteger(gateway.node_performance.most_recent), - }; -} diff --git a/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx b/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx deleted file mode 100644 index 10f230e26a..0000000000 --- a/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import { FormControl, MenuItem, Select } from '@mui/material' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export enum VersionSelectOptions { - latestVersion = 'Latest versions', - olderVersions = 'Older versions', - all = 'All', -} -export const VersionDisplaySelector = ({ - selected, - handleChange, -}: { - selected: VersionSelectOptions - handleChange: (option: VersionSelectOptions) => void -}) => { - const isMobile = useIsMobile() - - return ( - <FormControl size="small"> - <Select - value={selected} - onChange={(e) => handleChange(e.target.value as VersionSelectOptions)} - labelId="simple-select-label" - id="simple-select" - sx={{ - marginRight: isMobile ? 0 : 2, - }} - > - <MenuItem - value={VersionSelectOptions.latestVersion} - data-testid="show-gateway-latest-version" - > - {VersionSelectOptions.latestVersion} - </MenuItem> - <MenuItem - value={VersionSelectOptions.olderVersions} - data-testid="show-gateway-old-versions" - > - {VersionSelectOptions.olderVersions} - </MenuItem> - <MenuItem - value={VersionSelectOptions.all} - data-testid="show-gateway-all-versions" - > - {VersionSelectOptions.all} - </MenuItem> - </Select> - </FormControl> - ) -} diff --git a/explorer-nextjs/app/components/Icons.ts b/explorer-nextjs/app/components/Icons.ts deleted file mode 100644 index 88761b9d21..0000000000 --- a/explorer-nextjs/app/components/Icons.ts +++ /dev/null @@ -1,28 +0,0 @@ -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; -import { MixnodeStatus } from '../typeDefs/explorer-api'; - -export const Icons = { - Mixnodes: { - Status: { - Active: CheckCircleOutlineIcon, - Standby: PauseCircleOutlineIcon, - Inactive: CircleOutlinedIcon, - }, - }, -}; - -export const getMixNodeIcon = (value: any) => { - if (value && typeof value === 'string') { - switch (value) { - case MixnodeStatus.active: - return Icons.Mixnodes.Status.Active; - case MixnodeStatus.standby: - return Icons.Mixnodes.Status.Standby; - default: - return Icons.Mixnodes.Status.Inactive; - } - } - return Icons.Mixnodes.Status.Inactive; -}; diff --git a/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx b/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx deleted file mode 100644 index 58ea4c97b2..0000000000 --- a/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import * as React from 'react' -import { Alert, Box, CircularProgress, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import Table from '@mui/material/Table' -import TableBody from '@mui/material/TableBody' -import TableCell from '@mui/material/TableCell' -import TableContainer from '@mui/material/TableContainer' -import TableHead from '@mui/material/TableHead' -import TableRow from '@mui/material/TableRow' -import Paper from '@mui/material/Paper' -import { ExpandMore } from '@mui/icons-material' -import { currencyToString } from '@/app/utils/currency' -import { useMixnodeContext } from '@/app/context/mixnode' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export const BondBreakdownTable: FCWithChildren = () => { - const { mixNode, delegations, uniqDelegations } = useMixnodeContext() - const [showDelegations, toggleShowDelegations] = - React.useState<boolean>(false) - - const [bonds, setBonds] = React.useState({ - delegations: '0', - pledges: '0', - bondsTotal: '0', - hasLoaded: false, - }) - const theme = useTheme() - const isMobile = useIsMobile() - - React.useEffect(() => { - if (mixNode?.data) { - // delegations - const decimalisedDelegations = currencyToString({ - amount: mixNode.data.total_delegation.amount.toString(), - denom: mixNode.data.total_delegation.denom, - }) - - // pledges - const decimalisedPledges = currencyToString({ - amount: mixNode.data.pledge_amount.amount.toString(), - denom: mixNode.data.pledge_amount.denom, - }) - - // bonds total (del + pledges) - const pledgesSum = Number(mixNode.data.pledge_amount.amount) - const delegationsSum = Number(mixNode.data.total_delegation.amount) - const bondsTotal = currencyToString({ - amount: (pledgesSum + delegationsSum).toString(), - }) - - setBonds({ - delegations: decimalisedDelegations, - pledges: decimalisedPledges, - bondsTotal, - hasLoaded: true, - }) - } - }, [mixNode]) - - const expandDelegations = () => { - if (delegations?.data && delegations.data.length > 0) { - toggleShowDelegations(!showDelegations) - } - } - const calcBondPercentage = (num: number) => { - if (mixNode?.data) { - const rawDelegationAmount = Number(mixNode.data.total_delegation.amount) - const rawPledgeAmount = Number(mixNode.data.pledge_amount.amount) - const rawTotalBondsAmount = rawDelegationAmount + rawPledgeAmount - return ((num * 100) / rawTotalBondsAmount).toFixed(1) - } - return 0 - } - - if (mixNode?.isLoading || delegations?.isLoading) { - return <CircularProgress /> - } - - if (mixNode?.error) { - return <Alert severity="error">Mixnode not found</Alert> - } - if (delegations?.error) { - return <Alert severity="error">Unable to get delegations for mixnode</Alert> - } - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals"> - <TableBody> - <TableRow sx={isMobile ? { minWidth: '70vw' } : null}> - <TableCell - sx={{ - fontWeight: 400, - width: '150px', - }} - align="left" - > - Stake total - </TableCell> - <TableCell align="left" data-testid="bond-total-amount"> - {bonds.bondsTotal} - </TableCell> - </TableRow> - <TableRow> - <TableCell align="left">Bond</TableCell> - <TableCell align="left" data-testid="pledge-total-amount"> - {bonds.pledges} - </TableCell> - </TableRow> - <TableRow> - <TableCell onClick={expandDelegations} align="left"> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - }} - > - Delegation total {'\u00A0'} - {delegations?.data && delegations?.data?.length > 0 && ( - <ExpandMore /> - )} - </Box> - </TableCell> - <TableCell align="left" data-testid="delegation-total-amount"> - {bonds.delegations} - </TableCell> - </TableRow> - </TableBody> - </Table> - - {showDelegations && ( - <Box - sx={{ - maxHeight: 400, - overflowY: 'scroll', - p: 2, - background: theme.palette.background.paper, - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'baseline', - width: '100%', - p: 2, - borderBottom: `1px solid ${theme.palette.divider}`, - }} - data-testid="delegations-total-amount" - > - <Typography - sx={{ - fontSize: 16, - fontWeight: 600, - }} - > - Delegations   - </Typography> - </Box> - <Table stickyHeader> - <TableHead> - <TableRow> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Delegators - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Amount - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - width: '200px', - }} - align="left" - > - Share of stake - </TableCell> - </TableRow> - </TableHead> - - <TableBody> - {uniqDelegations?.data?.map(({ owner, amount: { amount } }) => ( - <TableRow key={owner}> - <TableCell sx={isMobile ? { width: 190 } : null} align="left"> - {owner} - </TableCell> - <TableCell align="left"> - {currencyToString({ amount: amount.toString() })} - </TableCell> - <TableCell align="left"> - {calcBondPercentage(amount)}% - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </Box> - )} - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/DetailSection.tsx b/explorer-nextjs/app/components/MixNodes/DetailSection.tsx deleted file mode 100644 index 94a24f338d..0000000000 --- a/explorer-nextjs/app/components/MixNodes/DetailSection.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from 'react' -import { Box, Button, Grid, Typography, useTheme } from '@mui/material' -import Identicon from 'react-identicons' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MixNodeDescriptionResponse } from '@/app/typeDefs/explorer-api' -import { getMixNodeStatusText, MixNodeStatus } from './Status' -import { MixnodeRowType } from '.' - -interface MixNodeDetailProps { - mixNodeRow: MixnodeRowType - mixnodeDescription: MixNodeDescriptionResponse -} - -export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ - mixNodeRow, - mixnodeDescription, -}) => { - const theme = useTheme() - const palette = [theme.palette.text.primary] - const isMobile = useIsMobile() - const statusText = React.useMemo( - () => getMixNodeStatusText(mixNodeRow.status), - [mixNodeRow.status] - ) - - return ( - <Grid container> - <Grid item xs={12} md={6}> - <Box - display="flex" - flexDirection={isMobile ? 'column' : 'row'} - width="100%" - > - <Box - width={72} - height={72} - sx={{ - minWidth: 72, - minHeight: 72, - borderWidth: 1, - borderColor: theme.palette.text.primary, - borderStyle: 'solid', - borderRadius: '50%', - display: 'grid', - placeItems: 'center', - }} - > - <Identicon - size={43} - string={mixNodeRow.identity_key} - palette={palette} - /> - </Box> - <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: isMobile ? 2 : 4, - borderRadius: '30px', - fontWeight: 600, - padding: 0, - }} - href={mixnodeDescription.link} - target="_blank" - > - <Typography - component="span" - textOverflow="ellipsis" - whiteSpace="nowrap" - overflow="hidden" - maxWidth="250px" - > - {mixnodeDescription.link} - </Typography> - </Button> - </Box> - </Box> - </Grid> - <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={isMobile ? 'start' : 'self-end'} - > - Node status: - </Typography> - <Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}> - <MixNodeStatus status={mixNodeRow.status} /> - </Box> - <Typography - mt={1} - alignSelf={isMobile ? 'start' : 'self-end'} - color={theme.palette.text.secondary} - fontSize="smaller" - > - This node is {statusText} in this epoch - </Typography> - </Box> - </Grid> - </Grid> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts b/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts deleted file mode 100644 index b2115c0749..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ColumnsType } from '../../DetailTable'; - -export const EconomicsInfoColumns: ColumnsType[] = [ - { - field: 'estimatedTotalReward', - title: 'Estimated Total Reward', - 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', - 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', - 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: 'profitMargin', - title: 'Profit Margin', - 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', - 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: 'nodePerformance', - title: 'Routing Score', - width: '10%', - 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.", - }, - { - field: 'avgUptime', - title: 'Avg. Score', - tooltipInfo: "Mixnode's average routing score in the last 24 hour", - }, -]; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx b/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx deleted file mode 100644 index aef36113df..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { EconomicsProgress } from './EconomicsProgress'; - -export default { - title: 'Mix Node Detail/Economics/ProgressBar', - component: EconomicsProgress, -} as ComponentMeta<typeof EconomicsProgress>; - -const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />; - -export const Empty = Template.bind({}); -Empty.args = {}; - -export const OverThreshold = Template.bind({}); -OverThreshold.args = { - threshold: 100, - value: 120, -}; - -export const UnderThreshold = Template.bind({}); -UnderThreshold.args = { - threshold: 100, - value: 80, -}; - -export const OnThreshold = Template.bind({}); -OnThreshold.args = { - threshold: 100, - value: 100, -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx b/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx deleted file mode 100644 index 24db62d167..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/system'; - -const parseToNumber = (value: number | undefined | string) => - typeof value === 'string' ? parseInt(value || '', 10) : value || 0; - -export const EconomicsProgress: FCWithChildren< - LinearProgressProps & { - threshold?: number; - color: string; - } -> = ({ threshold, color, ...props }) => { - const theme = useTheme(); - const { value } = props; - - const valueNumber: number = parseToNumber(value); - const thresholdNumber: number = parseToNumber(threshold); - const percentageToDisplay = Math.min(valueNumber, thresholdNumber); - - return ( - <Box - sx={{ - width: 6 / 10, - color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee, - }} - > - <LinearProgress - {...props} - variant="determinate" - color={color} - value={percentageToDisplay} - sx={{ width: '100%', borderRadius: '5px' }} - /> - </Box> - ); -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx deleted file mode 100644 index c9c82c0438..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { DelegatorsInfoTable } from './Table'; -import { EconomicsInfoColumns } from './Columns'; -import { EconomicsInfoRowWithIndex } from './types'; - -export default { - title: 'Mix Node Detail/Economics', - component: DelegatorsInfoTable, -} as ComponentMeta<typeof DelegatorsInfoTable>; - -const row: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: 'High', - }, - - estimatedOperatorReward: { - value: '80000.123456 NYM', - }, - estimatedTotalReward: { - value: '80000.123456 NYM', - }, - profitMargin: { - value: '10 %', - }, - operatingCost: { - value: '11121 NYM', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Good', - }, -}; - -const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Low', - }, -}; - -const emptyRow: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: '-', - progressBarValue: 0, - }, - - estimatedOperatorReward: { - value: '-', - }, - estimatedTotalReward: { - value: '-', - }, - profitMargin: { - value: '-', - }, - operatingCost: { - value: '-', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />; - -export const Empty = Template.bind({}); -Empty.args = { - rows: [emptyRow], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceHigh = Template.bind({}); -selectionChanceHigh.args = { - rows: [row], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceGood = Template.bind({}); -selectionChanceGood.args = { - rows: [rowGoodProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceLow = Template.bind({}); -selectionChanceLow.args = { - rows: [rowLowProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts b/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts deleted file mode 100644 index 62b27cf229..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { currencyToString, unymToNym } from '@/app/utils/currency'; -import { useMixnodeContext } from '@/app/context/mixnode'; -import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '@/app/typeDefs/explorer-api'; -import { toPercentIntegerString } from '@/app/utils'; -import { EconomicsInfoRowWithIndex } from './types'; - -const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => - economicDynamicsStats?.data?.active_set_inclusion_probability || '-'; - -export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { - const { economicDynamicsStats, mixNode } = useMixnodeContext(); - - const estimatedNodeRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_total_node_reward.toString() || '', - }) || '-'; - const estimatedOperatorRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_operator_reward.toString() || '', - }) || '-'; - const profitMargin = mixNode?.data?.profit_margin_percent - ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) - : '-'; - 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; - - return { - id: 1, - estimatedTotalReward: { - value: estimatedNodeRewards, - }, - estimatedOperatorReward: { - value: estimatedOperatorRewards, - }, - selectionChance: { - value: selectionChance(economicDynamicsStats), - }, - profitMargin: { - value: profitMargin ? `${profitMargin} %` : '-', - }, - operatingCost: { - value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-', - }, - avgUptime: { - value: avgUptime ? `${avgUptime} %` : '-', - }, - nodePerformance: { - value: nodePerformance, - }, - }; -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx b/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx deleted file mode 100644 index e497a72edd..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { useIsMobile } from '@/app/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> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx b/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx deleted file mode 100644 index a70a013a24..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react' -import { - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, -} from '@mui/material' -import { Box } from '@mui/system' -import { useTheme } from '@mui/material/styles' -import { Tooltip } from '@nymproject/react/tooltip/Tooltip' -import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types' -import { UniversalTableProps } from '@/app/components/DetailTable' -import { textColour } from '@/app/utils' - -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, columnsData, rows }) => { - const theme = useTheme() - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, tooltipInfo, width }) => ( - <TableCell - key={field} - sx={{ fontSize: 14, fontWeight: 600, width }} - > - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <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> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows?.map((eachRow) => ( - <TableRow - key={eachRow.id} - sx={{ '&:last-child td, &:last-child th': { border: 0 } }} - > - {columnsData?.map((_, index: number) => { - const { field } = columnsData[index] - const value: EconomicsRowsType = (eachRow as any)[field] - return ( - <TableCell - key={_.title} - sx={{ - color: textColour(value, field, theme), - }} - data-testid={`${_.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(value, columnsData[index].field)} - </TableCell> - ) - })} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/index.ts b/explorer-nextjs/app/components/MixNodes/Economics/index.ts deleted file mode 100644 index 6dec752246..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { DelegatorsInfoTable } from './Table'; -export { EconomicsInfoColumns } from './Columns'; -export { EconomicsInfoRows } from './Rows'; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/types.ts b/explorer-nextjs/app/components/MixNodes/Economics/types.ts deleted file mode 100644 index 0bc550253f..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type EconomicsRowsType = { - progressBarValue?: number; - value: string; -}; - -type TEconomicsInfoProperties = - | 'estimatedTotalReward' - | 'estimatedOperatorReward' - | 'estimatedOperatorReward' - | 'selectionChance' - | 'profitMargin' - | 'avgUptime' - | 'nodePerformance' - | 'operatingCost'; - -export type EconomicsInfoRow = { - [k in TEconomicsInfoProperties]: EconomicsRowsType; -}; - -export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number }; diff --git a/explorer-nextjs/app/components/MixNodes/Status.tsx b/explorer-nextjs/app/components/MixNodes/Status.tsx deleted file mode 100644 index bc8f1fe660..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Status.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react' -import { Typography } from '@mui/material' -import { getMixNodeIcon } from '@/app/components/Icons' -import { MixnodeStatus } from '@/app/typeDefs/explorer-api' -import { useGetMixNodeStatusColor } from '@/app/hooks/useGetMixnodeStatusColor' - -interface MixNodeStatusProps { - status: MixnodeStatus -} -// TODO: should be done with i18n -export const getMixNodeStatusText = (status: MixnodeStatus) => { - switch (status) { - case MixnodeStatus.active: - return 'active' - case MixnodeStatus.standby: - return 'on standby' - default: - return 'inactive' - } -} - -export const MixNodeStatus: FCWithChildren<MixNodeStatusProps> = ({ - status, -}) => { - const Icon = React.useMemo(() => getMixNodeIcon(status), [status]) - const color = useGetMixNodeStatusColor(status) - - return ( - <Typography color={color} display="flex" alignItems="center"> - <Icon /> - <Typography ml={1} component="span" color="inherit"> - {`${status[0].toUpperCase()}${status.slice(1)}`} - </Typography> - </Typography> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx b/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx deleted file mode 100644 index dc505869ed..0000000000 --- a/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from 'react' -import { MenuItem } from '@mui/material' -import Select from '@mui/material/Select' -import { SelectChangeEvent } from '@mui/material/Select/SelectInput' -import { SxProps } from '@mui/system' -import { - MixnodeStatus, - MixnodeStatusWithAll, -} from '@/app/typeDefs/explorer-api' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MixNodeStatus } from './Status' - -// TODO: replace with i18n -const ALL_NODES = 'All nodes' - -interface MixNodeStatusDropdownProps { - status?: MixnodeStatusWithAll - sx?: SxProps - onSelectionChanged?: (status?: MixnodeStatusWithAll) => void -} - -export const MixNodeStatusDropdown: FCWithChildren< - MixNodeStatusDropdownProps -> = ({ status, onSelectionChanged, sx }) => { - const isMobile = useIsMobile() - const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>( - status || MixnodeStatusWithAll.all - ) - const onChange = React.useCallback( - (event: SelectChangeEvent) => { - setStatusValue(event.target.value as MixnodeStatusWithAll) - if (onSelectionChanged) { - onSelectionChanged(event.target.value as MixnodeStatusWithAll) - } - }, - [onSelectionChanged] - ) - - return ( - <Select - labelId="mixnodeStatusSelect_label" - id="mixnodeStatusSelect" - value={statusValue} - onChange={onChange} - renderValue={(value) => { - switch (value) { - case 'active': - case 'standby': - case 'inactive': - return <MixNodeStatus status={value as unknown as MixnodeStatus} /> - default: - return ALL_NODES - } - }} - sx={{ - width: isMobile ? '50%' : 200, - ...sx, - }} - > - <MenuItem - value={MixnodeStatus.active} - data-testid="mixnodeStatusSelectOption_active" - > - <MixNodeStatus status={MixnodeStatus.active} /> - </MenuItem> - <MenuItem - value={MixnodeStatus.standby} - data-testid="mixnodeStatusSelectOption_standby" - > - <MixNodeStatus status={MixnodeStatus.standby} /> - </MenuItem> - <MenuItem - value={MixnodeStatus.inactive} - data-testid="mixnodeStatusSelectOption_inactive" - > - <MixNodeStatus status={MixnodeStatus.inactive} /> - </MenuItem> - <MenuItem value={'all'} data-testid="mixnodeStatusSelectOption_allNodes"> - {ALL_NODES} - </MenuItem> - </Select> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/index.ts b/explorer-nextjs/app/components/MixNodes/index.ts deleted file mode 100644 index ccb6c5a8b3..0000000000 --- a/explorer-nextjs/app/components/MixNodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Status'; -export * from './StatusDropdown'; -export * from './mappings'; diff --git a/explorer-nextjs/app/components/MixNodes/mappings.ts b/explorer-nextjs/app/components/MixNodes/mappings.ts deleted file mode 100644 index df8558d39b..0000000000 --- a/explorer-nextjs/app/components/MixNodes/mappings.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable camelcase */ -import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; -import { toPercentInteger, toPercentIntegerString } from '@/app/utils'; -import { unymToNym } from '@/app/utils/currency'; - -export type MixnodeRowType = { - mix_id: number; - id: string; - status: MixnodeStatus; - owner: string; - location: string; - identity_key: string; - bond: number; - self_percentage: string; - pledge_amount: number; - host: string; - layer: string; - profit_percentage: number; - avg_uptime: string; - stake_saturation: React.ReactNode; - operating_cost: number; - node_performance: number; - blacklisted: boolean; -}; - -export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { - return (arrayOfMixnodes || []).map(mixNodeResponseItemToMixnodeRowType); -} - -export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): MixnodeRowType { - const pledge = Number(item.pledge_amount.amount) || 0; - const delegations = Number(item.total_delegation.amount) || 0; - const totalBond = pledge + delegations; - const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); - const profitPercentage = toPercentInteger(item.profit_margin_percent) || 0; - const uncappedSaturation = typeof item.uncapped_saturation === 'number' ? item.uncapped_saturation * 100 : 0; - - return { - mix_id: item.mix_id, - id: item.owner, - status: item.status, - owner: item.owner, - identity_key: item.mix_node.identity_key || '', - bond: totalBond || 0, - location: item?.location?.country_name || '', - self_percentage: selfPercentage, - pledge_amount: pledge, - host: item?.mix_node?.host || '', - layer: item?.layer || '', - profit_percentage: profitPercentage, - avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`, - stake_saturation: Number(uncappedSaturation.toFixed(2)), - operating_cost: Number(unymToNym(item.operating_cost?.amount, 6)) || 0, - node_performance: toPercentInteger(item.node_performance.most_recent), - blacklisted: item.blacklisted, - }; -} diff --git a/explorer-nextjs/app/components/Nav/DesktopNav.tsx b/explorer-nextjs/app/components/Nav/DesktopNav.tsx deleted file mode 100644 index e1faab46f1..0000000000 --- a/explorer-nextjs/app/components/Nav/DesktopNav.tsx +++ /dev/null @@ -1,379 +0,0 @@ -'use client' - -import * as React from 'react' -import { ExpandLess, ExpandMore, Menu } from '@mui/icons-material' -import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles' -import { Link as MuiLink } from '@mui/material' -import Button from '@mui/material/Button' -import Box from '@mui/material/Box' -import ListItem from '@mui/material/ListItem' -import MuiDrawer from '@mui/material/Drawer' -import AppBar from '@mui/material/AppBar' -import Toolbar from '@mui/material/Toolbar' -import Typography from '@mui/material/Typography' -import List from '@mui/material/List' -import IconButton from '@mui/material/IconButton' -import ListItemButton from '@mui/material/ListItemButton' -import ListItemIcon from '@mui/material/ListItemIcon' -import ListItemText from '@mui/material/ListItemText' -import { NYM_WEBSITE } from '@/app/api/constants' -import { useMainContext } from '@/app/context/main' -import { MobileDrawerClose } from '@/app/icons/MobileDrawerClose' -import { NavOptionType, originalNavOptions } from '@/app/context/nav' -import { ReleaseAlert } from '@/app/components/ReleaseAlert' -import { DarkLightSwitchDesktop } from '@/app/components/Switch' -import { Footer } from '@/app/components/Footer' -import { ConnectKeplrWallet } from '@/app/components/Wallet/ConnectKeplrWallet' -import { usePathname, useRouter } from 'next/navigation' -import {SearchToolbar} from "@/app/components/Nav/Search"; - -const drawerWidth = 255 -const bannerHeight = 80 - -const openedMixin = (theme: Theme): CSSObject => ({ - width: drawerWidth, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - overflowX: 'hidden', -}) - -const closedMixin = (theme: Theme): CSSObject => ({ - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - overflowX: 'hidden', - width: `calc(${theme.spacing(7)} + 1px)`, -}) - -const DrawerHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - padding: theme.spacing(0, 1), - height: 64, -})) - -const Drawer = styled(MuiDrawer, { - shouldForwardProp: (prop) => prop !== 'open', -})(({ theme, open }) => ({ - width: drawerWidth, - flexShrink: 0, - whiteSpace: 'nowrap', - boxSizing: 'border-box', - ...(open && { - ...openedMixin(theme), - '& .MuiDrawer-paper': openedMixin(theme), - }), - ...(!open && { - ...closedMixin(theme), - '& .MuiDrawer-paper': closedMixin(theme), - }), -})) - -type ExpandableButtonType = { - title: string - url: string - isActive?: boolean - Icon?: React.ReactNode - nested?: NavOptionType[] - isChild?: boolean - isMobile: boolean - drawIsTempOpen: boolean - drawIsFixed: boolean - isExternalLink?: boolean - openDrawer: () => void - closeDrawer?: () => void - fixDrawerClose?: () => void -} - -export const ExpandableButton: FCWithChildren<ExpandableButtonType> = ({ - title, - url, - drawIsTempOpen, - drawIsFixed, - Icon, - nested, - isMobile, - isChild, - isExternalLink, - openDrawer, - closeDrawer, - fixDrawerClose, -}) => { - const { palette } = useTheme() - const pathname = usePathname() - const router = useRouter() - - const handleClick = () => { - if (title === 'Network Components') { - return undefined - } - - if (isExternalLink) { - window.open(url, '_blank') - - return undefined - } - - if (!isExternalLink) { - router.push(url, {}) - } - - if (closeDrawer) { - closeDrawer() - } - } - const selectedStyle = { - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - } - - return ( - <> - <ListItem - disablePadding - disableGutters - sx={{ - borderBottom: isChild ? 'none' : '1px solid rgba(255, 255, 255, 0.1)', - ...(pathname === url - ? selectedStyle - : { - background: palette.nym.networkExplorer.nav.background, - borderRight: 'none', - }), - }} - > - <ListItemButton - onClick={() => handleClick()} - sx={{ - pt: 2, - pb: 2, - background: isChild - ? palette.nym.networkExplorer.nav.selected.nested - : 'none', - }} - > - <ListItemIcon sx={{ minWidth: '39px' }}>{Icon}</ListItemIcon> - <ListItemText - primary={title} - sx={{ - color: palette.nym.networkExplorer.nav.text, - }} - /> - </ListItemButton> - </ListItem> - {nested?.map((each) => ( - <ExpandableButton - url={each.url} - key={each.title} - title={each.title} - openDrawer={openDrawer} - drawIsTempOpen={drawIsTempOpen} - closeDrawer={closeDrawer} - drawIsFixed={drawIsFixed} - fixDrawerClose={fixDrawerClose} - isMobile={isMobile} - isChild - isExternalLink={each.isExternal} - /> - ))} - </> - ) -} - -export const Nav: FCWithChildren = ({ children }) => { - const { environment } = useMainContext() - const [drawerIsOpen, setDrawerToOpen] = React.useState(false) - const [fixedOpen, setFixedOpen] = React.useState(false) - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false) - const theme = useTheme() - - const explorerName = environment - ? `${environment} Explorer` - : 'Mainnet Explorer' - - const switchNetworkText = - environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet' - const switchNetworkLink = - environment === 'mainnet' - ? 'https://sandbox-explorer.nymtech.net' - : 'https://explorer.nymtech.net' - - const fixDrawerOpen = () => { - setFixedOpen(true) - setDrawerToOpen(true) - } - - const fixDrawerClose = () => { - setFixedOpen(false) - setDrawerToOpen(false) - } - - const tempDrawerOpen = () => { - if (!fixedOpen) { - setDrawerToOpen(true) - } - } - - const tempDrawerClose = () => { - if (!fixedOpen) { - setDrawerToOpen(false) - } - } - - return ( - <Box sx={{ display: 'flex' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <Toolbar - disableGutters - sx={{ - display: 'flex', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - ml: 0.5, - }} - > - <IconButton component="a" href={NYM_WEBSITE} target="_blank"> - {/* <NymLogo /> */} - </IconButton> - <Typography - variant="h6" - noWrap - sx={{ - color: theme.palette.nym.networkExplorer.nav.text, - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink - href="/" - underline="none" - color="inherit" - textTransform="capitalize" - > - {explorerName} - </MuiLink> - <Button - size="small" - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ - borderRadius: 2, - textTransform: 'none', - width: 150, - ml: 4, - fontSize: 14, - fontWeight: 600, - }} - > - {switchNetworkText} - </Button> - </Typography> - </Box> - <Box - sx={{ - mr: 2, - alignItems: 'center', - display: 'flex', - }} - > - <Box> - <SearchToolbar/> - </Box> - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - pr: 0, - pl: 2, - justifyContent: 'flex-end', - alignItems: 'center', - }} - > - <Box sx={{ mr: 1 }}> - <ConnectKeplrWallet /> - </Box> - <DarkLightSwitchDesktop defaultChecked /> - </Box> - </Box> - </Toolbar> - </AppBar> - <Drawer - variant="permanent" - open={true} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - borderRadius: 0, - top: openMaintenance ? bannerHeight : 0, - }, - }} - > - <DrawerHeader - sx={{ - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - justifyContent: 'flex-start', - paddingLeft: 0, - display: 'none', - }} - > - <IconButton - onClick={drawerIsOpen ? fixDrawerClose : fixDrawerOpen} - sx={{ - padding: 1, - ml: 1, - color: theme.palette.nym.networkExplorer.nav.text, - }} - > - {drawerIsOpen ? <MobileDrawerClose /> : <Menu />} - </IconButton> - </DrawerHeader> - - <List - sx={{ pb: 0 }} - onMouseEnter={tempDrawerOpen} - onMouseLeave={tempDrawerClose} - > - {originalNavOptions.map((props) => ( - <ExpandableButton - key={props.url} - closeDrawer={tempDrawerClose} - drawIsTempOpen={drawerIsOpen} - drawIsFixed={fixedOpen} - fixDrawerClose={fixDrawerClose} - openDrawer={tempDrawerOpen} - isMobile={false} - {...props} - /> - ))} - </List> - </Drawer> - <Box - style={{ width: `calc(100% - ${drawerWidth}px` }} - sx={{ py: 5, px: 6, mt: 7 }} - > - <ReleaseAlert /> - {children} - <Footer /> - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Nav/MobileNav.tsx b/explorer-nextjs/app/components/Nav/MobileNav.tsx deleted file mode 100644 index 3d1cfdcbf9..0000000000 --- a/explorer-nextjs/app/components/Nav/MobileNav.tsx +++ /dev/null @@ -1,147 +0,0 @@ -'use client' - -import * as React from 'react' -import { useTheme } from '@mui/material/styles' -import { - AppBar, - Box, - Drawer, - IconButton, - List, - ListItem, - ListItemButton, - ListItemIcon, - Toolbar, -} from '@mui/material' -import { Menu } from '@mui/icons-material' -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MobileDrawerClose } from '@/app/icons/MobileDrawerClose' -import { Footer } from '../Footer' -import { ExpandableButton } from './DesktopNav' -import { ConnectKeplrWallet } from '../Wallet/ConnectKeplrWallet' -import { NetworkTitle } from '../NetworkTitle' -import { originalNavOptions } from '@/app/context/nav' -import { ReleaseAlert } from '@/app/components/ReleaseAlert' -import {SearchToolbar} from "@/app/components/Nav/Search"; - -export const MobileNav: FCWithChildren = ({ children }) => { - const theme = useTheme() - const [drawerOpen, setDrawerOpen] = React.useState(false) - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false) - const isSmallMobile = useIsMobile(400) - - const toggleDrawer = () => { - setDrawerOpen(!drawerOpen) - } - - const openDrawer = () => { - setDrawerOpen(true) - } - - return ( - <Box sx={{ display: 'flex', flexDirection: 'column' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner - open={openMaintenance} - onClick={() => setOpenMaintenance(false)} - /> - <Toolbar - sx={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }} - > - <IconButton onClick={toggleDrawer}> - <Menu sx={{ color: 'primary.contrastText' }} /> - </IconButton> - {!isSmallMobile && <NetworkTitle />} - </Box> - <Box sx={{ - alignItems: 'center', - display: 'flex', - }}> - <Box mr={0.5}> - <SearchToolbar/> - </Box> - <ConnectKeplrWallet /> - </Box> - </Toolbar> - </AppBar> - <Drawer - anchor="left" - open={drawerOpen} - onClose={toggleDrawer} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - }, - }} - > - <Box role="presentation"> - <List sx={{ pt: 0, pb: 0 }}> - <ListItem - disablePadding - disableGutters - sx={{ - height: 64, - background: theme.palette.nym.networkExplorer.nav.background, - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - }} - > - <ListItemButton - onClick={toggleDrawer} - sx={{ - pt: 2, - pb: 2, - background: theme.palette.nym.networkExplorer.nav.background, - display: 'flex', - justifyContent: 'flex-start', - }} - > - <ListItemIcon> - <MobileDrawerClose /> - </ListItemIcon> - </ListItemButton> - </ListItem> - {originalNavOptions.map((props) => ( - <ExpandableButton - key={props.url} - title={props.title} - openDrawer={openDrawer} - url={props.url} - drawIsTempOpen={false} - drawIsFixed={false} - Icon={props.Icon} - nested={props.nested} - closeDrawer={toggleDrawer} - isMobile - /> - ))} - </List> - </Box> - </Drawer> - - <Box sx={{ width: '100%', p: 4, mt: 7 }}> - <ReleaseAlert /> - {children} - <Footer /> - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Nav/Navbar.tsx b/explorer-nextjs/app/components/Nav/Navbar.tsx deleted file mode 100644 index 15bcd47566..0000000000 --- a/explorer-nextjs/app/components/Nav/Navbar.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client' - -import React from 'react' -import { useIsMobile } from '@/app/hooks' -import { MobileNav } from './MobileNav' -import { Nav } from './DesktopNav' - -const Navbar = ({ children }: { children: React.ReactNode }) => { - const isMobile = useIsMobile() - - if (isMobile) { - return <MobileNav>{children}</MobileNav> - } - - return <Nav>{children}</Nav> -} - -export { Navbar } diff --git a/explorer-nextjs/app/components/Nav/Search.tsx b/explorer-nextjs/app/components/Nav/Search.tsx deleted file mode 100644 index 888125b84a..0000000000 --- a/explorer-nextjs/app/components/Nav/Search.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import { styled, alpha } from '@mui/material/styles'; -import AppBar from '@mui/material/AppBar'; -import Box from '@mui/material/Box'; -import Toolbar from '@mui/material/Toolbar'; -import IconButton from '@mui/material/IconButton'; -import Typography from '@mui/material/Typography'; -import InputBase from '@mui/material/InputBase'; -import MenuIcon from '@mui/icons-material/Menu'; -import SearchIcon from '@mui/icons-material/Search'; -import {useRouter} from "next/navigation"; - -const Search = styled('div')(({ theme }) => ({ - position: 'relative', - borderRadius: theme.shape.borderRadius, - backgroundColor: alpha(theme.palette.common.white, 0.15), - '&:hover': { - backgroundColor: alpha(theme.palette.common.white, 0.25), - }, - marginLeft: 0, - width: '100%', - [theme.breakpoints.up('sm')]: { - marginLeft: theme.spacing(1), - width: 'auto', - }, -})); - -const SearchIconWrapper = styled('div')(({ theme }) => ({ - padding: theme.spacing(0, 2), - height: '100%', - position: 'absolute', - pointerEvents: 'none', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -})); - -const StyledInputBase = styled(InputBase)(({ theme }) => ({ - color: 'inherit', - width: '100%', - '& .MuiInputBase-input': { - padding: theme.spacing(1, 1, 1, 0), - // vertical padding + font size from searchIcon - paddingLeft: `calc(1em + ${theme.spacing(4)})`, - [theme.breakpoints.up('sm')]: { - width: '30ch', - }, - }, -})); - -export const SearchToolbar = () => { - const [search, setSearch] = React.useState<string>(); - const router = useRouter(); - const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => { - e.preventDefault(); - if(search?.trim().length) { - router.push(`/account/${search.trim()}`); - } - } - return ( - <Search> - <SearchIconWrapper> - <SearchIcon /> - </SearchIconWrapper> - <form onSubmit={handleSubmit}> - <StyledInputBase - placeholder="Search for account id…" - inputProps={{ 'aria-label': 'search' }} - onChange={(event: React.ChangeEvent<HTMLInputElement>) => { - setSearch(event.target.value); - }} - /> - </form> - </Search> - ); -} \ No newline at end of file diff --git a/explorer-nextjs/app/components/NetworkTitle.tsx b/explorer-nextjs/app/components/NetworkTitle.tsx deleted file mode 100644 index 0c450007da..0000000000 --- a/explorer-nextjs/app/components/NetworkTitle.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react' -import { Button, Typography, Link as MuiLink } from '@mui/material' -import { useMainContext } from '@/app/context/main' - -type NetworkTitleProps = { - showToggleNetwork?: boolean -} - -const NetworkTitle = ({ showToggleNetwork }: NetworkTitleProps) => { - const { environment } = useMainContext() - - const explorerName = - `${ - environment && environment.charAt(0).toUpperCase() + environment.slice(1) - } Explorer` || 'Mainnet Explorer' - - const switchNetworkText = - environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet' - const switchNetworkLink = - environment === 'mainnet' - ? 'https://sandbox-explorer.nymtech.net' - : 'https://explorer.nymtech.net' - return ( - <Typography - variant="h6" - noWrap - sx={{ - color: 'nym.networkExplorer.nav.text', - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink href="/" underline="none" color="inherit" fontWeight={700}> - {explorerName} - </MuiLink> - - {showToggleNetwork && ( - <Button - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ - textTransform: 'none', - width: 114, - fontSize: '12px', - fontWeight: 600, - ml: 1, - }} - > - {switchNetworkText} - </Button> - )} - </Typography> - ) -} - -export { NetworkTitle } diff --git a/explorer-nextjs/app/components/ReleaseAlert.tsx b/explorer-nextjs/app/components/ReleaseAlert.tsx deleted file mode 100644 index 5d974420f9..0000000000 --- a/explorer-nextjs/app/components/ReleaseAlert.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Alert, Box, Typography } from '@mui/material'; - -export const ReleaseAlert = () => ( - <Alert severity="warning" sx={{ mb: 3, fontSize: 'medium', width: '100%' }}> - <Box> - <Typography>You are now viewing the legacy Nym mixnet explorer. Explorer 2.0 is coming soon.</Typography> - </Box> - </Alert> -); diff --git a/explorer-nextjs/app/components/Socials.tsx b/explorer-nextjs/app/components/Socials.tsx deleted file mode 100644 index 4a4856c8f2..0000000000 --- a/explorer-nextjs/app/components/Socials.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' -import { Box, IconButton } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { TelegramIcon } from '../icons/socials/TelegramIcon' -import { GitHubIcon } from '../icons/socials/GitHubIcon' -import { TwitterIcon } from '../icons/socials/TwitterIcon' -import { DiscordIcon } from '../icons/socials/DiscordIcon' - -// socials -export const TELEGRAM_LINK = 'https://nymtech.net/go/telegram'; -export const TWITTER_LINK = 'https://nymtech.net/go/x'; -export const GITHUB_LINK = 'https://nymtech.net/go/github'; -export const DISCORD_LINK = 'https://nymtech.net/go/discord'; - -export const Socials: FCWithChildren<{ isFooter?: boolean }> = ({ - isFooter = false, -}) => { - const theme = useTheme() - const color = isFooter - ? theme.palette.nym.networkExplorer.footer.socialIcons - : theme.palette.nym.networkExplorer.topNav.socialIcons - return ( - <Box> - <IconButton - component="a" - href={TELEGRAM_LINK} - target="_blank" - data-testid="telegram" - > - <TelegramIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={DISCORD_LINK} - target="_blank" - data-testid="discord" - > - <DiscordIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={TWITTER_LINK} - target="_blank" - data-testid="twitter" - > - <TwitterIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={GITHUB_LINK} - target="_blank" - data-testid="github" - > - <GitHubIcon color={color} size={24} /> - </IconButton> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/StatsCard.tsx b/explorer-nextjs/app/components/StatsCard.tsx deleted file mode 100644 index 2c87998986..0000000000 --- a/explorer-nextjs/app/components/StatsCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react' -import { Box, Card, CardContent, IconButton, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import EastIcon from '@mui/icons-material/East' - -interface StatsCardProps { - icon: React.ReactNode - title: string - count?: string | number - errorMsg?: Error | string - onClick?: () => void - color?: string -} -export const StatsCard: FCWithChildren<StatsCardProps> = ({ - icon, - title, - count, - onClick, - errorMsg, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <Card onClick={onClick} sx={{ height: '100%' }}> - <CardContent - sx={{ - padding: 1.5, - paddingLeft: 3, - '&:last-child': { - paddingBottom: 1.5, - }, - cursor: 'pointer', - fontSize: 14, - fontWeight: 600, - }} - > - <Box display="flex" alignItems="center" color={color}> - <Box display="flex"> - {icon} - <Typography - ml={3} - mr={0.75} - fontSize="inherit" - fontWeight="inherit" - data-testid={`${title}-amount`} - > - {count === undefined || count === null ? '' : count} - </Typography> - <Typography - mr={1} - fontSize="inherit" - fontWeight="inherit" - data-testid={title} - > - {title} - </Typography> - </Box> - <IconButton color="inherit" sx={{ fontSize: '16px' }}> - <EastIcon fontSize="inherit" /> - </IconButton> - </Box> - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {typeof errorMsg === 'string' - ? errorMsg - : errorMsg.message || 'Oh no! An error occurred'} - </Typography> - )} - </CardContent> - </Card> - ) -} diff --git a/explorer-nextjs/app/components/StyledLink.tsx b/explorer-nextjs/app/components/StyledLink.tsx deleted file mode 100644 index 36dd798180..0000000000 --- a/explorer-nextjs/app/components/StyledLink.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { Link as MuiLink, SxProps, Typography } from '@mui/material' -import Link from 'next/link' - -type StyledLinkProps = { - to: string - children: React.ReactNode - target?: React.HTMLAttributeAnchorTarget - dataTestId?: string - color?: string - sx?: SxProps -} - -const StyledLink = ({ - to, - children, - dataTestId, - target, - color, - sx, -}: StyledLinkProps) => ( - <Link - href={to} - target={target} - data-testid={dataTestId} - style={{ textDecoration: 'none' }} - > - <Typography component="a" sx={{ ...sx }} color={color}> - {children} - </Typography> - </Link> -) - -export default StyledLink diff --git a/explorer-nextjs/app/components/Switch.tsx b/explorer-nextjs/app/components/Switch.tsx deleted file mode 100644 index 840530a327..0000000000 --- a/explorer-nextjs/app/components/Switch.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import Switch from '@mui/material/Switch'; -import { Button } from '@mui/material'; -import { useMainContext } from '../context/main'; -import { LightSwitchSVG } from '../icons/LightSwitchSVG'; - -export const DarkLightSwitch = styled(Switch)(({ theme }) => ({ - width: 55, - height: 34, - padding: 7, - '& .MuiSwitch-switchBase': { - margin: 1, - padding: 2, - transform: 'translateX(4px)', - '&.Mui-checked': { - color: '#fff', - transform: 'translateX(22px)', - '& .MuiSwitch-thumb:before': { - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')', - }, - '& + .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - }, - }, - }, - '& .MuiSwitch-thumb': { - backgroundColor: theme.palette.nym.networkExplorer.nav.text, - width: 25, - height: 25, - marginTop: '2px', - '&:before': { - content: "''", - position: 'absolute', - width: '100%', - height: '100%', - left: 0, - top: 0, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')', - }, - }, - '& .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - borderRadius: 20 / 2, - }, -})); - -export const DarkLightSwitchMobile: FCWithChildren = () => { - const { toggleMode } = useMainContext(); - return ( - <Button onClick={() => toggleMode()} data-testid="switch-button" sx={{ p: 0, minWidth: 0 }}> - <LightSwitchSVG /> - </Button> - ); -}; - -export const DarkLightSwitchDesktop: FCWithChildren<{ defaultChecked: boolean }> = ({ defaultChecked }) => { - const { toggleMode } = useMainContext(); - return ( - <Button sx={{ paddingLeft: 0 }} onClick={() => toggleMode()} data-testid="switch-button"> - <DarkLightSwitch defaultChecked={defaultChecked} /> - </Button> - ); -}; diff --git a/explorer-nextjs/app/components/TableToolbar.tsx b/explorer-nextjs/app/components/TableToolbar.tsx deleted file mode 100644 index f22b84aca9..0000000000 --- a/explorer-nextjs/app/components/TableToolbar.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client' - -import React from 'react' -import { Box, SelectChangeEvent } from '@mui/material' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { Filters } from './Filters/Filters' - -const fieldsHeight = '42.25px' - -type TableToolBarProps = { - childrenBefore?: React.ReactNode - childrenAfter?: React.ReactNode -} - -export const TableToolbar: FCWithChildren<TableToolBarProps> = ({ - childrenBefore, - childrenAfter, -}) => { - const isMobile = useIsMobile() - return ( - <Box - sx={{ - width: '100%', - marginBottom: 2, - display: 'flex', - flexDirection: isMobile ? 'column' : 'row', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: isMobile ? 'column-reverse' : 'row', - alignItems: 'middle', - }} - > - <Box - sx={{ - display: 'flex', - justifyContent: 'space-between', - height: fieldsHeight, - }} - > - {childrenBefore} - </Box> - </Box> - - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'end', - gap: 1, - marginTop: isMobile ? 2 : 0, - }} - > - {childrenAfter} - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Title.tsx b/explorer-nextjs/app/components/Title.tsx deleted file mode 100644 index 032837d3c8..0000000000 --- a/explorer-nextjs/app/components/Title.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { Typography } from '@mui/material'; - -export const Title: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - variant="h5" - sx={{ - fontWeight: 600, - }} - data-testid={text} - > - {text} - </Typography> -); diff --git a/explorer-nextjs/app/components/Tooltip.tsx b/explorer-nextjs/app/components/Tooltip.tsx deleted file mode 100644 index 0febd9f891..0000000000 --- a/explorer-nextjs/app/components/Tooltip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material'; - -type ValueType<T> = T[keyof T]; - -type Props = { - text: string; - id: string; - placement?: ValueType<Pick<TooltipProps, 'placement'>>; - tooltipSx?: TooltipComponentsPropsOverrides; - children: React.ReactNode; -}; - -export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => ( - <MUITooltip - title={text} - id={id} - placement={placement || 'top-start'} - componentsProps={{ - tooltip: { - sx: { - maxWidth: 200, - background: (t) => t.palette.nym.networkExplorer.tooltip.background, - color: (t) => t.palette.nym.networkExplorer.tooltip.color, - '& .MuiTooltip-arrow': { - color: (t) => t.palette.nym.networkExplorer.tooltip.background, - }, - }, - ...tooltipSx, - }, - }} - arrow - > - {children as ReactElement<any, any>} - </MUITooltip> -); diff --git a/explorer-nextjs/app/components/TwoColSmallTable.tsx b/explorer-nextjs/app/components/TwoColSmallTable.tsx deleted file mode 100644 index a211c29bdd..0000000000 --- a/explorer-nextjs/app/components/TwoColSmallTable.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp'; -import ErrorIcon from '@mui/icons-material/Error'; - -interface TableProps { - title?: string; - icons?: boolean[]; - keys: string[]; - values: number[]; - marginBottom?: boolean; - error?: string; - loading: boolean; -} - -export const TwoColSmallTable: FCWithChildren<TableProps> = ({ - loading, - title, - icons, - keys, - values, - marginBottom, - error, -}) => ( - <> - {title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>} - - <TableContainer component={Paper} sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}> - <Table aria-label="two col small table"> - <TableBody> - {keys.map((each: string, i: number) => ( - <TableRow key={each}> - {icons && <TableCell>{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}</TableCell>} - <TableCell sx={error ? { opacity: 0.4 } : null} data-testid={each.replace(/ /g, '')}> - {each} - </TableCell> - <TableCell - sx={error ? { opacity: 0.4 } : null} - align="right" - data-testid={`${each.replace(/ /g, '-')}-value`} - > - {values[i]} - </TableCell> - {error && ( - <TableCell align="right" sx={{ opacity: 0.4 }}> - {values[i]} - </TableCell> - )} - {!error && loading && ( - <TableCell align="right"> - <CircularProgress /> - </TableCell> - )} - {error && !icons && ( - <TableCell sx={{ opacity: 0.2 }} align="right"> - <ErrorIcon /> - </TableCell> - )} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </> -); - -TwoColSmallTable.defaultProps = { - title: undefined, - icons: undefined, - marginBottom: false, - error: undefined, -}; diff --git a/explorer-nextjs/app/components/Universal-DataGrid.tsx b/explorer-nextjs/app/components/Universal-DataGrid.tsx deleted file mode 100644 index 36e98be9ce..0000000000 --- a/explorer-nextjs/app/components/Universal-DataGrid.tsx +++ /dev/null @@ -1,96 +0,0 @@ -'use client' - -import * as React from 'react' -import { makeStyles } from '@mui/styles' -import { - DataGrid, - GridColDef, - GridEventListener, - useGridApiContext, -} from '@mui/x-data-grid' -import Pagination from '@mui/material/Pagination' -import { LinearProgress } from '@mui/material' -import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity' - -const useStyles = makeStyles({ - root: { - display: 'flex', - }, -}) - -const CustomPagination = () => { - const apiRef = useGridApiContext() - const classes = useStyles() - console.log(apiRef.current.state) - - return ( - <Pagination - className={classes.root} - sx={{ mt: 2 }} - color="primary" - count={apiRef.current.state.pagination.paginationModel.pageSize} - page={apiRef.current.state.pagination.paginationModel.page + 1} - onChange={(_, value) => apiRef.current.setPage(value - 1)} - /> - ) -} - -type DataGridProps = { - columns: GridColDef[] - pagination?: true | undefined - pageSize?: string | undefined - rows: any - loading?: boolean - initialState?: GridInitialStateCommunity - onRowClick?: GridEventListener<'rowClick'> | undefined -} -export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({ - rows, - columns, - loading, - pagination, - pageSize, - initialState, - onRowClick, -}) => { - if (loading) return <LinearProgress /> - - return ( - <DataGrid - onRowClick={onRowClick} - pagination={pagination} - rows={rows} - slots={{ - pagination: CustomPagination, - }} - columns={columns} - autoHeight - hideFooter={!pagination} - initialState={initialState} - style={{ - width: '100%', - border: 'none', - }} - sx={{ - '*::-webkit-scrollbar': { - width: '1em', - }, - '*::-webkit-scrollbar-track': { - background: (t) => t.palette.nym.networkExplorer.scroll.backgroud, - outline: (t) => - `1px solid ${t.palette.nym.networkExplorer.scroll.border}`, - boxShadow: 'auto', - borderRadius: 'auto', - }, - '*::-webkit-scrollbar-thumb': { - backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color, - borderRadius: '20px', - width: '.4em', - border: (t) => - `3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`, - shadow: 'auto', - }, - }} - /> - ) -} diff --git a/explorer-nextjs/app/components/UptimeChart.tsx b/explorer-nextjs/app/components/UptimeChart.tsx deleted file mode 100644 index 00a981a3af..0000000000 --- a/explorer-nextjs/app/components/UptimeChart.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Chart } from 'react-google-charts'; -import { format } from 'date-fns'; -import { ApiState, UptimeStoryResponse } from '../typeDefs/explorer-api'; - -interface ChartProps { - title?: string; - xLabel: string; - yLabel?: string; - uptimeStory: ApiState<UptimeStoryResponse>; - loading: boolean; -} - -type FormattedDateRecord = [string, number]; -type FormattedChartHeadings = string[]; -type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord]; - -export const UptimeChart: FCWithChildren<ChartProps> = ({ title, xLabel, yLabel, uptimeStory, loading }) => { - const [formattedChartData, setFormattedChartData] = React.useState<FormattedChartData>(); - const theme = useTheme(); - const color = theme.palette.text.primary; - React.useEffect(() => { - if (uptimeStory.data?.history) { - const allFormattedChartData: FormattedChartData = [['Date', 'Score']]; - uptimeStory.data.history.forEach((eachDate) => { - const formattedDateUptimeRecord: FormattedDateRecord = [ - format(new Date(eachDate.date), 'MMM dd'), - eachDate.uptime, - ]; - allFormattedChartData.push(formattedDateUptimeRecord); - }); - setFormattedChartData(allFormattedChartData); - } else { - const emptyData: any = [ - ['Date', 'Score'], - ['Jul 27', 10], - ]; - setFormattedChartData(emptyData); - } - }, [uptimeStory]); - - return ( - <> - {title && <Typography>{title}</Typography>} - {loading && <CircularProgress />} - - {!loading && uptimeStory && ( - <Chart - style={{ minHeight: 480 }} - chartType="LineChart" - loader={<p>...</p>} - data={ - uptimeStory.data - ? formattedChartData - : [ - ['Date', 'Routing Score'], - [format(new Date(Date.now()), 'MMM dd'), 0], - ] - } - options={{ - backgroundColor: - theme.palette.mode === 'dark' ? theme.palette.nym.networkExplorer.background.tertiary : undefined, - color: uptimeStory.error ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 1)', - colors: ['#FB7A21'], - legend: { - textStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - - intervals: { style: 'sticks' }, - hAxis: { - // horizontal / date - title: xLabel, - titleTextStyle: { - color, - }, - textStyle: { - color, - // fontSize: 11 - }, - gridlines: { - count: -1, - }, - }, - vAxis: { - // vertical / % Routing Score - viewWindow: { - min: 0, - max: 100, - }, - title: yLabel, - titleTextStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - textStyle: { - color, - fontSize: 11, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - }} - /> - )} - </> - ); -}; - -UptimeChart.defaultProps = { - title: undefined, -}; diff --git a/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx b/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx deleted file mode 100644 index 23114f9056..0000000000 --- a/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import { Button, IconButton, Stack, CircularProgress } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { useWalletContext } from '@/app/context/wallet' -import { WalletAddress, WalletBalance } from '@/app/components/Wallet' - -export const ConnectKeplrWallet = () => { - const { - connectWallet, - disconnectWallet, - isWalletConnected, - isWalletConnecting, - } = useWalletContext() - const isMobile = useIsMobile(1200) - - if (!connectWallet || !disconnectWallet) { - return null - } - - if (isWalletConnected) { - return ( - <Stack direction="row" spacing={1}> - <WalletBalance /> - <WalletAddress /> - <IconButton - size="small" - onClick={async () => { - await disconnectWallet() - }} - > - <CloseIcon fontSize="small" sx={{ color: 'white' }} /> - </IconButton> - </Stack> - ) - } - - return ( - <Button - variant="outlined" - onClick={() => connectWallet()} - disabled={isWalletConnecting} - endIcon={ - isWalletConnecting && <CircularProgress size={14} color="inherit" /> - } - > - Connect {isMobile ? '' : ' Wallet'} - </Button> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/WalletAddress.tsx b/explorer-nextjs/app/components/Wallet/WalletAddress.tsx deleted file mode 100644 index 3e251eb2bb..0000000000 --- a/explorer-nextjs/app/components/Wallet/WalletAddress.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { ElipsSVG } from '@/app/icons/ElipsSVG' -import { trimAddress } from '@/app/utils' -import { useWalletContext } from '@/app/context/wallet' - -export const WalletAddress = () => { - const { address } = useWalletContext() - - const displayAddress = trimAddress(address, 7) - - return ( - <Box display="flex" alignItems="center" gap={0.5}> - <ElipsSVG /> - <Typography variant="body1" fontWeight={600}> - {displayAddress} - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/WalletBalance.tsx b/explorer-nextjs/app/components/Wallet/WalletBalance.tsx deleted file mode 100644 index 7d1c5979bc..0000000000 --- a/explorer-nextjs/app/components/Wallet/WalletBalance.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { useWalletContext } from '@/app/context/wallet' -import { useIsMobile } from '@/app/hooks' -import { TokenSVG } from '@/app/icons/TokenSVG' - -export const WalletBalance = () => { - const { balance } = useWalletContext() - const isMobile = useIsMobile(1200) - - const showBalance = !isMobile && balance.status === 'success' - - if (!showBalance) { - return null - } - - return ( - <Box display="flex" alignItems="center" gap={1}> - <TokenSVG /> - <Typography variant="body1" fontWeight={600}> - {balance.data} NYM - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/index.ts b/explorer-nextjs/app/components/Wallet/index.ts deleted file mode 100644 index 645c8b4ffb..0000000000 --- a/explorer-nextjs/app/components/Wallet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './WalletBalance'; -export * from './WalletAddress'; diff --git a/explorer-nextjs/app/components/WorldMap.tsx b/explorer-nextjs/app/components/WorldMap.tsx deleted file mode 100644 index 9b5c329571..0000000000 --- a/explorer-nextjs/app/components/WorldMap.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client' - -import * as React from 'react' -import { scaleLinear } from 'd3-scale' -import { - ComposableMap, - Geographies, - Geography, - Marker, - ZoomableGroup, -} from 'react-simple-maps' -import ReactTooltip from 'react-tooltip' -import { CircularProgress } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { ApiState, CountryDataResponse } from '../typeDefs/explorer-api' -import MAP_TOPOJSON from '../assets/world-110m.json' - -type MapProps = { - userLocation?: [number, number] - countryData?: ApiState<CountryDataResponse> - loading: boolean -} - -export const WorldMap: FCWithChildren<MapProps> = ({ - countryData, - userLocation, - loading, -}) => { - const { palette } = useTheme() - - const colorScale = React.useMemo(() => { - if (countryData?.data) { - const heighestNumberOfNodes = Math.max( - ...Object.values(countryData.data).map((country) => country.nodes) - ) - return scaleLinear<string, string>() - .domain([ - 0, - 1, - heighestNumberOfNodes / 4, - heighestNumberOfNodes / 2, - heighestNumberOfNodes, - ]) - .range(palette.nym.networkExplorer.map.fills) - .unknown(palette.nym.networkExplorer.map.fills[0]) - } - return () => palette.nym.networkExplorer.map.fills[0] - }, [countryData, palette]) - - const [tooltipContent, setTooltipContent] = React.useState<string | null>( - null - ) - - if (loading) { - return <CircularProgress /> - } - - return ( - <> - <ComposableMap - data-tip="" - style={{ - backgroundColor: palette.nym.networkExplorer.background.tertiary, - width: '100%', - height: 'auto', - }} - viewBox="0, 50, 800, 350" - projection="geoMercator" - projectionConfig={{ - scale: userLocation ? 200 : 100, - center: userLocation, - }} - > - <ZoomableGroup> - <Geographies geography={MAP_TOPOJSON}> - {({ geographies }) => - geographies.map((geo) => { - const d = (countryData?.data || {})[geo.properties.ISO_A3] - return ( - <Geography - key={geo.rsmKey} - geography={geo} - fill={colorScale(d?.nodes || 0)} - stroke={palette.nym.networkExplorer.map.stroke} - strokeWidth={0.2} - onMouseEnter={() => { - const { NAME_LONG } = geo.properties - if (!userLocation) { - setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`) - } - }} - onMouseLeave={() => { - setTooltipContent('') - }} - style={{ - hover: - !userLocation && countryData - ? { - fill: palette.nym.highlight, - outline: 'white', - } - : undefined, - }} - /> - ) - }) - } - </Geographies> - - {userLocation && ( - <Marker coordinates={userLocation}> - <g - fill="grey" - stroke="#FF5533" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - transform="translate(-12, -10)" - > - <circle cx="12" cy="10" r="5" /> - </g> - </Marker> - )} - </ZoomableGroup> - </ComposableMap> - <ReactTooltip>{tooltipContent}</ReactTooltip> - </> - ) -} diff --git a/explorer-nextjs/app/components/index.ts b/explorer-nextjs/app/components/index.ts deleted file mode 100644 index 92eeebd010..0000000000 --- a/explorer-nextjs/app/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './CustomColumnHeading'; -export * from './Title'; -export * from './Universal-DataGrid'; -export * from './Tooltip'; -export { default as StyledLink } from './StyledLink'; -export * from './Delegations'; -export * from './MixNodes'; -export * from './TableToolbar'; -export * from './Icons'; diff --git a/explorer-nextjs/app/context/cosmos-kit.tsx b/explorer-nextjs/app/context/cosmos-kit.tsx deleted file mode 100644 index 987d30aa65..0000000000 --- a/explorer-nextjs/app/context/cosmos-kit.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client' - -import React from 'react' -import { ChainProvider } from '@cosmos-kit/react' -import { wallets as keplr } from '@cosmos-kit/keplr-extension' -import { assets, chains } from 'chain-registry' -import { Chain, AssetList } from '@chain-registry/types' -import { VALIDATOR_BASE_URL } from '@/app/api/constants' - -const nymSandbox: Chain = { - chain_name: 'sandbox', - chain_id: 'sandbox', - bech32_prefix: 'n', - network_type: 'devnet', - pretty_name: 'Nym Sandbox', - status: 'active', - slip44: 118, - apis: { - rpc: [ - { - address: 'https://rpc.sandbox.nymtech.net', - }, - ], - }, -} - -const nymSandboxAssets = { - chain_name: 'sandbox', - assets: [ - { - name: 'Nym', - base: 'unym', - symbol: 'NYM', - display: 'NYM', - denom_units: [], - }, - ], -} - -const CosmosKitProvider = ({ children }: { children: React.ReactNode }) => { - // Only use the nyx chains - const chainsFixedUp = React.useMemo(() => { - const nyx = chains.find((chain) => chain.chain_id === 'nyx') - - return nyx ? [nymSandbox, nyx] : [nymSandbox] - }, [chains]) - - // Only use the nyx assets - const assetsFixedUp = React.useMemo(() => { - const nyx = assets.find((asset) => asset.chain_name === 'nyx') - - return nyx ? [nymSandboxAssets, nyx] : [nymSandboxAssets] - }, [assets]) as AssetList[] - - return ( - <ChainProvider - chains={chainsFixedUp} - assetLists={assetsFixedUp} - wallets={[...keplr]} - endpointOptions={{ - endpoints: { - nyx: { - rpc: [VALIDATOR_BASE_URL], - }, - }, - }} - > - {children} - </ChainProvider> - ) -} - -export default CosmosKitProvider diff --git a/explorer-nextjs/app/context/delegations.tsx b/explorer-nextjs/app/context/delegations.tsx deleted file mode 100644 index 75a099970c..0000000000 --- a/explorer-nextjs/app/context/delegations.tsx +++ /dev/null @@ -1,252 +0,0 @@ -'use client' - -import React, { - createContext, - useCallback, - useContext, - useMemo, - useState, -} from 'react' -import { - Delegation, - PendingEpochEvent, - PendingEpochEventKind, -} from '@nymproject/contract-clients/Mixnet.types' -import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' -import { useWalletContext } from './wallet' -import { useMainContext } from './main' - -const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] } - -export type PendingEvent = ReturnType<typeof getEventsByAddress> - -export type DelegationWithRewards = Delegation & { - rewards: string - identityKey: string - pending: PendingEvent -} - -const getEventsByAddress = (kind: PendingEpochEventKind, address: String) => { - if ('delegate' in kind && kind.delegate.owner === address) { - return { - kind: 'delegate' as const, - mixId: kind.delegate.mix_id, - amount: kind.delegate.amount, - } - } - - if ('undelegate' in kind && kind.undelegate.owner === address) { - return { - kind: 'undelegate' as const, - mixId: kind.undelegate.mix_id, - } - } - - return undefined -} - -interface DelegationsState { - delegations?: DelegationWithRewards[] - handleGetDelegations: () => Promise<void> - handleDelegate: ( - mixId: number, - amount: string - ) => Promise<ExecuteResult | undefined> - handleUndelegate: (mixId: number) => Promise<ExecuteResult | undefined> -} - -export const DelegationsContext = createContext<DelegationsState>({ - delegations: undefined, - handleGetDelegations: async () => { - throw new Error('Please connect your wallet') - }, - handleDelegate: async () => { - throw new Error('Please connect your wallet') - }, - handleUndelegate: async () => { - throw new Error('Please connect your wallet') - }, -}) - -export const DelegationsProvider = ({ - children, -}: { - children: React.ReactNode -}) => { - const [delegations, setDelegations] = useState<DelegationWithRewards[]>() - const { address, nymQueryClient, nymClient } = useWalletContext() - const { fetchMixnodes } = useMainContext() - - const handleGetPendingEvents = async () => { - if (!nymQueryClient) { - return undefined - } - - if (!address) { - return undefined - } - - const response = await nymQueryClient.getPendingEpochEvents({}) - const pendingEvents: PendingEvent[] = [] - - response.events.forEach((e: PendingEpochEvent) => { - const event = getEventsByAddress(e.event.kind, address) - if (event) { - pendingEvents.push(event) - } - }) - - return pendingEvents - } - - const handleGetDelegationRewards = async (mixId: number) => { - if (!nymQueryClient) { - return undefined - } - - if (!address) { - return undefined - } - - const response = await nymQueryClient.getPendingDelegatorReward({ - address, - mixId, - }) - - return response - } - - const handleGetDelegations = useCallback(async () => { - if (!nymQueryClient) { - setDelegations(undefined) - return undefined - } - - if (!address) { - setDelegations(undefined) - return undefined - } - - // Get all mixnodes - Required to get the identity key for each delegation - const mixnodes = await fetchMixnodes() - - // Get delegations - const delegationsResponse = await nymQueryClient.getDelegatorDelegations({ - delegator: address, - }) - - // Get rewards for each delegation - const rewardsResponse = await Promise.all( - delegationsResponse.delegations.map((d: Delegation) => - handleGetDelegationRewards(d.mix_id) - ) - ) - - // Get all pending events - const pendingEvents = await handleGetPendingEvents() - - const delegationsWithRewards: DelegationWithRewards[] = [] - - // Merge delegations with rewards and pending events - delegationsResponse.delegations.forEach((d: Delegation, index: number) => { - delegationsWithRewards.push({ - ...d, - pending: pendingEvents?.find((e: PendingEvent) => - e?.mixId === d.mix_id ? e.kind : undefined - ), - identityKey: - mixnodes?.find((m) => m.mix_id === d.mix_id)?.mix_node.identity_key || - '', - rewards: rewardsResponse[index]?.amount_earned_detailed || '0', - }) - }) - - // Add pending events that are not in the delegations list - pendingEvents?.forEach((e) => { - if ( - e && - !delegationsWithRewards.find( - (d: DelegationWithRewards) => d.mix_id === e.mixId - ) - ) { - delegationsWithRewards.push({ - mix_id: e.mixId, - height: 0, - cumulative_reward_ratio: '0', - owner: address, - amount: { - amount: '0', - denom: 'unym', - }, - rewards: '0', - identityKey: - mixnodes?.find((m) => m.mix_id === e.mixId)?.mix_node - .identity_key || '', - pending: e, - }) - } - }) - - setDelegations(delegationsWithRewards) - - return undefined - }, [address, nymQueryClient]) - - const handleDelegate = async (mixId: number, amount: string) => { - if (!address) { - throw new Error('Please connect your wallet') - } - - const amountToDelegate = (Number(amount) * 1000000).toString() - const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }] - try { - const tx = await nymClient?.delegateToMixnode( - { mixId }, - fee, - 'Delegation from Nym Explorer', - uNymFunds - ) - - return tx as unknown as ExecuteResult - } catch (e) { - console.error('Failed to delegate to mixnode', e) - throw e - } - } - - const handleUndelegate = async (mixId: number) => { - const tx = await nymClient?.undelegateFromMixnode( - { mixId }, - fee, - 'Undelegation from Nym Explorer' - ) - - return tx as unknown as ExecuteResult - } - - const contextValue: DelegationsState = useMemo( - () => ({ - delegations, - handleGetDelegations, - handleDelegate, - handleUndelegate, - }), - [delegations, handleGetDelegations] - ) - - return ( - <DelegationsContext.Provider value={contextValue}> - {children} - </DelegationsContext.Provider> - ) -} - -export const useDelegationsContext = () => { - const context = useContext(DelegationsContext) - if (!context) { - throw new Error( - 'useDelegationsContext must be used within a DelegationsProvider' - ) - } - return context -} diff --git a/explorer-nextjs/app/context/gateway.tsx b/explorer-nextjs/app/context/gateway.tsx deleted file mode 100644 index 458a9da907..0000000000 --- a/explorer-nextjs/app/context/gateway.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ApiState, - GatewayReportResponse, - UptimeStoryResponse, -} from '@/app/typeDefs/explorer-api' -import { Api } from '@/app/api' -import { useApiState } from './hooks' - -/** - * This context provides the state for a single gateway by identity key. - */ - -interface GatewayState { - uptimeReport?: ApiState<GatewayReportResponse> - uptimeStory?: ApiState<UptimeStoryResponse> -} - -export const GatewayContext = React.createContext<GatewayState>({}) - -export const useGatewayContext = (): React.ContextType<typeof GatewayContext> => - React.useContext<GatewayState>(GatewayContext) - -/** - * Provides a state context for a gateway by identity - * @param gatewayIdentityKey The identity key of the gateway - */ -export const GatewayContextProvider = ({ - gatewayIdentityKey, - children, -}: { - gatewayIdentityKey: string - children: JSX.Element -}) => { - const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = - useApiState<GatewayReportResponse>( - gatewayIdentityKey, - Api.fetchGatewayReportById, - 'Failed to fetch gateway uptime report by id' - ) - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = - useApiState<UptimeStoryResponse>( - gatewayIdentityKey, - Api.fetchGatewayUptimeStoryById, - 'Failed to fetch gateway uptime history' - ) - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearUptimeReportById() - clearUptimeHistory() - Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]) - }, [gatewayIdentityKey]) - - const state = React.useMemo<GatewayState>( - () => ({ - uptimeReport, - uptimeStory, - }), - [uptimeReport, uptimeStory] - ) - - return ( - <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider> - ) -} diff --git a/explorer-nextjs/app/context/hooks.ts b/explorer-nextjs/app/context/hooks.ts deleted file mode 100644 index 55004f89ee..0000000000 --- a/explorer-nextjs/app/context/hooks.ts +++ /dev/null @@ -1,49 +0,0 @@ -'use client' - -import * as React from 'react'; -import { ApiState } from '@/app/typeDefs/explorer-api'; - -/** - * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously - * @param id The id to fetch - * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) - * @param errorMessage A static error message, to use when no dynamic error message is returned - */ -export const useApiState = <T>( - id: string, - fn: (argId: string) => Promise<T>, - errorMessage: string, -): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => { - // stores the state - const [value, setValue] = React.useState<ApiState<T>>(); - - // clear the value - const clearValueFn = () => setValue(undefined); - - // this provides a method to trigger the delegate to fetch data - const wrappedFetchFn = React.useCallback(async () => { - setValue({ isLoading: true }); - try { - // keep previous state and set to loading - setValue((prevState) => ({ ...prevState, isLoading: true })); - - // delegate to user function to get data and set if successful - const data = await fn(id); - const newValue: ApiState<T> = { - isLoading: false, - data, - }; - setValue(newValue); - return newValue; - } catch (error) { - // return the caught error or create a new error with the static error message - const newValue: ApiState<T> = { - error: error instanceof Error ? error : new Error(errorMessage), - isLoading: false, - }; - setValue(newValue); - return newValue; - } - }, [setValue, fn, id, errorMessage]); - return [value, wrappedFetchFn, clearValueFn]; -}; diff --git a/explorer-nextjs/app/context/main.tsx b/explorer-nextjs/app/context/main.tsx deleted file mode 100644 index 0494b87749..0000000000 --- a/explorer-nextjs/app/context/main.tsx +++ /dev/null @@ -1,297 +0,0 @@ -'use client' - -import * as React from 'react' -import { PaletteMode } from '@mui/material' -import { - ApiState, - BlockResponse, - CountryDataResponse, - GatewayResponse, - MixNodeResponse, - MixnodeStatus, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - DirectoryServiceProvider, -} from '@/app/typeDefs/explorer-api' -import { EnumFilterKey } from '@/app/typeDefs/filters' -import { Api, getEnvironment } from '@/app/api' -import { toPercentIntegerString } from '@/app/utils' -import { NavOptionType, originalNavOptions } from './nav' - -interface StateData { - summaryOverview?: ApiState<SummaryOverviewResponse> - block?: ApiState<BlockResponse> - countryData?: ApiState<CountryDataResponse> - gateways?: ApiState<GatewayResponse> - globalError?: string | undefined - mixnodes?: ApiState<MixNodeResponse> - nodes?: ApiState<any> - mode: PaletteMode - validators?: ApiState<ValidatorsResponse> - environment?: Environment - serviceProviders?: ApiState<DirectoryServiceProvider[]> -} - -interface StateApi { - fetchMixnodes: ( - status?: MixnodeStatus - ) => Promise<MixNodeResponse | undefined> - filterMixnodes: (filters: any, status: any) => void - fetchNodes: () => Promise<any> - fetchNodeById: (id: number) => Promise<any> - fetchAccountById: (accountAddr: string) => Promise<any> - toggleMode: () => void -} - -type State = StateData & StateApi - -export const MainContext = React.createContext<State>({ - mode: 'dark', - toggleMode: () => undefined, - filterMixnodes: () => null, - fetchMixnodes: () => Promise.resolve(undefined), - fetchNodes: async () => undefined, - fetchNodeById: async () => undefined, - fetchAccountById: async () => undefined, -}) - -export const useMainContext = (): React.ContextType<typeof MainContext> => - React.useContext<State>(MainContext) - -export const MainContextProvider: FCWithChildren = ({ children }) => { - // network explorer environment - const [environment, setEnvironment] = React.useState<Environment>() - - // light/dark mode - const [mode, setMode] = React.useState<PaletteMode>('dark') - - // global / banner error messaging - const [globalError] = React.useState<string>() - - // various APIs for Overview page - const [summaryOverview, setSummaryOverview] = - React.useState<ApiState<SummaryOverviewResponse>>() - const [nodes, setNodes] = React.useState<ApiState<any>>() - const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>() - const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>() - 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<DirectoryServiceProvider[]>>() - - const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark')) - - const fetchOverviewSummary = async () => { - try { - const data = await Api.fetchOverviewSummary() - setSummaryOverview({ data, isLoading: false }) - } catch (error) { - setSummaryOverview({ - error: - error instanceof Error - ? error - : new Error('Overview summary api fail'), - isLoading: false, - }) - } - } - - const fetchMixnodes = async () => { - let data - setMixnodes((d) => ({ ...d, isLoading: true })) - try { - data = await Api.fetchMixnodes() - setMixnodes({ data, isLoading: false }) - } catch (error) { - setMixnodes({ - error: error instanceof Error ? error : new Error('Mixnode api fail'), - isLoading: false, - }) - } - return data - } - - const filterMixnodes = async ( - filters: { [key in EnumFilterKey]: number[] }, - status?: MixnodeStatus - ) => { - setMixnodes((d) => ({ ...d, isLoading: true })) - const mxns = await Api.fetchMixnodes() - - const filtered = mxns?.filter( - (m) => - +m.profit_margin_percent >= filters.profitMargin[0] / 100 && - +m.profit_margin_percent <= filters.profitMargin[1] / 100 && - m.stake_saturation >= filters.stakeSaturation[0] && - m.stake_saturation <= filters.stakeSaturation[1] && - m.avg_uptime >= filters.routingScore[0] && - m.avg_uptime <= filters.routingScore[1] - ) - - setMixnodes({ data: filtered, isLoading: false }) - } - - const fetchGateways = async () => { - setGateways((d) => ({ ...d, isLoading: true })) - try { - const data = await Api.fetchGateways() - setGateways({ data, isLoading: false }) - } catch (error) { - setGateways({ - error: error instanceof Error ? error : new Error('Gateways api fail'), - isLoading: false, - }) - } - } - const fetchValidators = async () => { - try { - const data = await Api.fetchValidators() - setValidators({ data, isLoading: false }) - } catch (error) { - setValidators({ - error: - error instanceof Error ? error : new Error('Validators api fail'), - isLoading: false, - }) - } - } - const fetchBlock = async () => { - try { - const data = await Api.fetchBlock() - setBlock({ data, isLoading: false }) - } catch (error) { - setBlock({ - error: error instanceof Error ? error : new Error('Block api fail'), - isLoading: false, - }) - } - } - const fetchCountryData = async () => { - setCountryData({ data: undefined, isLoading: true }) - try { - const res = await Api.fetchCountryData() - setCountryData({ data: res, isLoading: false }) - } catch (error) { - setCountryData({ - error: - error instanceof Error ? error : new Error('Country Data api fail'), - isLoading: false, - }) - } - } - - const fetchServiceProviders = async () => { - setServiceProviders({ data: undefined, isLoading: true }) - try { - const res = await Api.fetchServiceProviders() - const resWithRoutingScorePercentage = res.map((item) => ({ - ...item, - routing_score: item.routing_score - ? `${toPercentIntegerString(item.routing_score.toString())}%` - : item.routing_score, - })) - setServiceProviders({ - data: resWithRoutingScorePercentage, - isLoading: false, - }) - } catch (error) { - setServiceProviders({ - error: - error instanceof Error - ? error - : new Error('Service provider api fail'), - isLoading: false, - }) - } - } - - const fetchNodes = async () => { - setNodes({ data: undefined, isLoading: true }) - try { - const res = await Api.fetchNodes(); - res.forEach((node: any) => node.total_stake = - Math.round(Number.parseFloat(node.rewarding_details?.operator || "0") - + Number.parseFloat(node.rewarding_details?.delegates || "0")) - ); - setNodes({ - data: res.sort((a: any, b: any) => b.total_stake - a.total_stake), - isLoading: false, - }) - } catch (error) { - setNodes({ - error: - error instanceof Error - ? error - : new Error('Service provider api fail'), - isLoading: false, - }) - } }; - - const fetchNodeById = async (id: number) => { - const res = await Api.fetchNodeById(id); - return res; - }; - - const fetchAccountById = async (id: string) => { - const res = await Api.fetchAccountById(id); - return res; - }; - - React.useEffect(() => { - if (environment === 'mainnet') { - fetchServiceProviders() - } - }, [environment]) - - React.useEffect(() => { - setEnvironment(getEnvironment()) - Promise.all([ - fetchOverviewSummary(), - fetchGateways(), - fetchValidators(), - fetchBlock(), - fetchCountryData(), - ]) - }, []) - - const state = React.useMemo<State>( - () => ({ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - nodes, - summaryOverview, - validators, - serviceProviders, - toggleMode, - fetchMixnodes, - filterMixnodes, - fetchNodes, - fetchNodeById, - fetchAccountById, - }), - [ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - nodes, - summaryOverview, - validators, - serviceProviders, - ] - ) - - return <MainContext.Provider value={state}>{children}</MainContext.Provider> -} diff --git a/explorer-nextjs/app/context/mixnode.tsx b/explorer-nextjs/app/context/mixnode.tsx deleted file mode 100644 index 71b48fd045..0000000000 --- a/explorer-nextjs/app/context/mixnode.tsx +++ /dev/null @@ -1,173 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ApiState, - DelegationsResponse, - UniqDelegationsResponse, - MixNodeDescriptionResponse, - MixNodeEconomicDynamicsStatsResponse, - MixNodeResponseItem, - StatsResponse, - StatusResponse, - UptimeStoryResponse, -} from '../typeDefs/explorer-api' -import { Api } from '../api' -import { useApiState } from './hooks' -import { - mixNodeResponseItemToMixnodeRowType, - MixnodeRowType, -} from '../components/MixNodes' - -/** - * This context provides the state for a single mixnode by identity key. - */ - -interface MixnodeState { - delegations?: ApiState<DelegationsResponse> - uniqDelegations?: ApiState<UniqDelegationsResponse> - description?: ApiState<MixNodeDescriptionResponse> - economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse> - mixNode?: ApiState<MixNodeResponseItem | undefined> - mixNodeRow?: MixnodeRowType - stats?: ApiState<StatsResponse> - status?: ApiState<StatusResponse> - uptimeStory?: ApiState<UptimeStoryResponse> -} - -export const MixnodeContext = React.createContext<MixnodeState>({}) - -export const useMixnodeContext = (): React.ContextType<typeof MixnodeContext> => - React.useContext<MixnodeState>(MixnodeContext) - -interface MixnodeContextProviderProps { - mixId: string - children: React.ReactNode -} - -/** - * Provides a state context for a mixnode by identity - * @param mixId The mixID of the mixnode - */ -export const MixnodeContextProvider: FCWithChildren< - MixnodeContextProviderProps -> = ({ mixId, children }) => { - const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState< - MixNodeResponseItem | undefined - >(mixId, Api.fetchMixnodeByID, 'Failed to fetch mixnode by id') - - const [mixNodeRow, setMixnodeRow] = React.useState< - MixnodeRowType | undefined - >() - - const [delegations, fetchDelegations, clearDelegations] = - useApiState<DelegationsResponse>( - mixId, - Api.fetchDelegationsById, - 'Failed to fetch delegations for mixnode' - ) - - const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] = - useApiState<UniqDelegationsResponse>( - mixId, - Api.fetchUniqDelegationsById, - 'Failed to fetch delegations for mixnode' - ) - - const [status, fetchStatus, clearStatus] = useApiState<StatusResponse>( - mixId, - Api.fetchStatusById, - 'Failed to fetch mixnode status' - ) - - const [stats, fetchStats, clearStats] = useApiState<StatsResponse>( - mixId, - Api.fetchStatsById, - 'Failed to fetch mixnode stats' - ) - - const [description, fetchDescription, clearDescription] = - useApiState<MixNodeDescriptionResponse>( - mixId, - Api.fetchMixnodeDescriptionById, - 'Failed to fetch mixnode description' - ) - - const [ - economicDynamicsStats, - fetchEconomicDynamicsStats, - clearEconomicDynamicsStats, - ] = useApiState<MixNodeEconomicDynamicsStatsResponse>( - mixId, - Api.fetchMixnodeEconomicDynamicsStatsById, - 'Failed to fetch mixnode dynamics stats by id' - ) - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = - useApiState<UptimeStoryResponse>( - mixId, - Api.fetchUptimeStoryById, - 'Failed to fetch mixnode uptime history' - ) - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearMixnodeById() - clearDelegations() - clearUniqDelegations() - clearStatus() - clearStats() - clearDescription() - clearEconomicDynamicsStats() - clearUptimeHistory() - - // fetch the mixnode, then get all the other stuff - fetchMixnodeById().then((value) => { - if (!value.data || value.error) { - setMixnodeRow(undefined) - return - } - setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data)) - Promise.all([ - fetchDelegations(), - fetchUniqDelegations(), - fetchStatus(), - fetchStats(), - fetchDescription(), - fetchEconomicDynamicsStats(), - fetchUptimeHistory(), - ]) - }) - }, [mixId]) - - const state = React.useMemo<MixnodeState>( - () => ({ - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }), - [ - { - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }, - ] - ) - - return ( - <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider> - ) -} diff --git a/explorer-nextjs/app/context/nav.tsx b/explorer-nextjs/app/context/nav.tsx deleted file mode 100644 index e41882edbb..0000000000 --- a/explorer-nextjs/app/context/nav.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client' - -import * as React from 'react' -import { DelegateIcon } from '@/app/icons/DelevateSVG' -import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' -import { OverviewSVG } from '@/app/icons/OverviewSVG' -import { NodemapSVG } from '@/app/icons/NodemapSVG' -import { NetworkComponentsSVG } from '@/app/icons/NetworksSVG' - -export type NavOptionType = { - url: string - title: string - Icon?: React.ReactNode - nested?: NavOptionType[] - isExpandedChild?: boolean - isExternal?: boolean -} - -export const originalNavOptions: NavOptionType[] = [ - { - url: '/', - title: 'Overview', - Icon: <OverviewSVG />, - }, - { - url: '/network-components', - title: 'Network Components', - Icon: <NetworkComponentsSVG />, - nested: [ - { - url: '/network-components/nodes', - title: 'Nodes', - }, - { - url: '/network-components/mixnodes', - title: 'Mixnodes (legacy)', - }, - { - url: '/network-components/gateways', - title: 'Gateways (legacy)', - }, - { - url: `${BLOCK_EXPLORER_BASE_URL}/validators`, - title: 'Validators', - isExternal: true, - }, - ], - }, - { - url: '/nodemap', - title: 'Nodemap', - Icon: <NodemapSVG />, - }, - { - url: '/delegations', - title: 'Delegations', - Icon: <DelegateIcon sx={{ color: 'white' }} />, - }, -] diff --git a/explorer-nextjs/app/context/node.tsx b/explorer-nextjs/app/context/node.tsx deleted file mode 100644 index 2aa885439c..0000000000 --- a/explorer-nextjs/app/context/node.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ApiState, - NymNodeReportResponse, - UptimeStoryResponse, -} from '@/app/typeDefs/explorer-api' -import { Api } from '@/app/api' -import { useApiState } from './hooks' - -/** - * This context provides the state for a single gateway by identity key. - */ - -interface NymNodeState { - uptimeReport?: ApiState<NymNodeReportResponse> - uptimeHistory?: ApiState<UptimeStoryResponse> -} - -export const NymNodeContext = React.createContext<NymNodeState>({}) - -export const useNymNodeContext = (): React.ContextType<typeof NymNodeContext> => - React.useContext<NymNodeState>(NymNodeContext) - -/** - * Provides a state context for a gateway by identity - * @param gatewayIdentityKey The identity key of the gateway - */ -export const NymNodeContextProvider = ({ - nymNodeId, - children, -}: { - nymNodeId: string - children: JSX.Element -}) => { - const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = - useApiState<any>( - nymNodeId, - Api.fetchNymNodePerformanceById, - 'Failed to fetch gateway uptime report by id' - ) - - const [uptimeHistory, fetchUptimeHistory, clearUptimeHistory] = - useApiState<UptimeStoryResponse>( - nymNodeId, - async (arg) => { - const res = await Api.fetchNymNodeUptimeHistoryById(arg); - const uptimeHistory: UptimeStoryResponse = { - history: res.history.data, - identity: '', - owner: '', - } - return uptimeHistory; - }, - 'Failed to fetch gateway uptime history' - ) - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearUptimeReportById() - clearUptimeHistory() - Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]) - }, [nymNodeId]) - - const state = React.useMemo<NymNodeState>( - () => ({ - uptimeReport, - uptimeHistory, - }), - [uptimeReport, uptimeHistory] - ) - - return ( - <NymNodeContext.Provider value={state}>{children}</NymNodeContext.Provider> - ) -} diff --git a/explorer-nextjs/app/context/wallet.tsx b/explorer-nextjs/app/context/wallet.tsx deleted file mode 100644 index c9acb6b850..0000000000 --- a/explorer-nextjs/app/context/wallet.tsx +++ /dev/null @@ -1,123 +0,0 @@ -'use client' - -import React, { - createContext, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import { useChain } from '@cosmos-kit/react' -import { Wallet } from '@cosmos-kit/core' -import { unymToNym } from '@/app/utils/currency' -import { useNymClient } from '@/app/hooks' -import { - MixnetClient, - MixnetQueryClient, -} from '@nymproject/contract-clients/Mixnet.client' -import { COSMOS_KIT_USE_CHAIN } from '@/app/api/constants' - -interface WalletState { - balance: { status: 'loading' | 'success'; data?: string } - address?: string - isWalletConnected: boolean - isWalletConnecting: boolean - wallet?: Wallet - nymClient?: MixnetClient - nymQueryClient?: MixnetQueryClient - connectWallet: () => Promise<void> - disconnectWallet: () => Promise<void> -} - -export const WalletContext = createContext<WalletState>({ - address: undefined, - balance: { status: 'loading', data: undefined }, - isWalletConnected: false, - isWalletConnecting: false, - nymClient: undefined, - nymQueryClient: undefined, - connectWallet: async () => { - throw new Error('Please connect your wallet') - }, - disconnectWallet: async () => { - throw new Error('Please connect your wallet') - }, -}) - -export const WalletProvider = ({ children }: { children: React.ReactNode }) => { - const [balance, setBalance] = useState<WalletState['balance']>({ - status: 'loading', - data: undefined, - }) - - const { - connect, - disconnect, - wallet, - address, - isWalletConnected, - isWalletConnecting, - getCosmWasmClient, - } = useChain(COSMOS_KIT_USE_CHAIN) - - const { nymClient, nymQueryClient } = useNymClient(address) - - const getBalance = async (walletAddress: string) => { - const account = await getCosmWasmClient() - const uNYMBalance = await account.getBalance(walletAddress, 'unym') - const NYMBalance = unymToNym(uNYMBalance.amount) - - return NYMBalance - } - - const init = async (walletAddress: string) => { - const walletBalance = await getBalance(walletAddress) - setBalance({ status: 'success', data: walletBalance }) - } - - useEffect(() => { - if (isWalletConnected && address) { - init(address) - } - }, [address, isWalletConnected]) - - const handleConnectWallet = async () => { - await connect() - } - - const handleDisconnectWallet = async () => { - await disconnect() - setBalance({ status: 'loading', data: undefined }) - } - - const contextValue: WalletState = useMemo( - () => ({ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - connectWallet: handleConnectWallet, - disconnectWallet: handleDisconnectWallet, - }), - [ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - ] - ) - - return ( - <WalletContext.Provider value={contextValue}> - {children} - </WalletContext.Provider> - ) -} - -export const useWalletContext = () => useContext(WalletContext) diff --git a/explorer-nextjs/app/delegations/page.tsx b/explorer-nextjs/app/delegations/page.tsx deleted file mode 100644 index 60b27b3c91..0000000000 --- a/explorer-nextjs/app/delegations/page.tsx +++ /dev/null @@ -1,311 +0,0 @@ -'use client' - -import React, { useCallback, useEffect, useMemo } from 'react' -import { - Alert, - AlertTitle, - Box, - Button, - Card, - Chip, - IconButton, - Tooltip, - Typography, -} from '@mui/material' -import { DelegationModal, DelegationModalProps, Title } from '@/app/components' -import { useWalletContext } from '@/app/context/wallet' -import { unymToNym } from '@/app/utils/currency' -import { - DelegationWithRewards, - DelegationsProvider, - PendingEvent, - useDelegationsContext, -} from '@/app/context/delegations' -import { urls } from '@/app/utils' -import { useClipboard } from 'use-clipboard-copy' -import { Close } from '@mui/icons-material' -import { useRouter } from 'next/navigation' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' - -const mapToDelegationsRow = ( - delegation: DelegationWithRewards, - index: number -) => ({ - identity: delegation.identityKey, - mix_id: delegation.mix_id, - amount: `${unymToNym(delegation.amount.amount)} NYM`, - rewards: `${unymToNym(delegation.rewards)} NYM`, - id: index, - pending: delegation.pending, -}) - -const Banner = ({ onClose }: { onClose: () => void }) => { - const { copy } = useClipboard() - - return ( - <Alert - severity="info" - sx={{ mb: 3, fontSize: 'medium', width: '100%' }} - action={ - <IconButton size="small" onClick={onClose}> - <Close fontSize="small" /> - </IconButton> - } - > - <AlertTitle> Mobile Delegations Beta</AlertTitle> - <Box> - <Typography> - This is a beta release for mobile delegations If you have any feedback - or feature suggestions contact us at support@nymte.ch - <Button - size="small" - onClick={() => copy('support@nymte.ch')} - sx={{ display: 'inline-block' }} - > - Copy - </Button> - </Typography> - </Box> - </Alert> - ) -} - -const DelegationsPage = () => { - const [confirmationModalProps, setConfirmationModalProps] = React.useState< - DelegationModalProps | undefined - >() - const [isLoading, setIsLoading] = React.useState(false) - const [showBanner, setShowBanner] = React.useState(true) - - const { isWalletConnected } = useWalletContext() - const { handleGetDelegations, handleUndelegate, delegations } = - useDelegationsContext() - - const router = useRouter() - - useEffect(() => { - let timeoutId: NodeJS.Timeout - - const fetchDelegations = async () => { - setIsLoading(true) - try { - await handleGetDelegations() - } catch (error) { - setConfirmationModalProps({ - status: 'error', - message: "Couldn't fetch delegations. Please try again later.", - }) - } finally { - setIsLoading(false) - - timeoutId = setTimeout(() => { - fetchDelegations() - }, 60_000) - } - } - - fetchDelegations() - - return () => { - clearTimeout(timeoutId) - } - }, [handleGetDelegations]) - - const getTooltipTitle = (pending: PendingEvent) => { - if (pending?.kind === 'undelegate') { - return 'You have an undelegation pending' - } - - if (pending?.kind === 'delegate') { - return `You have a delegation pending worth ${unymToNym( - pending.amount.amount - )} NYM` - } - - return undefined - } - - const onUndelegate = useCallback( - async (mixId: number) => { - setConfirmationModalProps({ status: 'loading' }) - - try { - const tx = await handleUndelegate(mixId) - - if (tx) { - setConfirmationModalProps({ - status: 'success', - message: 'Undelegation can take up to one hour to process', - transactions: [ - { - url: `${urls('MAINNET').blockExplorer}/transaction/${ - tx.transactionHash - }`, - hash: tx.transactionHash, - }, - ], - }) - } - } catch (error) { - if (error instanceof Error) { - setConfirmationModalProps({ status: 'error', message: error.message }) - } - } - }, - [handleUndelegate] - ) - - const columns = useMemo< - MRT_ColumnDef<ReturnType<typeof mapToDelegationsRow>>[] - >(() => { - return [ - { - id: 'delegations-data', - header: 'Delegations Data', - columns: [ - { - id: 'identity', - accessorKey: 'identity', - header: 'Identity Key', - width: 400, - }, - { - id: 'mix_id', - accessorKey: 'mix_id', - header: 'Mix ID', - size: 150, - }, - { - id: 'amount', - accessorKey: 'amount', - header: 'Amount', - width: 150, - }, - { - id: 'rewards', - accessorKey: 'rewards', - header: 'Rewards', - width: 150, - enableColumnFilters: false, - }, - { - id: 'undelegate', - accessorKey: 'undelegate', - header: '', - enableSorting: false, - enableColumnActions: false, - Filter: () => null, - Cell: ({ row }) => { - return ( - <Box - sx={{ width: '100%', display: 'flex', justifyContent: 'end' }} - > - {row.original.pending ? ( - <Tooltip - placement="left" - title={getTooltipTitle(row.original.pending)} - onClick={(e) => e.stopPropagation()} - PopperProps={{}} - > - <Chip size="small" label="Pending events" /> - </Tooltip> - ) : ( - <Button - size="small" - variant="outlined" - onClick={(e) => { - e.stopPropagation() - onUndelegate(row.original.mix_id) - }} - > - Undelegate - </Button> - )} - </Box> - ) - }, - }, - ], - }, - ] - }, [onUndelegate]) - - const data = useMemo(() => { - return (delegations || []).map(mapToDelegationsRow) - }, [delegations]) - - const table = useMaterialReactTable({ - columns, - data, - enableFullScreenToggle: false, - state: { - isLoading, - }, - initialState: { - columnPinning: { right: ['undelegate'] }, - }, - }) - - return ( - <Box> - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - if (confirmationModalProps.status === 'success') { - await handleGetDelegations() - } - setConfirmationModalProps(undefined) - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - {showBanner && <Banner onClose={() => setShowBanner(false)} />} - <Box display="flex" justifyContent="space-between" alignItems="center"> - <Title text="Your Delegations" /> - <Button - variant="contained" - color="primary" - onClick={() => router.push('/network-components/mixnodes')} - > - Delegate - </Button> - </Box> - {!isWalletConnected ? ( - <Box> - <Typography mb={2} variant="h6"> - Connect your wallet to view your delegations. - </Typography> - </Box> - ) : null} - - <Card - sx={{ - mt: 2, - padding: 2, - height: '100%', - }} - > - <MaterialReactTable table={table} /> - </Card> - </Box> - ) -} - -const Delegations = () => ( - <DelegationsProvider> - <DelegationsPage /> - </DelegationsProvider> -) - -export default Delegations diff --git a/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx b/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx deleted file mode 100644 index 4dbf9eb16e..0000000000 --- a/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react' -import { FallbackProps } from 'react-error-boundary' -import { Alert, AlertTitle, Container } from '@mui/material' -import { NymThemeProvider } from '@nymproject/mui-theme' -import { NymLogo } from '@nymproject/react/logo/NymLogo' - -export const ErrorBoundaryContent: FCWithChildren<FallbackProps> = ({ - error, -}) => ( - <NymThemeProvider mode="dark"> - <Container sx={{ py: 4 }}> - <NymLogo height="75px" width="75px" /> - <h1>Oh no! Sorry, something went wrong</h1> - <Alert severity="error" data-testid="error-message"> - <AlertTitle>{error.name}</AlertTitle> - {error.message} - </Alert> - {process.env.NODE_ENV === 'development' && ( - <Alert severity="info" sx={{ mt: 2 }} data-testid="stack-trace"> - <AlertTitle>Stack trace</AlertTitle> - {error.stack} - </Alert> - )} - </Container> - </NymThemeProvider> -) diff --git a/explorer-nextjs/app/hooks/index.ts b/explorer-nextjs/app/hooks/index.ts deleted file mode 100644 index ba04855f77..0000000000 --- a/explorer-nextjs/app/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './useIsMobile'; -export * from './useIsMounted'; -export * from './useGetMixnodeStatusColor'; -export * from './useNymClient'; diff --git a/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts b/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts deleted file mode 100644 index 3e1762209f..0000000000 --- a/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts +++ /dev/null @@ -1,19 +0,0 @@ -'use client' - -import { useTheme } from '@mui/material'; -import { MixnodeStatus } from '@/app/typeDefs/explorer-api'; - -export const useGetMixNodeStatusColor = (status: MixnodeStatus) => { - const theme = useTheme(); - - switch (status) { - case MixnodeStatus.active: - return theme.palette.nym.networkExplorer.mixnodes.status.active; - - case MixnodeStatus.standby: - return theme.palette.nym.networkExplorer.mixnodes.status.standby; - - default: - return theme.palette.nym.networkExplorer.mixnodes.status.inactive; - } -}; diff --git a/explorer-nextjs/app/hooks/useIsMobile.ts b/explorer-nextjs/app/hooks/useIsMobile.ts deleted file mode 100644 index 0948d36cbe..0000000000 --- a/explorer-nextjs/app/hooks/useIsMobile.ts +++ /dev/null @@ -1,11 +0,0 @@ -'use client' - -import { Breakpoint, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; - -export const useIsMobile = (queryInput: number | Breakpoint = 'md') => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(queryInput)); - - return isMobile; -}; diff --git a/explorer-nextjs/app/hooks/useIsMounted.ts b/explorer-nextjs/app/hooks/useIsMounted.ts deleted file mode 100644 index 5a78ae4fec..0000000000 --- a/explorer-nextjs/app/hooks/useIsMounted.ts +++ /dev/null @@ -1,16 +0,0 @@ -'use client' - -import { useRef, useEffect, useCallback } from 'react'; - -export function useIsMounted(): () => boolean { - const ref = useRef(false); - - useEffect(() => { - ref.current = true; - return () => { - ref.current = false; - }; - }, []); - - return useCallback(() => ref.current, [ref]); -} diff --git a/explorer-nextjs/app/hooks/useNymClient.tsx b/explorer-nextjs/app/hooks/useNymClient.tsx deleted file mode 100644 index 2fa400b130..0000000000 --- a/explorer-nextjs/app/hooks/useNymClient.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { useChain } from '@cosmos-kit/react' -import { contracts } from '@nymproject/contract-clients' -import { - MixnetClient, - MixnetQueryClient, -} from '@nymproject/contract-clients/Mixnet.client' -import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from '@/app/api/constants' - -export const useNymClient = (address?: string) => { - const [nymClient, setNymClient] = useState<MixnetClient>() - const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>() - - const { getCosmWasmClient, getSigningCosmWasmClient } = - useChain(COSMOS_KIT_USE_CHAIN) - - useEffect(() => { - if (address) { - const init = async () => { - const cosmWasmSigningClient = await getSigningCosmWasmClient() - const cosmWasmClient = await getCosmWasmClient() - - const client = new contracts.Mixnet.MixnetClient( - cosmWasmSigningClient as any, - address, - NYM_MIXNET_CONTRACT - ) - const queryClient = new contracts.Mixnet.MixnetQueryClient( - cosmWasmClient as any, - NYM_MIXNET_CONTRACT - ) - - setNymClient(client) - setNymQueryClient(queryClient) - } - - init() - } - }, [address, getCosmWasmClient, getSigningCosmWasmClient]) - - return { nymClient, nymQueryClient } -} diff --git a/explorer-nextjs/app/icons/DelevateSVG.tsx b/explorer-nextjs/app/icons/DelevateSVG.tsx deleted file mode 100644 index 56180d1b8a..0000000000 --- a/explorer-nextjs/app/icons/DelevateSVG.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { SvgIcon, SvgIconProps } from '@mui/material'; - -export const DelegateIcon = (props: SvgIconProps) => ( - <SvgIcon {...props}> - <path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" /> - <path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" /> - <rect x="18" y="12" width="2" height="3" /> - <rect x="18" y="17" width="2" height="3" /> - <rect x="4" y="17" width="2" height="3" /> - </SvgIcon> -); diff --git a/explorer-nextjs/app/icons/ElipsSVG.tsx b/explorer-nextjs/app/icons/ElipsSVG.tsx deleted file mode 100644 index 4a430d6cb6..0000000000 --- a/explorer-nextjs/app/icons/ElipsSVG.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -export const ElipsSVG: FCWithChildren = () => ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" /> - <defs> - <radialGradient - id="paint0_angular_2549_7570" - cx="0" - cy="0" - r="1" - gradientUnits="userSpaceOnUse" - gradientTransform="translate(12 12.5) rotate(90) scale(12)" - > - <stop stopColor="#22D27E" /> - <stop offset="1" stopColor="#9002FF" /> - </radialGradient> - </defs> - </svg> -); diff --git a/explorer-nextjs/app/icons/GatewaysSVG.tsx b/explorer-nextjs/app/icons/GatewaysSVG.tsx deleted file mode 100644 index 00e4a21198..0000000000 --- a/explorer-nextjs/app/icons/GatewaysSVG.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const GatewaysSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M16.2 12H22.7" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M1.30005 12H12" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path - d="M20.1 9.40015L22.7 12.0001L20.1 14.6001" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/LightSwitchSVG.tsx b/explorer-nextjs/app/icons/LightSwitchSVG.tsx deleted file mode 100644 index 7a32590dfc..0000000000 --- a/explorer-nextjs/app/icons/LightSwitchSVG.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const LightSwitchSVG: FCWithChildren = () => { - const { palette } = useTheme(); - return ( - <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z" - fill={palette.background.default} - /> - <path d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z" fill={palette.text.primary} /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/MixnodesSVG.tsx b/explorer-nextjs/app/icons/MixnodesSVG.tsx deleted file mode 100644 index 56902cb0de..0000000000 --- a/explorer-nextjs/app/icons/MixnodesSVG.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const MixnodesSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0621L2.97681 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M13.0103 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 2.99512L13.0103 23.0621" stroke={color} strokeMiterlimit="10" /> - <path - d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/MobileDrawerClose.tsx b/explorer-nextjs/app/icons/MobileDrawerClose.tsx deleted file mode 100644 index 6c8ecfacbc..0000000000 --- a/explorer-nextjs/app/icons/MobileDrawerClose.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; - -export const MobileDrawerClose: FCWithChildren = (props) => ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -5 24 24" width="25" height="25" {...props}> - <path - d="M0 12H13V10H0V12ZM0 7H10V5H0V7ZM0 0V2H13V0H0ZM18 9.59L14.42 6L18 2.41L16.59 1L11.59 6L16.59 11L18 9.59Z" - fill="#F2F2F2" - /> - </svg> -); diff --git a/explorer-nextjs/app/icons/NetworksSVG.tsx b/explorer-nextjs/app/icons/NetworksSVG.tsx deleted file mode 100644 index 0db2b9bdfb..0000000000 --- a/explorer-nextjs/app/icons/NetworksSVG.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NetworkComponentsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M17.2 10.5V4.40002L12 1.40002L6.8 4.40002V10.5L12 13.5L17.2 10.5Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M12 19.6V13.5L6.8 10.5L1.5 13.5V19.6L6.8 22.6L12 19.6Z" stroke={color} strokeMiterlimit="10" /> - <path d="M22.5 19.6V13.5L17.2 10.5L12 13.5V19.6L17.2 22.6L22.5 19.6Z" stroke={color} strokeMiterlimit="10" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/NodemapSVG.tsx b/explorer-nextjs/app/icons/NodemapSVG.tsx deleted file mode 100644 index 9486d64dcc..0000000000 --- a/explorer-nextjs/app/icons/NodemapSVG.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NodemapSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M1 9.6999C1 5.0999 4.7 1.3999 9.3 1.3999C13.9 1.3999 17.6 5.0999 17.6 9.6999C17.6 14.2999 9.3 21.5999 9.3 21.5999C9.3 21.5999 1 14.2999 1 9.6999Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M9.30005 12C11.233 12 12.8 10.433 12.8 8.5C12.8 6.567 11.233 5 9.30005 5C7.36705 5 5.80005 6.567 5.80005 8.5C5.80005 10.433 7.36705 12 9.30005 12Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M1.5 22.5999H17.1" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/NymVpn.tsx b/explorer-nextjs/app/icons/NymVpn.tsx deleted file mode 100644 index ab9f484d91..0000000000 --- a/explorer-nextjs/app/icons/NymVpn.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' - -interface DiscordIconProps { - size?: { width: number; height: number } -} - -export const NymVpnIcon: FCWithChildren<DiscordIconProps> = ({ size }) => ( - <svg - width={size?.width} - height={size?.height} - viewBox="0 0 170 24" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M19.6118 0.128906H19.5405V0.187854V20.7961L10.7849 0.164277L10.773 0.128906H10.7255H5.75959H0.187819H0.128418V0.187854V23.8142V23.8732H0.187819H5.75959H5.81899V23.8142V3.17063L14.6103 23.8378L14.6222 23.8732H14.6697H19.6118H25.1717H25.2311V23.8142V0.187854V0.128906H25.1717H19.6118Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M19.4121 0H25.3603V24H14.5297L14.4901 23.8819L5.94824 3.80121V24H0V0H10.8663L10.906 0.118132L19.4121 20.1621V0ZM19.5409 20.7951L10.7853 0.163225L10.7734 0.127854H0.128835V23.8721H5.81941V3.16958L14.6107 23.8368L14.6226 23.8721H25.2315V0.127854H19.5409V20.7951Z" - fill="white" - /> - <path - d="M89.8116 0.128906H79.1908H79.1314L79.1195 0.176068L73.6784 20.8904L68.2255 0.176068L68.2136 0.128906H68.1661H57.5215H57.4502V0.187854V23.8142V23.8732H57.5215H63.0814H63.1408V23.8142V3.33568L68.5225 23.826L68.5343 23.8732H68.5937H78.7394H78.7869L78.7988 23.826L84.1804 3.33568V23.8142V23.8732H84.2398H89.8116H89.871V23.8142V0.187854V0.128906H89.8116Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M79.0312 0H90.0003V24H84.052V4.33208L78.9242 23.856L78.9238 23.8572L78.8879 24H68.4342L68.3982 23.8572L68.3979 23.856L63.27 4.33208V24H57.3218V0H68.3146L68.3505 0.142699L68.3509 0.144015L73.6787 20.383L78.9949 0.144015L78.9953 0.142765L79.0312 0ZM73.6788 20.8894L68.2259 0.175015L68.214 0.127854H57.4506V23.8721H63.1412V3.33463L68.5229 23.825L68.5348 23.8721H78.7873L78.7992 23.825L84.1809 3.33463V23.8721H89.8714V0.127854H79.1318L79.1199 0.175015L73.6788 20.8894Z" - fill="white" - /> - <path - d="M48.2909 0.128906H48.2553L48.2434 0.152487L41.4836 11.8124L34.6882 0.152487L34.6763 0.128906H34.6407H28.2135H28.0947L28.1541 0.223225L38.6205 18.2142V23.8142V23.8732H38.6799H44.2517H44.3111V23.8142V18.2142L54.7775 0.223225L54.8369 0.128906H54.7181H48.2909Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M48.1757 0H55.0693L54.8879 0.288036L44.4399 18.2474V24H38.4917V18.2474L28.0437 0.288036L27.8623 0H34.756L34.8017 0.0907854L41.4833 11.5555L48.1299 0.0909153L48.1757 0ZM48.2434 0.151434L41.4836 11.8114L34.6882 0.151434L34.6763 0.127854H28.0948L28.1542 0.222173L38.6205 18.2131V23.8721H44.3111V18.2131L54.7775 0.222173L54.8369 0.127854H48.2553L48.2434 0.151434Z" - fill="white" - /> - <path - d="M169.238 0V24H166.422C166.006 24 165.654 23.9341 165.366 23.8023C165.088 23.6596 164.811 23.418 164.534 23.0776L153.542 8.76321C153.584 9.19149 153.611 9.60878 153.622 10.0151C153.643 10.4104 153.654 10.7838 153.654 11.1352V24H148.886V0H151.734C151.968 0 152.166 0.0109813 152.326 0.032944C152.486 0.0549066 152.63 0.0988326 152.758 0.164722C152.886 0.219629 153.008 0.30199 153.126 0.411805C153.243 0.521619 153.376 0.669869 153.526 0.856553L164.614 15.2697C164.56 14.8085 164.523 14.3638 164.502 13.9355C164.48 13.4962 164.47 13.0844 164.47 12.7001V0H169.238Z" - fill="#A8A6A6" - /> - <path - d="M134.206 11.7776C135.614 11.7776 136.627 11.4317 137.246 10.7399C137.865 10.048 138.174 9.08167 138.174 7.84077C138.174 7.29169 138.094 6.79204 137.934 6.3418C137.774 5.89156 137.529 5.50721 137.198 5.18874C136.878 4.8593 136.467 4.60673 135.966 4.43102C135.475 4.25532 134.889 4.16747 134.206 4.16747H131.39V11.7776H134.206ZM134.206 0C135.849 0 137.257 0.203157 138.43 0.609471C139.614 1.0048 140.585 1.55388 141.342 2.25669C142.11 2.95951 142.675 3.78861 143.038 4.74399C143.401 5.69938 143.582 6.73164 143.582 7.84077C143.582 9.03775 143.395 10.1359 143.022 11.1352C142.649 12.1345 142.078 12.9911 141.31 13.7049C140.542 14.4187 139.566 14.9787 138.382 15.385C137.209 15.7804 135.817 15.978 134.206 15.978H131.39V24H125.982V0H134.206Z" - fill="#A8A6A6" - /> - <path - d="M121.584 0L112.24 24H107.344L98 0H102.352C102.821 0 103.2 0.115305 103.488 0.345915C103.776 0.565545 103.995 0.851064 104.144 1.20247L108.656 14.0508C108.869 14.6108 109.077 15.2258 109.28 15.8957C109.483 16.5546 109.675 17.2464 109.856 17.9712C110.005 17.2464 110.171 16.5546 110.352 15.8957C110.544 15.2258 110.747 14.6108 110.96 14.0508L115.44 1.20247C115.557 0.894989 115.765 0.620452 116.064 0.378861C116.373 0.126287 116.752 0 117.2 0H121.584Z" - fill="#A8A6A6" - /> - </svg> -) diff --git a/explorer-nextjs/app/icons/OverviewSVG.tsx b/explorer-nextjs/app/icons/OverviewSVG.tsx deleted file mode 100644 index 2ced0bb17b..0000000000 --- a/explorer-nextjs/app/icons/OverviewSVG.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const OverviewSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M1.4 21.6H22.6" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M14.1 2.40002H9.9V21.5H14.1V2.40002Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M20.8 6.59998H16.6V21.5H20.8V6.59998Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M7.4 11.8H3.2V21.6H7.4V11.8Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/TokenSVG.tsx b/explorer-nextjs/app/icons/TokenSVG.tsx deleted file mode 100644 index 94ab1468c9..0000000000 --- a/explorer-nextjs/app/icons/TokenSVG.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; - -export const TokenSVG: FCWithChildren = () => { - const color = 'white'; - - return ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <g clipPath="url(#clip0_2549_7563)"> - <path - d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z" - fill={color} - /> - <path - d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_2549_7563"> - <rect width="24" height="24" fill="white" transform="translate(0 0.5)" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/ValidatorsSVG.tsx b/explorer-nextjs/app/icons/ValidatorsSVG.tsx deleted file mode 100644 index cf03f1c330..0000000000 --- a/explorer-nextjs/app/icons/ValidatorsSVG.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const ValidatorsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clipPath="url(#clip0)"> - <path - d="M18.2001 18.4V19.7001C18.2001 21.4001 16.9 22.7001 15.2 22.7001H4.30005C2.60005 22.7001 1.30005 21.4001 1.30005 19.7001V4.30005C1.30005 2.60005 2.60005 1.30005 4.30005 1.30005H15.1C16.8 1.30005 18.1 2.60005 18.1 4.30005V5.60005V18.4H18.2001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M13.4 22.7001H17.4C19.1 22.7001 20.4 21.4001 20.4 19.7001V18.4V5.60005V4.30005C20.4 2.60005 19.1 1.30005 17.4 1.30005H11.5" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M15.2 22.7001H19.7C21.4 22.7001 22.7 21.4001 22.7 19.7001V18.4V5.60005V4.30005C22.7 2.60005 21.4 1.30005 19.7 1.30005H13.8" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M5 12.3L7.9 15.3L14.5 8.69995" - stroke={color} - strokeWidth="2" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - </g> - <defs> - <clipPath id="clip0"> - <rect width="24" height="24" fill="white" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/socials/DiscordIcon.tsx b/explorer-nextjs/app/icons/socials/DiscordIcon.tsx deleted file mode 100644 index b81c571c33..0000000000 --- a/explorer-nextjs/app/icons/socials/DiscordIcon.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface DiscordIconProps { - size?: number | string - color?: string -} - -export const DiscordIcon: FCWithChildren<DiscordIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2296)"> - <path - d="M12.4 0C5.80002 0 0.400024 5.4 0.400024 12C0.400024 18.6 5.80002 24 12.4 24C19 24 24.4 18.6 24.4 12C24.4 5.4 19 0 12.4 0ZM20.1 15.9C18.8 16.9 17.5 17.5 16.2 17.9C16.2 17.9 16.2 17.9 16.1 17.9C15.8 17.5 15.5 17.1 15.3 16.6V16.5C15.7 16.3 16.1 16.1 16.5 15.9V15.8C16.4 15.7 16.3 15.7 16.3 15.6C16.3 15.6 16.3 15.6 16.2 15.6C13.7 16.8 10.9 16.8 8.40002 15.6C8.40002 15.6 8.40002 15.6 8.30002 15.6C8.20002 15.7 8.10002 15.7 8.10002 15.8V15.9C8.50002 16.1 8.90002 16.3 9.30002 16.5C9.30002 16.5 9.30002 16.5 9.30002 16.6C9.10002 17.1 8.80002 17.5 8.50002 17.9C8.50002 17.9 8.50002 17.9 8.40002 17.9C7.10002 17.5 5.90002 16.9 4.50002 15.9C4.40002 13 5.00002 10.1 7.00002 7.1C8.00002 6.6 9.00002 6.3 10.2 6.1C10.2 6.1 10.2 6.1 10.3 6.1C10.4 6.3 10.6 6.7 10.7 6.9C11.9 6.7 13.1 6.7 14.2 6.9C14.3 6.7 14.5 6.3 14.6 6.1C14.6 6.1 14.6 6.1 14.7 6.1C15.8 6.3 16.9 6.6 17.9 7.1C19.5 9.7 20.4 12.6 20.1 15.9Z" - fill={color} - /> - <path - d="M15 11C14.2 11 13.6 11.7 13.6 12.6C13.6 13.5 14.2 14.2 15 14.2C15.8 14.2 16.4 13.5 16.4 12.6C16.4 11.7 15.8 11 15 11Z" - fill={color} - /> - <path - d="M9.80002 11C9.10002 11 8.40002 11.7 8.40002 12.6C8.40002 13.5 9.00002 14.2 9.80002 14.2C10.6 14.2 11.2 13.5 11.2 12.6C11.2 11.7 10.6 11 9.80002 11Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2296"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/GitHubIcon.tsx b/explorer-nextjs/app/icons/socials/GitHubIcon.tsx deleted file mode 100644 index 11389e6969..0000000000 --- a/explorer-nextjs/app/icons/socials/GitHubIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface GitHubIconProps { - size?: number | string - color?: string -} - -export const GitHubIcon: FCWithChildren<GitHubIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2302)"> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M12.7 0C5.90002 0 0.400024 5.5 0.400024 12.3C0.400024 17.7 3.90002 22.3 8.80002 24C9.40002 24.1 9.60002 23.7 9.60002 23.4C9.60002 23.1 9.60002 22.1 9.60002 21.1C6.50002 21.7 5.70002 20.3 5.50002 19.7C5.40002 19.3 4.80002 18.3 4.20002 18C3.80002 17.8 3.20002 17.2 4.20002 17.2C5.20002 17.2 5.90002 18.1 6.10002 18.5C7.20002 20.4 9.00002 19.8 9.70002 19.5C9.80002 18.7 10.1 18.2 10.5 17.9C7.80002 17.6 4.90002 16.5 4.90002 11.8C4.90002 10.5 5.40002 9.4 6.20002 8.5C6.00002 8 5.60002 6.8 6.30002 5.1C6.30002 5.1 7.30002 4.8 9.70002 6.4C10.7 6.1 11.7 6 12.8 6C13.8 6 14.9 6.1 15.9 6.4C18.3 4.8 19.3 5.1 19.3 5.1C20 6.8 19.5 8.1 19.4 8.4C20.2 9.3 20.7 10.4 20.7 11.7C20.7 16.4 17.8 17.5 15.1 17.8C15.5 18.2 15.9 18.9 15.9 20.1C15.9 21.7 15.9 23.1 15.9 23.5C15.9 23.8 16.1 24.2 16.7 24.1C21.6 22.5 25.1 17.9 25.1 12.4C25 5.5 19.5 0 12.7 0Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2302"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/TelegramIcon.tsx b/explorer-nextjs/app/icons/socials/TelegramIcon.tsx deleted file mode 100644 index a66020a849..0000000000 --- a/explorer-nextjs/app/icons/socials/TelegramIcon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface TelegramIconProps { - size?: number | string - color?: string -} - -export const TelegramIcon: FCWithChildren<TelegramIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <path - d="M12.4 24C19.029 24 24.4 18.629 24.4 12C24.4 5.371 19.029 0 12.4 0C5.77102 0 0.400024 5.371 0.400024 12C0.400024 18.629 5.77102 24 12.4 24ZM5.89102 11.74L17.461 7.279C17.998 7.085 18.467 7.41 18.293 8.222L18.294 8.221L16.324 17.502C16.178 18.16 15.787 18.32 15.24 18.01L12.24 15.799L10.793 17.193C10.633 17.353 10.498 17.488 10.188 17.488L10.401 14.435L15.961 9.412C16.203 9.199 15.907 9.079 15.588 9.291L8.71702 13.617L5.75502 12.693C5.11202 12.489 5.09802 12.05 5.89102 11.74Z" - fill={color} - /> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/TwitterIcon.tsx b/explorer-nextjs/app/icons/socials/TwitterIcon.tsx deleted file mode 100644 index 5d94de99a3..0000000000 --- a/explorer-nextjs/app/icons/socials/TwitterIcon.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface TwitterIconProps { - size?: number | string - color?: string -} - -export const TwitterIcon: FCWithChildren<TwitterIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2294)"> - <path - d="M12.4 0C5.77362 0 0.400024 5.3736 0.400024 12C0.400024 18.6264 5.77362 24 12.4 24C19.0264 24 24.4 18.6264 24.4 12C24.4 5.3736 19.0264 0 12.4 0ZM17.8791 9.35632C17.8844 9.47443 17.887 9.59308 17.887 9.71228C17.887 13.3519 15.1166 17.5488 10.0502 17.549H10.0504H10.0502C8.49475 17.549 7.0473 17.0931 5.82837 16.3118C6.04388 16.3372 6.26324 16.3499 6.48535 16.3499C7.77588 16.3499 8.9635 15.9097 9.90631 15.1708C8.70056 15.1485 7.68396 14.3522 7.33313 13.2578C7.50104 13.29 7.67371 13.3076 7.85077 13.3076C8.10217 13.3076 8.3457 13.2737 8.57715 13.2105C7.31683 12.9582 6.36743 11.8444 6.36743 10.5106C6.36743 10.4982 6.36743 10.487 6.3678 10.4755C6.73895 10.6818 7.16339 10.806 7.6153 10.8199C6.87573 10.3264 6.38959 9.48285 6.38959 8.52722C6.38959 8.02258 6.526 7.5498 6.76257 7.14276C8.12085 8.80939 10.1508 9.90546 12.4399 10.0206C12.3927 9.81885 12.3683 9.60864 12.3683 9.39258C12.3683 7.87207 13.6019 6.63849 15.123 6.63849C15.9153 6.63849 16.6309 6.97339 17.1335 7.50879C17.761 7.38501 18.3502 7.15576 18.8825 6.84027C18.6765 7.48315 18.24 8.02258 17.6713 8.36371C18.2285 8.29706 18.7595 8.14929 19.2529 7.92993C18.8843 8.48236 18.4169 8.96759 17.8791 9.35632V9.35632Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2294"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/layout.tsx b/explorer-nextjs/app/layout.tsx deleted file mode 100644 index a532714d84..0000000000 --- a/explorer-nextjs/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from 'next' -import '@interchain-ui/react/styles' -import { App } from './App' - -export const metadata: Metadata = { - title: 'Nym Network Explorer', -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - <html lang="en"> - <body> - <App>{children}</App> - </body> - </html> - ) -} diff --git a/explorer-nextjs/app/loading.tsx b/explorer-nextjs/app/loading.tsx deleted file mode 100644 index bb90466dc5..0000000000 --- a/explorer-nextjs/app/loading.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { LinearProgress, Box } from '@mui/material' - -export default function Loading() { - return ( - <Box sx={{ py: 16 }}> - <LinearProgress /> - </Box> - ) -} diff --git a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx b/explorer-nextjs/app/network-components/gateways/[id]/page.tsx deleted file mode 100644 index 87f779872a..0000000000 --- a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client' - -import * as React from 'react' -import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material' -import { useParams } from 'next/navigation' -import {GatewayBond, LocatedGateway} from '@/app/typeDefs/explorer-api' -import { ColumnsType, DetailTable } from '@/app/components/DetailTable' -import { - gatewayEnrichedToGridRow, - GatewayEnrichedRowType, -} from '@/app/components/Gateways/Gateways' -import { ComponentError } from '@/app/components/ComponentError' -import { ContentCard } from '@/app/components/ContentCard' -import { TwoColSmallTable } from '@/app/components/TwoColSmallTable' -import { UptimeChart } from '@/app/components/UptimeChart' -import { - GatewayContextProvider, - useGatewayContext, -} from '@/app/context/gateway' -import { useMainContext } from '@/app/context/main' -import { Title } from '@/app/components/Title' - -const columns: ColumnsType[] = [ - { - field: 'identity_key', - title: 'Identity Key', - headerAlign: 'left', - width: 230, - }, - { - field: 'bond', - title: 'Bond', - headerAlign: 'left', - }, - { - field: 'avgUptime', - title: 'Avg. Score', - headerAlign: 'left', - tooltipInfo: "Gateway's average routing score in the last 24 hours", - }, - { - field: 'host', - title: 'IP', - headerAlign: 'left', - width: 99, - }, - { - field: 'location', - title: 'Location', - headerAlign: 'left', - }, - { - field: 'owner', - title: 'Owner', - headerAlign: 'left', - }, - { - field: 'version', - title: 'Version', - headerAlign: 'left', - }, -] - -/** - * Shows gateway details - */ -const PageGatewayDetailsWithState = ({ - selectedGateway, -}: { - selectedGateway: LocatedGateway | undefined -}) => { - const [enrichGateway, setEnrichGateway] = - React.useState<GatewayEnrichedRowType>() - const [status, setStatus] = React.useState<number[] | undefined>() - const { uptimeReport, uptimeStory } = useGatewayContext() - - React.useEffect(() => { - if (uptimeReport?.data && selectedGateway) { - setEnrichGateway( - gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data) - ) - } - }, [uptimeReport, selectedGateway]) - - React.useEffect(() => { - if (enrichGateway) { - setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]) - } - }, [enrichGateway]) - - return ( - <Box component="main"> - <Title text="Legacy Gateway Detail" /> - - <Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}> - <AlertTitle> - Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet - </AlertTitle> - </Alert> - - <Grid container> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Gateway detail table" - rows={enrichGateway ? [enrichGateway] : []} - /> - </Grid> - </Grid> - - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Gateway Status"> - <TwoColSmallTable - loading={false} - keys={['Mix port', 'Client WS API Port']} - values={status.map((each) => each)} - icons={status.map((elem) => !!elem)} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && ( - <ComponentError text="There was a problem retrieving routing score." /> - )} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ) -} - -/** - * Guard component to handle loadingW and not found states - */ -const PageGatewayDetailGuard = () => { - const [selectedGateway, setSelectedGateway] = React.useState<LocatedGateway>() - const { gateways } = useMainContext() - const { id } = useParams() - - React.useEffect(() => { - if (gateways?.data) { - setSelectedGateway( - gateways.data.find((g) => g.gateway.identity_key === id) - ) - } - }, [gateways, id]) - - if (gateways?.isLoading) { - return <CircularProgress /> - } - - if (gateways?.error) { - // eslint-disable-next-line no-console - console.error(gateways?.error) - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ) - } - - // loaded, but not found - if (gateways && !gateways.isLoading && !gateways.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Gateway not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ) - } - - return <PageGatewayDetailsWithState selectedGateway={selectedGateway} /> -} - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -const PageGatewayDetail = () => { - const { id } = useParams() - - if (!id || typeof id !== 'string') { - return ( - <Alert severity="error">Oh no! No mixnode identity key specified</Alert> - ) - } - - return ( - <GatewayContextProvider gatewayIdentityKey={id}> - <PageGatewayDetailGuard /> - </GatewayContextProvider> - ) -} - -export default PageGatewayDetail diff --git a/explorer-nextjs/app/network-components/gateways/page.tsx b/explorer-nextjs/app/network-components/gateways/page.tsx deleted file mode 100644 index c2660ce387..0000000000 --- a/explorer-nextjs/app/network-components/gateways/page.tsx +++ /dev/null @@ -1,256 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { Box, Card, Grid, Stack } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid' -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' -import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip' -import { diff, gte, rcompare } from 'semver' -import { useMainContext } from '@/app/context/main' -import { TableToolbar } from '@/app/components/TableToolbar' -import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' -import { Title } from '@/app/components/Title' -import { unymToNym } from '@/app/utils/currency' -import { Tooltip } from '@/app/components/Tooltip' -import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' -import { splice } from '@/app/utils' -import { - VersionDisplaySelector, - VersionSelectOptions, -} from '@/app/components/Gateways/VersionDisplaySelector' -import StyledLink from '@/app/components/StyledLink' -import { - GatewayRowType, - gatewayToGridRow, -} from '@/app/components/Gateways/Gateways' -import {LocatedGateway} from "@/app/typeDefs/explorer-api"; - -const gatewaySanitize = (g?: LocatedGateway): boolean => { - if(!g) { - return false; - } - - if(!g.gateway.version || !g.gateway.version.trim().length) { - return false; - } - - if(g.gateway.version === "null") { - return false; - } - - return true; -} - -const PageGateways = () => { - const { gateways } = useMainContext() - const [versionFilter, setVersionFilter] = - React.useState<VersionSelectOptions>(VersionSelectOptions.all) - - const theme = useTheme() - - const highestVersion = React.useMemo(() => { - if (gateways?.data) { - const versions = gateways.data.filter(gatewaySanitize).reduce( - (a: string[], b) => [...a, b.gateway.version], - [] - ) - const [lastestVersion] = versions.sort(rcompare) - return lastestVersion - } - // fallback value - return '2.0.0' - }, [gateways]) - - const filterByLatestVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version) - return versionDiff === 'patch' || versionDiff === null - }) - if (filtered) return filtered - return [] - }, [gateways]) - - const filterByOlderVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version) - return versionDiff === 'major' || versionDiff === 'minor' - }) - if (filtered) return filtered - return [] - }, [gateways]) - - const filteredByVersion = React.useMemo(() => { - switch (versionFilter) { - case VersionSelectOptions.latestVersion: - return filterByLatestVersions - case VersionSelectOptions.olderVersions: - return filterByOlderVersions - case VersionSelectOptions.all: - return gateways?.data || [] - default: - return [] - } - }, [versionFilter, gateways]) - - const data = useMemo(() => { - return gatewayToGridRow(filteredByVersion || []) - }, [filteredByVersion]) - - const columns = useMemo<MRT_ColumnDef<GatewayRowType>[]>(() => { - return [ - { - id: 'gateway-data', - header: 'Gateways Data', - columns: [ - { - id: 'identity_key', - header: 'Identity Key', - accessorKey: 'identity_key', - size: 250, - Cell: ({ row }) => { - return ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={row.original.identity_key} - tooltip={`Copy identity key ${row.original.identity_key} to clipboard`} - /> - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - dataTestId="identity-link" - color="text.primary" - > - {splice(7, 29, row.original.identity_key)} - </StyledLink> - </Stack> - ) - }, - }, - { - id: 'version', - header: 'Version', - accessorKey: 'version', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - data-testid="version" - color="text.primary" - > - {row.original.version} - </StyledLink> - ) - }, - }, - { - id: 'location', - header: 'Location', - accessorKey: 'location', - size: 150, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip - text={row.original.location} - id="gateway-location-text" - > - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {row.original.location} - </Box> - </Tooltip> - </Box> - ) - }, - }, - { - id: 'host', - header: 'IP:Port', - accessorKey: 'host', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - data-testid="host" - color="text.primary" - > - {row.original.host} - </StyledLink> - ) - }, - }, - { - id: 'owner', - header: 'Owner', - accessorKey: 'owner', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} - target="_blank" - data-testid="owner" - color="text.primary" - > - {splice(7, 29, row.original.owner)} - </StyledLink> - ) - }, - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data, - }) - - return ( - <> - <Box mb={2}> - <Title text="Legacy Gateways" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <VersionDisplaySelector - handleChange={(option) => setVersionFilter(option)} - selected={versionFilter} - /> - } - /> - <MaterialReactTable table={table} /> - </Card> - </Grid> - </Grid> - </> - ) -} - -export default PageGateways diff --git a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx b/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx deleted file mode 100644 index 5e03b5d132..0000000000 --- a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx +++ /dev/null @@ -1,302 +0,0 @@ -'use client' - -import * as React from 'react' -import { - Alert, - AlertTitle, - Box, - CircularProgress, - Grid, - Typography, -} from '@mui/material' -import { ColumnsType, DetailTable } from '@/app/components/DetailTable' -import { BondBreakdownTable } from '@/app/components/MixNodes/BondBreakdown' -import { - DelegatorsInfoTable, - EconomicsInfoColumns, - EconomicsInfoRows, -} from '@/app/components/MixNodes/Economics' -import { ComponentError } from '@/app/components/ComponentError' -import { ContentCard } from '@/app/components/ContentCard' -import { TwoColSmallTable } from '@/app/components/TwoColSmallTable' -import { UptimeChart } from '@/app/components/UptimeChart' -import { WorldMap } from '@/app/components/WorldMap' -import { MixNodeDetailSection } from '@/app/components/MixNodes/DetailSection' -import { - MixnodeContextProvider, - useMixnodeContext, -} from '@/app/context/mixnode' -import { Title } from '@/app/components/Title' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { useParams } from 'next/navigation' - -const columns: ColumnsType[] = [ - { - field: 'owner', - title: 'Owner', - width: '15%', - }, - { - field: 'identity_key', - title: 'Identity Key', - width: '15%', - }, - - { - field: 'bond', - title: 'Stake', - 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 940k NYMs, 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 %', - tooltipInfo: - "Percentage of the operator's bond to the total stake on the node", - }, - - { - field: 'host', - width: '10%', - title: 'Host', - }, - { - field: 'location', - title: 'Location', - }, - - { - field: 'layer', - title: 'Layer', - }, -] - -/** - * Shows mix node details - */ -const PageMixnodeDetailWithState = () => { - const { - mixNode, - mixNodeRow, - description, - stats, - status, - uptimeStory, - uniqDelegations, - } = useMixnodeContext() - const isMobile = useIsMobile() - return ( - <Box component="main"> - <Title text="Legacy Mixnode Detail" /> - <Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}> - <AlertTitle> - Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet - </AlertTitle> - </Alert> - <Grid container spacing={2} mt={1} mb={6}> - <Grid item xs={12}> - {mixNodeRow && description?.data && ( - <MixNodeDetailSection - mixNodeRow={mixNodeRow} - mixnodeDescription={description.data} - /> - )} - {mixNodeRow?.blacklisted && ( - <Typography - textAlign={isMobile ? 'left' : 'right'} - fontSize="smaller" - sx={{ color: 'error.main' }} - > - This node is having a poor performance - </Typography> - )} - </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 - columnsData={EconomicsInfoColumns} - tableName="Delegators info table" - rows={[EconomicsInfoRows()]} - /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <ContentCard - title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`} - > - <BondBreakdownTable /> - </ContentCard> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - <ContentCard title="Mixnode Stats"> - {stats && ( - <> - {stats.error && ( - <ComponentError text="There was a problem retrieving this nodes stats." /> - )} - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since startup" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_startup || 0, - stats?.data?.packets_sent_since_startup || 0, - stats?.data?.packets_explicitly_dropped_since_startup || 0, - ]} - /> - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since last update" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_last_update || 0, - stats?.data?.packets_sent_since_last_update || 0, - stats?.data?.packets_explicitly_dropped_since_last_update || - 0, - ]} - marginBottom - /> - </> - )} - {!stats && <Typography>No stats information</Typography>} - </ContentCard> - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && ( - <ComponentError text="There was a problem retrieving routing score." /> - )} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Mixnode Status"> - {status.error && ( - <ComponentError text="There was a problem retrieving port information" /> - )} - <TwoColSmallTable - loading={status.isLoading} - error={status?.error?.message} - keys={['Mix port', 'Verloc port', 'HTTP port']} - values={[1789, 1790, 8000].map((each) => each)} - icons={ - (status?.data?.ports && Object.values(status.data.ports)) || [ - false, - false, - false, - ] - } - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {mixNode && ( - <ContentCard title="Location"> - {mixNode?.error && ( - <ComponentError text="There was a problem retrieving this mixnode location" /> - )} - {mixNode?.data?.location?.latitude && - mixNode?.data?.location?.longitude && ( - <WorldMap - loading={mixNode.isLoading} - userLocation={[ - mixNode.data.location.longitude, - mixNode.data.location.latitude, - ]} - /> - )} - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ) -} - -/** - * Guard component to handle loading and not found states - */ -const PageMixnodeDetailGuard = () => { - const { mixNode } = useMixnodeContext() - const { id } = useParams() - - if (mixNode?.isLoading) { - return <CircularProgress /> - } - - if (mixNode?.error) { - // eslint-disable-next-line no-console - console.error(mixNode?.error) - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ) - } - - // loaded, but not found - if (mixNode && !mixNode.isLoading && !mixNode.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Mixnode not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ) - } - - return <PageMixnodeDetailWithState /> -} - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -const PageMixnodeDetail = () => { - const { id } = useParams() - - if (!id || typeof id !== 'string') { - return ( - <Alert severity="error">Oh no! No mixnode identity key specified</Alert> - ) - } - - return ( - <MixnodeContextProvider mixId={id}> - <PageMixnodeDetailGuard /> - </MixnodeContextProvider> - ) -} - -export default PageMixnodeDetail diff --git a/explorer-nextjs/app/network-components/mixnodes/page.tsx b/explorer-nextjs/app/network-components/mixnodes/page.tsx deleted file mode 100644 index 2f2774a7f4..0000000000 --- a/explorer-nextjs/app/network-components/mixnodes/page.tsx +++ /dev/null @@ -1,382 +0,0 @@ -'use client' - -import React, { useCallback, useMemo } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import { - MaterialReactTable, - useMaterialReactTable, - type MRT_ColumnDef, -} from 'material-react-table' -import { Grid, Card, Button, Box, Stack } from '@mui/material' -import { - CustomColumnHeading, - DelegateIconButton, - DelegateModal, - DelegationModal, - DelegationModalProps, - MixNodeStatusDropdown, - MixnodeRowType, - StyledLink, - TableToolbar, - Title, - Tooltip, - mixnodeToGridRow, -} from '@/app/components' -import { DelegationsProvider } from '@/app/context/delegations' -import { useWalletContext } from '@/app/context/wallet' -import { useGetMixNodeStatusColor, useIsMobile } from '@/app/hooks' -import { useMainContext } from '@/app/context/main' -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' -import { splice } from '@/app/utils' -import { currencyToString } from '@/app/utils/currency' -import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' -import { - MixnodeStatusWithAll, - toMixnodeStatus, -} from '@/app/typeDefs/explorer-api' - -export default function MixnodesPage() { - const isMobile = useIsMobile() - const { isWalletConnected } = useWalletContext() - const { mixnodes, fetchMixnodes } = useMainContext() - const router = useRouter() - - const [itemSelectedForDelegation, setItemSelectedForDelegation] = - React.useState<{ - mixId: number - identityKey: string - }>() - const [confirmationModalProps, setConfirmationModalProps] = React.useState< - DelegationModalProps | undefined - >() - - const search = useSearchParams() - const status = search.get('status') as MixnodeStatusWithAll - - React.useEffect(() => { - // when the status changes, get the mixnodes - fetchMixnodes(toMixnodeStatus(status)) - }, [status]) - - const handleMixnodeStatusChanged = (newStatus?: MixnodeStatusWithAll) => { - router.push( - newStatus && newStatus !== 'all' - ? `/network-components/mixnodes?status=${newStatus}` - : '/network-components/mixnodes' - ) - } - - const handleOnDelegate = useCallback( - ({ identityKey, mixId }: { identityKey: string; mixId: number }) => { - if (!isWalletConnected) { - setConfirmationModalProps({ - status: 'info', - message: 'Please connect your wallet to delegate', - }) - } else { - setItemSelectedForDelegation({ identityKey, mixId }) - } - }, - [isWalletConnected] - ) - - const handleNewDelegation = (delegationModalProps: DelegationModalProps) => { - setItemSelectedForDelegation(undefined) - setConfirmationModalProps(delegationModalProps) - } - - const columns = useMemo<MRT_ColumnDef<MixnodeRowType>[]>(() => { - return [ - { - id: 'mixnode-data', - header: 'Mixnode Data', - columns: [ - { - id: 'delegate', - accessorKey: 'delegate', - size: isMobile ? 50 : 150, - header: '', - grow: false, - Cell: ({ row }) => ( - <DelegateIconButton - size="small" - onDelegate={() => - handleOnDelegate({ - identityKey: row.original.identity_key, - mixId: row.original.mix_id, - }) - } - /> - ), - enableSorting: false, - enableColumnActions: false, - Filter: () => null, - }, - { - id: 'identity_key', - header: 'Identity Key', - accessorKey: 'identity_key', - size: 250, - Cell: ({ row }) => { - return ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={row.original.identity_key} - tooltip={`Copy identity key ${row.original.identity_key} to clipboard`} - /> - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - dataTestId="identity-link" - > - {splice(7, 29, row.original.identity_key)} - </StyledLink> - </Stack> - ) - }, - }, - { - id: 'bond', - header: 'Stake', - accessorKey: 'bond', - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - > - {currencyToString({ amount: row.original.bond.toString() })} - </StyledLink> - ), - }, - { - id: 'stake_saturation', - header: 'Stake Saturation', - accessorKey: 'stake_saturation', - size: 225, - Header() { - return ( - <CustomColumnHeading - headingTitle="Stake Saturation" - 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 940k NYMs, 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." - /> - ) - }, - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.stake_saturation} %`}</StyledLink> - ), - }, - { - id: 'pledge_amount', - header: 'Bond', - accessorKey: 'pledge_amount', - size: 185, - Header: () => ( - <CustomColumnHeading - headingTitle="Bond" - tooltipInfo="Node operator's share of stake." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - > - {currencyToString({ - amount: row.original.pledge_amount.toString(), - })} - </StyledLink> - ), - }, - { - id: 'profit_percentage', - accessorKey: 'profit_percentage', - header: 'Profit Margin', - size: 145, - Header: () => ( - <CustomColumnHeading - headingTitle="Profit Margin" - tooltipInfo="Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.profit_percentage}%`}</StyledLink> - ), - }, - { - id: 'operating_cost', - accessorKey: 'operating_cost', - size: 220, - header: 'Operating Cost', - disableColumnMenu: true, - Header: () => ( - <CustomColumnHeading - headingTitle="Operating Cost" - 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." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.operating_cost} NYM`}</StyledLink> - ), - }, - { - id: 'owner', - accessorKey: 'owner', - size: 150, - header: 'Owner', - Header: () => <CustomColumnHeading headingTitle="Owner" />, - Cell: ({ row }) => ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} - color={useGetMixNodeStatusColor(row.original.status)} - target="_blank" - data-testid="big-dipper-link" - > - {splice(7, 29, row.original.owner)} - </StyledLink> - ), - }, - { - id: 'location', - accessorKey: 'location', - header: 'Location', - maxSize: 150, - Header: () => <CustomColumnHeading headingTitle="Location" />, - Cell: ({ row }) => ( - <Tooltip text={row.original.location} id="mixnode-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - cursor: 'pointer', - color: useGetMixNodeStatusColor(row.original.status), - }} - > - {row.original.location} - </Box> - </Tooltip> - ), - }, - { - id: 'host', - accessorKey: 'host', - header: 'Host', - size: 130, - Header: () => <CustomColumnHeading headingTitle="Host" />, - Cell: ({ row }) => ( - <StyledLink - color={useGetMixNodeStatusColor(row.original.status)} - to={`/network-components/mixnodes/${row.original.mix_id}`} - > - {row.original.host} - </StyledLink> - ), - }, - ], - }, - ] - }, [handleOnDelegate, isMobile]) - - const data = useMemo(() => { - return mixnodeToGridRow(mixnodes?.data) - }, [mixnodes?.data]) - - const table = useMaterialReactTable({ - columns, - data, - enableFullScreenToggle: false, - state: { - isLoading: mixnodes?.isLoading, - }, - layoutMode: 'grid-no-grow', - initialState: { - columnPinning: { left: ['delegate'] }, - }, - }) - - return ( - <DelegationsProvider> - <Box mb={2}> - <Title text="Legacy Mixnodes" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <MixNodeStatusDropdown - sx={{ mr: 2 }} - status={status} - onSelectionChanged={handleMixnodeStatusChanged} - /> - } - childrenAfter={ - isWalletConnected && ( - <Button - fullWidth - size="large" - variant="outlined" - onClick={() => router.push('/delegations')} - > - Delegations - </Button> - ) - } - /> - <MaterialReactTable table={table} /> - </Card> - </Grid> - </Grid> - {itemSelectedForDelegation && ( - <DelegateModal - onClose={() => { - setItemSelectedForDelegation(undefined) - }} - header="Delegate" - buttonText="Delegate stake" - denom="nym" - onOk={(delegationModalProps: DelegationModalProps) => - handleNewDelegation(delegationModalProps) - } - identityKey={itemSelectedForDelegation.identityKey} - mixId={itemSelectedForDelegation.mixId} - /> - )} - - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - setConfirmationModalProps(undefined) - if (confirmationModalProps.status === 'success') { - router.push('/delegations') - } - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - </DelegationsProvider> - ) -} diff --git a/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx b/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx deleted file mode 100644 index fac36689f2..0000000000 --- a/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import { Chip } from "@mui/material"; - -export const DeclaredRole = ({ declared_role }: { declared_role?: any }) => ( - <> - {declared_role?.mixnode && <Chip size="small" label="Mixnode" sx={{ mr: 0.5 }} color="info" />} - {declared_role?.entry && <Chip size="small" label="Entry" sx={{ mr: 0.5 }} color="success" />} - {declared_role?.exit_nr && <Chip size="small" label="Exit NR" sx={{ mr: 0.5 }} color="warning" />} - {declared_role?.exit_ipr && <Chip size="small" label="Exit IPR" sx={{ mr: 0.5 }} color="warning" />} - </> -) \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx b/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx deleted file mode 100644 index a1b754ccce..0000000000 --- a/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, {useMemo} from "react"; -import {MaterialReactTable, MRT_ColumnDef, useMaterialReactTable} from "material-react-table"; -import StyledLink from "../../../components/StyledLink"; -import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants"; -import {splice} from "@/app/utils"; -import {humanReadableCurrencyToString} from "@/app/utils/currency"; -import {Typography} from "@mui/material"; -import {useTheme} from "@mui/material/styles"; -import WarningIcon from '@mui/icons-material/Warning'; -import { Tooltip } from '@/app/components/Tooltip' - -export const NodeDelegationsTable = ({ node }: { node: any}) => { - const columns = useMemo<MRT_ColumnDef<any>[]>(() => { - return [ - { - id: 'nym-node-delegation-data', - header: 'Nym Node Delegations', - columns: [ - { - id: 'owner', - header: 'Delegator', - accessorKey: 'owner', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner || "-"}`} - target="_blank" - data-testid="bond_information.node.owner" - color="text.primary" - > - {splice(7, 29, row.original.owner)} - </StyledLink> - ) - }, - }, - { - id: 'amount', - header: 'Amount', - accessorKey: 'amount', - size: 150, - Cell: ({ row }) => ( - <>{humanReadableCurrencyToString(row.original.amount)}</> - ) - }, - { - id: 'height', - header: 'Delegated at height', - accessorKey: 'height', - size: 150, - }, - { - id: 'proxy', - header: 'From vesting account?', - accessorKey: 'proxy', - size: 250, - Cell: ({ row }) => { - if(row.original.proxy?.length) { - return ( - <VestingDelegationWarning>Please re-delegate from your main account</VestingDelegationWarning> - ) - } - } - }, - ] - } - ]; - }, []); - - const table = useMaterialReactTable({ - columns, - data: node ? node.delegations : [], - }); - - return ( - <MaterialReactTable table={table} /> - ); -} - -export const VestingDelegationWarning = ({children, plural}: { plural?: boolean, children: React.ReactNode}) => { - const theme = useTheme(); - return ( - <Tooltip - text={`${plural ? 'These delegations have' : 'This delegation has'} been made with a vesting account. All tokens are liquid, if you are the delegator, please move the tokens into your main account and make the delegation from there.`} - id="delegations" - > - <Typography fontSize="inherit" color={theme.palette.warning.main} display="flex" alignItems="center"> - <WarningIcon sx={{ mr: 0.5 }}/> - {children} - </Typography> - </Tooltip> - ); -} \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/page.tsx b/explorer-nextjs/app/network-components/nodes/[id]/page.tsx deleted file mode 100644 index 95a172d21e..0000000000 --- a/explorer-nextjs/app/network-components/nodes/[id]/page.tsx +++ /dev/null @@ -1,336 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Alert, AlertTitle, Box, CircularProgress, Grid } from "@mui/material"; -import { useParams } from "next/navigation"; -import { ColumnsType, DetailTable } from "@/app/components/DetailTable"; -import { ComponentError } from "@/app/components/ComponentError"; -import { ContentCard } from "@/app/components/ContentCard"; -import { UptimeChart } from "@/app/components/UptimeChart"; -import { NymNodeContextProvider, useNymNodeContext } from "@/app/context/node"; -import { useMainContext } from "@/app/context/main"; -import { Title } from "@/app/components/Title"; -import Paper from "@mui/material/Paper"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableRow from "@mui/material/TableRow"; -import { humanReadableCurrencyToString } from "@/app/utils/currency"; -import { DeclaredRole } from "@/app/network-components/nodes/DeclaredRole"; -import { - NodeDelegationsTable, - VestingDelegationWarning, -} from "@/app/network-components/nodes/[id]/NodeDelegationsTable"; - -const columns: ColumnsType[] = [ - { - field: "identity_key", - title: "Identity Key", - headerAlign: "left", - width: 230, - }, - { - field: "bond", - title: "Bond", - headerAlign: "left", - }, - { - field: "host", - title: "IP", - headerAlign: "left", - width: 99, - }, - { - field: "location", - title: "Location", - headerAlign: "left", - }, - { - field: "owner", - title: "Owner", - headerAlign: "left", - }, - { - field: "version", - title: "Version", - headerAlign: "left", - }, -]; - -interface NodeEnrichedRowType { - node_id: number; - identity_key: string; - bond: string; - host: string; - location: string; - owner: string; - version: string; -} - -function nodeEnrichedToGridRow(node: any): NodeEnrichedRowType { - return { - node_id: node.node_id, - owner: node.bond_information?.owner || "", - identity_key: node.bond_information?.node?.identity_key || "", - location: node.location?.country_name || "", - bond: node.bond_information?.original_pledge.amount || 0, // TODO: format - host: node.bond_information?.node?.host || "", - version: node.description?.build_information?.build_version || "", - }; -} - -/** - * Shows nym node details - */ -const PageNymNodeDetailsWithState = ({ - selectedNymNode, -}: { - selectedNymNode?: any; -}) => { - const { uptimeHistory } = useNymNodeContext(); - const enrichedData = React.useMemo( - () => (selectedNymNode ? [nodeEnrichedToGridRow(selectedNymNode)] : []), - [] - ); - - const hasVestingContractDelegations = React.useMemo( - () => selectedNymNode?.delegations?.filter((d: any) => d.proxy)?.length, - [selectedNymNode] - ); - - return ( - <Box component="main"> - <Title text="Nym Node Detail" /> - - <Grid container mt={4}> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Node detail table" - rows={enrichedData} - /> - </Grid> - </Grid> - - <Grid container mt={2} spacing={2}> - {selectedNymNode.rewarding_details && ( - <Grid item xs={12} md={4}> - <TableContainer component={Paper}> - <Table> - <TableBody> - <TableRow> - <TableCell colSpan={2}>Delegations and Rewards</TableCell> - </TableRow> - <TableRow> - <TableCell - component="th" - scope="row" - sx={{ color: "inherit" }} - > - <strong>Operator</strong> - </TableCell> - <TableCell align="right"> - {humanReadableCurrencyToString({ - amount: - selectedNymNode.rewarding_details.operator.split( - "." - )[0], - denom: "unym", - })} - </TableCell> - </TableRow> - <TableRow> - <TableCell - component="th" - scope="row" - sx={{ color: "inherit" }} - > - <strong> - {hasVestingContractDelegations ? ( - <VestingDelegationWarning plural={true}> - Delegates ( - { - selectedNymNode.rewarding_details - .unique_delegations - }{" "} - delegates) - </VestingDelegationWarning> - ) : ( - <> - Delegates ( - { - selectedNymNode.rewarding_details - .unique_delegations - }{" "} - delegates) - </> - )} - </strong> - </TableCell> - <TableCell align="right"> - {humanReadableCurrencyToString({ - amount: - selectedNymNode.rewarding_details.delegates.split( - "." - )[0], - denom: "unym", - })} - </TableCell> - </TableRow> - <TableRow> - <TableCell - component="th" - scope="row" - sx={{ color: "inherit" }} - > - <strong>Profit margin</strong> - </TableCell> - <TableCell align="right"> - {selectedNymNode.rewarding_details.cost_params - .profit_margin_percent * 100} - % - </TableCell> - </TableRow> - <TableRow> - <TableCell - component="th" - scope="row" - sx={{ color: "inherit" }} - > - <strong>Operator costs</strong> - </TableCell> - <TableCell align="right"> - {humanReadableCurrencyToString( - selectedNymNode.rewarding_details.cost_params - .interval_operating_cost - )} - </TableCell> - </TableRow> - </TableBody> - </Table> - </TableContainer> - </Grid> - )} - - {selectedNymNode.description?.declared_role && ( - <Grid item xs={12} md={4}> - <TableContainer component={Paper}> - <Table> - <TableBody> - <TableRow> - <TableCell colSpan={2}>Node roles</TableCell> - </TableRow> - <TableRow> - <TableCell>Self declared roles</TableCell> - <TableCell> - <DeclaredRole - declared_role={ - selectedNymNode.description?.declared_role - } - /> - </TableCell> - </TableRow> - </TableBody> - </Table> - </TableContainer> - </Grid> - )} - </Grid> - - <Grid container spacing={2} mt={2}> - <Grid item xs={12} md={8}> - {uptimeHistory && ( - <ContentCard title="Routing Score"> - {uptimeHistory.error && ( - <ComponentError text="There was a problem retrieving routing score." /> - )} - <UptimeChart - loading={uptimeHistory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeHistory} - /> - </ContentCard> - )} - </Grid> - </Grid> - - <Box mt={2}> - <NodeDelegationsTable node={selectedNymNode} /> - </Box> - </Box> - ); -}; - -/** - * Guard component to handle loading and not found states - */ -const PageNymNodeDetailGuard = () => { - const [selectedNode, setSelectedNode] = React.useState<any>(); - const [isLoading, setLoading] = React.useState<boolean>(true); - const [error, setError] = React.useState<string>(); - const { fetchNodeById } = useMainContext(); - const { id } = useParams(); - - React.useEffect(() => { - setSelectedNode(undefined); - setLoading(true); - (async () => { - if (typeof id === "string") { - try { - const res = await fetchNodeById(Number.parseInt(id)); - setSelectedNode(res); - } catch (e: any) { - setError(e.message); - } finally { - setLoading(false); - } - } - })(); - }, [id, fetchNodeById]); - - if (isLoading) { - return <CircularProgress />; - } - - // loaded, but not found - if (error) { - return ( - <Alert severity="warning"> - <AlertTitle>Nym node not found</AlertTitle> - Sorry, we could not find a node with id <code>{id || ""}</code> - </Alert> - ); - } - - if (!selectedNode) { - return ( - <Alert severity="warning"> - <AlertTitle>Legacy Node</AlertTitle> - Unable to load details for the selected Nym node. - </Alert> - ); - } - - return <PageNymNodeDetailsWithState selectedNymNode={selectedNode} />; -}; - -/** - * Wrapper component that adds the node content based on the `id` in the address URL - */ -const PageNymNodeDetail = () => { - const { id } = useParams(); - - if (!id || typeof id !== "string") { - return <Alert severity="error">Oh no! Could not find that node</Alert>; - } - - return ( - <NymNodeContextProvider nymNodeId={id}> - <PageNymNodeDetailGuard /> - </NymNodeContextProvider> - ); -}; - -export default PageNymNodeDetail; diff --git a/explorer-nextjs/app/network-components/nodes/page.tsx b/explorer-nextjs/app/network-components/nodes/page.tsx deleted file mode 100644 index 75e5b97fdd..0000000000 --- a/explorer-nextjs/app/network-components/nodes/page.tsx +++ /dev/null @@ -1,254 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { Box, Card, Grid, Stack, Chip } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' -import { diff, gte, rcompare } from 'semver' -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid' -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' -import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip' -import { useMainContext } from '@/app/context/main' -import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' -import { Title } from '@/app/components/Title' -import { humanReadableCurrencyToString } from '@/app/utils/currency' -import { Tooltip } from '@/app/components/Tooltip' -import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' -import { splice } from '@/app/utils' - -import StyledLink from '@/app/components/StyledLink' -import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole"; - -function getFlagEmoji(countryCode: string) { - const codePoints = countryCode - .toUpperCase() - .split('') - .map(char => 127397 + char.charCodeAt(0)); - return String.fromCodePoint(...codePoints); -} - -const PageNodes = () => { - const [isLoading, setLoading] = React.useState(true); - const { nodes, fetchNodes } = useMainContext() - - React.useEffect(() => { - (async () => { - try { - await fetchNodes(); - } finally { - setLoading(false); - } - })(); - }, []); - - const columns = useMemo<MRT_ColumnDef<any>[]>(() => { - return [ - { - id: 'nym-node-data', - header: 'Nym Node Data', - columns: [ - { - id: 'node_id', - header: 'Node Id', - accessorKey: 'node_id', - size: 75, - }, - { - id: 'identity_key', - header: 'Identity Key', - accessorKey: 'identity_key', - size: 250, - Cell: ({ row }) => { - return ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={row.original.bond_information.node.identity_key} - tooltip={`Copy identity key ${row.original.bond_information.node.identity_key} to clipboard`} - /> - <StyledLink - to={`/network-components/nodes/${row.original.node_id}`} - dataTestId="identity-link" - color="text.primary" - > - {splice(7, 29, row.original.bond_information.node.identity_key)} - </StyledLink> - </Stack> - ) - }, - }, - { - id: 'version', - header: 'Version', - accessorKey: 'description.build_information.build_version', - size: 75, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/nodes/${row.original.node_id}`} - data-testid="version" - color="text.primary" - > - {row.original.description?.build_information?.build_version || "-"} - </StyledLink> - ) - }, - }, - { - id: 'contract_node_type', - header: 'Kind', - accessorKey: 'contract_node_type', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/nodes/${row.original.node_id}`} - data-testid="contract_node_type" - color="text.primary" - > - <code>{row.original.contract_node_type || "-"}</code> - </StyledLink> - ) - }, - }, - { - id: 'declared_role', - header: 'Declare Role', - accessorKey: 'description.declared_role', - size: 250, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="declared_role-button" - > - <DeclaredRole declared_role={row.original.description?.declared_role}/> - </Box> - ) - }, - }, - { - id: 'total_stake', - header: 'Total Stake', - accessorKey: 'description.total_stake', - size: 250, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="total_stake-button" - > - {humanReadableCurrencyToString({ amount: row.original.total_stake || 0, denom: "unym" })} - </Box> - ) - }, - }, - { - id: 'location', - header: 'Location', - accessorKey: 'location.country_name', - size: 75, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip - text={row.original.location?.country_name || "-"} - id="nym-node-location-text" - > - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {row.original.location?.country_name ? <>{getFlagEmoji(row.original.location.two_letter_iso_country_code.toUpperCase())} {row.original.location.two_letter_iso_country_code}</> : <>-</> } - </Box> - </Tooltip> - </Box> - ) - }, - }, - { - id: 'host', - header: 'IP', - accessorKey: 'bond_information.node.host', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/nodes/${row.original.node_id}`} - data-testid="host" - color="text.primary" - > - {row.original.bond_information?.node?.host || "-"} - </StyledLink> - ) - }, - }, - { - id: 'owner', - header: 'Owner', - accessorKey: 'bond_information.owner', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.bond_information?.owner || "-"}`} - target="_blank" - data-testid="bond_information.node.owner" - color="text.primary" - > - {splice(7, 29, row.original.bond_information?.owner)} - </StyledLink> - ) - }, - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: nodes?.data || [], - state: { - isLoading, - showLoadingOverlay: isLoading, - }, - initialState: { - isLoading: true, - showLoadingOverlay: true, - } - }) - - return ( - <> - <Box mb={2}> - <Title text="Nym Nodes" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <MaterialReactTable table={table} /> - </Card> - </Grid> - </Grid> - </> - ) -} - -export default PageNodes diff --git a/explorer-nextjs/app/nodemap/page.tsx b/explorer-nextjs/app/nodemap/page.tsx deleted file mode 100644 index 674ae57a5e..0000000000 --- a/explorer-nextjs/app/nodemap/page.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { - Alert, - Box, - CircularProgress, - Grid, - SelectChangeEvent, - Typography, -} from '@mui/material' -import { ContentCard } from '@/app/components/ContentCard' -import { TableToolbar } from '@/app/components/TableToolbar' -import { Title } from '@/app/components/Title' -import { WorldMap } from '@/app/components/WorldMap' -import { useMainContext } from '@/app/context/main' -import { CountryDataRowType, countryDataToGridRow } from '@/app/utils' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' - -const PageMixnodesMap = () => { - const { countryData } = useMainContext() - - const data = useMemo(() => { - return countryDataToGridRow(Object.values(countryData?.data || {})) - }, [countryData]) - - const columns = useMemo<MRT_ColumnDef<CountryDataRowType>[]>(() => { - return [ - { - id: 'delegations-data', - header: 'Global Mixnodes Data', - columns: [ - { - id: 'country-name', - header: 'Location', - accessorKey: 'countryName', - }, - { - id: 'nodes', - header: 'Nodes', - accessorKey: 'nodes', - }, - { - id: 'percentage', - header: 'Percentage', - accessorKey: 'percentage', - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data, - }) - - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item data-testid="mixnodes-globe"> - <Title text="Mixnodes Around the Globe" /> - </Grid> - <Grid item> - <Grid container spacing={2}> - <Grid item xs={12}> - <ContentCard title="Distribution of nodes"> - <WorldMap loading={false} countryData={countryData} /> - <Box sx={{ marginTop: 2 }} /> - <TableToolbar /> - <MaterialReactTable table={table} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ) -} - -export default PageMixnodesMap diff --git a/explorer-nextjs/app/page.tsx b/explorer-nextjs/app/page.tsx deleted file mode 100644 index 2c6f9f781c..0000000000 --- a/explorer-nextjs/app/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client' - -import React, { useEffect } from 'react' -import { Box, Grid, Link, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import OpenInNewIcon from '@mui/icons-material/OpenInNew' -import { PeopleAlt } from '@mui/icons-material' -import { Title } from '@/app/components/Title' -import { StatsCard } from '@/app/components/StatsCard' -import { MixnodesSVG } from '@/app/icons/MixnodesSVG' -import { Icons } from '@/app/components/Icons' -import { GatewaysSVG } from '@/app/icons/GatewaysSVG' -import { ValidatorsSVG } from '@/app/icons/ValidatorsSVG' -import { ContentCard } from '@/app/components/ContentCard' -import { WorldMap } from '@/app/components/WorldMap' -import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' -import { formatNumber } from '@/app/utils' -import { useMainContext } from './context/main' -import { useRouter } from 'next/navigation' - -const PageOverview = () => { - const theme = useTheme() - const router = useRouter() - - const { - summaryOverview, - gateways, - validators, - block, - countryData, - serviceProviders, - } = useMainContext() - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item paddingBottom={3}> - <Title text="Overview" /> - </Grid> - <Grid item> - <Grid container spacing={3}> - {summaryOverview && ( - <> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Nodes" - icon={<MixnodesSVG />} - count={summaryOverview.data?.nymnodes?.count || ''} - errorMsg={summaryOverview?.error} - /> - </Grid> - </> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Mixnodes" - count={summaryOverview.data?.nymnodes?.roles?.mixnode || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Entry Gateways" - count={summaryOverview.data?.nymnodes?.roles?.entry || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Exit Gateways" - count={summaryOverview.data?.nymnodes?.roles?.exit_ipr || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="SOCKS5 Network Requesters" - count={summaryOverview.data?.nymnodes?.roles?.exit_nr || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {validators && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => window.open(`${BLOCK_EXPLORER_BASE_URL}/validators`)} - title="Validators" - count={validators?.data?.count || ''} - errorMsg={validators?.error} - icon={<ValidatorsSVG />} - /> - </Grid> - )} - {block?.data && ( - <Grid item xs={12}> - <Link - href={`${BLOCK_EXPLORER_BASE_URL}/blocks`} - target="_blank" - rel="noreferrer" - underline="none" - color="inherit" - marginY={2} - paddingX={3} - paddingY={0.25} - fontSize={14} - fontWeight={600} - display="flex" - alignItems="center" - > - <Typography fontWeight="inherit" fontSize="inherit"> - Current block height is {formatNumber(block.data)} - </Typography> - <OpenInNewIcon - fontWeight="inherit" - fontSize="inherit" - sx={{ ml: 0.5 }} - /> - </Link> - </Grid> - )} - <Grid item xs={12}> - <ContentCard title="Distribution of nodes around the world"> - <WorldMap loading={false} countryData={countryData} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ) -} - -export default PageOverview diff --git a/explorer-nextjs/app/providers/index.tsx b/explorer-nextjs/app/providers/index.tsx deleted file mode 100644 index 346d2fc88e..0000000000 --- a/explorer-nextjs/app/providers/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import CosmosKitProvider from '@/app/context/cosmos-kit' -import { WalletProvider } from '@/app/context/wallet' -import { NetworkExplorerThemeProvider } from '@/app/theme' -import { MainContextProvider } from '@/app/context/main' - -const Providers = ({ children }: { children: React.ReactNode }) => { - return ( - <MainContextProvider> - <NetworkExplorerThemeProvider> - <CosmosKitProvider> - <WalletProvider>{children}</WalletProvider> - </CosmosKitProvider> - </NetworkExplorerThemeProvider> - </MainContextProvider> - ) -} - -export { Providers } diff --git a/explorer-nextjs/app/theme/index.tsx b/explorer-nextjs/app/theme/index.tsx deleted file mode 100644 index fd072cfc64..0000000000 --- a/explorer-nextjs/app/theme/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import * as React from 'react' -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme' -import { useMainContext } from '../context/main' - -export const NetworkExplorerThemeProvider: FCWithChildren = ({ children }) => { - const { mode } = useMainContext() - - return ( - <NymNetworkExplorerThemeProvider mode={mode}> - {children} - </NymNetworkExplorerThemeProvider> - ) -} diff --git a/explorer-nextjs/app/theme/mui-theme.d.ts b/explorer-nextjs/app/theme/mui-theme.d.ts deleted file mode 100644 index 6d5f9aabf1..0000000000 --- a/explorer-nextjs/app/theme/mui-theme.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles'; -import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme'; - -/** - * If you are unfamiliar with Material UI theming, please read the following first: - * - https://mui.com/customization/theming/ - * - https://mui.com/customization/palette/ - * - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette - * - * This file adds typings to the theme using Typescript's module augmentation. - * - * Read the following if you are unfamiliar with module augmentation and declaration merging. Then - * look at the recommendations from Material UI docs for implementation: - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces - * - https://mui.com/customization/palette/#adding-new-colors - * - * - * IMPORTANT: - * - * The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than - * `type Foo = { ... }` - this is necessary to merge the definitions. - */ - -declare module '@mui/material/styles' { - /** - * This augments the definitions of the MUI Theme with the Nym theme, as well as - * a partial `ThemeOptions` type used by `createTheme` - * - * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below - */ - interface Theme extends NymTheme { } - interface ThemeOptions extends Partial<NymTheme> { } - interface Palette extends NymPaletteWithExtensions { } - interface PaletteOptions extends NymPaletteWithExtensionsOptions { } -} diff --git a/explorer-nextjs/app/typeDefs/explorer-api.ts b/explorer-nextjs/app/typeDefs/explorer-api.ts deleted file mode 100644 index b15490008d..0000000000 --- a/explorer-nextjs/app/typeDefs/explorer-api.ts +++ /dev/null @@ -1,294 +0,0 @@ -/* eslint-disable camelcase */ - -export interface ClientConfig { - url: string; - version: string; -} - -export interface SummaryOverviewResponse { - mixnodes: { - count: number; - activeset: { - active: number; - standby: number; - inactive: number; - }; - }; - gateways: { - count: number; - }; - validators: { - count: number; - }; - nymnodes: { - count: number; - roles: { - mixnode: number; - entry: number; - exit_nr: number; - exit_ipr: number; - }; - }; -} - -export interface MixNode { - host: string; - mix_port: number; - http_api_port: number; - verloc_port: number; - sphinx_key: string; - identity_key: string; - version: string; - location: string; -} - -export interface Gateway { - host: string; - mix_port: number; - clients_port: number; - location: string; - sphinx_key: string; - identity_key: string; - version: string; -} - -export interface Amount { - denom: string; - amount: number; -} - -export enum MixnodeStatus { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set -} - -export enum MixnodeStatusWithAll { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set - all = 'all', // any status -} - -export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | undefined => { - if (!status || status === MixnodeStatusWithAll.all) { - return undefined; - } - return status as unknown as MixnodeStatus; -}; - -export interface MixNodeResponseItem { - mix_id: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - layer: string; - status: MixnodeStatus; - location: { - country_name: string; - latitude?: number; - longitude?: number; - three_letter_iso_country_code: string; - two_letter_iso_country_code: string; - }; - mix_node: MixNode; - avg_uptime: number; - node_performance: NodePerformance; - stake_saturation: number; - uncapped_saturation: number; - operating_cost: Amount; - profit_margin_percent: string; - blacklisted: boolean; -} - -export type MixNodeResponse = MixNodeResponseItem[]; - -export interface MixNodeReportResponse { - identity: string; - owner: string; - most_recent_ipv4: boolean; - most_recent_ipv6: boolean; - last_hour_ipv4: number; - last_hour_ipv6: number; - last_day_ipv4: number; - last_day_ipv6: number; -} - -export interface StatsResponse { - update_time: Date; - previous_update_time: Date; - packets_received_since_startup: number; - packets_sent_since_startup: number; - packets_explicitly_dropped_since_startup: number; - packets_received_since_last_update: number; - packets_sent_since_last_update: number; - packets_explicitly_dropped_since_last_update: number; -} - -export interface NodePerformance { - most_recent: string; - last_hour: string; - last_24h: string; -} - -export type MixNodeHistoryResponse = StatsResponse; - -export interface GatewayBond { - block_height: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - gateway: Gateway; - node_performance: NodePerformance; - location?: Location; -} - -export interface GatewayBondAnnotated { - gateway_bond: GatewayBond; - node_performance: NodePerformance; -} - -export interface Location { - two_letter_iso_country_code: string; - three_letter_iso_country_code: string; - country_name: string; - latitude?: number; - longitude?: number; -} - -export interface LocatedGateway { - pledge_amount: Amount; - owner: string; - block_height: number; - gateway: Gateway; - proxy?: string; - location?: Location; -} - -export type GatewayResponse = LocatedGateway[]; - -export interface NymNodeReportResponse { - identity: string; - owner: string; - most_recent: number; - last_hour: number; - last_day: number; -} - -export interface GatewayReportResponse { - identity: string; - owner: string; - most_recent: number; - last_hour: number; - last_day: number; -} - -export type GatewayHistoryResponse = StatsResponse; - -export interface MixNodeDescriptionResponse { - name: string; - description: string; - link: string; - location: string; -} - -export type MixNodeStatsResponse = StatsResponse; - -export interface Validator { - address: string; - proposer_priority: string; - pub_key: { - type: string; - value: string; - }; -} -export interface ValidatorsResponse { - block_height: number; - count: string; - total: string; - validators: Validator[]; -} - -export type CountryData = { - ISO3: string; - nodes: number; -}; - -export type Delegation = { - owner: string; - amount: Amount; - block_height: number; -}; - -export type DelegationUniq = { - owner: string; - amount: Amount; -}; - -export type DelegationsResponse = Delegation[]; - -export type UniqDelegationsResponse = DelegationUniq[]; - -export interface CountryDataResponse { - [threeLetterCountryCode: string]: CountryData; -} - -export type BlockType = number; -export type BlockResponse = BlockType; - -export interface ApiState<RESPONSE> { - isLoading: boolean; - data?: RESPONSE; - error?: Error; -} - -export type StatusResponse = { - pending: boolean; - ports: { - 1789: boolean; - 1790: boolean; - 8000: boolean; - }; -}; - -export type UptimeTime = { - date: string; - uptime: number; -}; - -export type UptimeStoryResponse = { - history: UptimeTime[]; - identity: string; - owner: string; -}; - -export type MixNodeEconomicDynamicsStatsResponse = { - stake_saturation: number; - uncapped_saturation: number; - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - active_set_inclusion_probability: 'High' | 'Good' | 'Low'; - reserve_set_inclusion_probability: 'High' | 'Good' | 'Low'; - estimated_total_node_reward: number; - estimated_operator_reward: number; - estimated_delegators_reward: number; - current_interval_uptime: number; -}; - -export type Environment = 'mainnet' | 'sandbox' | 'qa'; - -export type ServiceProviderType = 'Network Requester'; - -export type DirectoryServiceProvider = { - id: string; - description: string; - address: string; - gateway: string; - routing_score: string | null; - service_type: ServiceProviderType; -}; - -export type DirectoryService = { - id: string; - description: string; - items: DirectoryServiceProvider[]; -}; diff --git a/explorer-nextjs/app/typeDefs/filters.ts b/explorer-nextjs/app/typeDefs/filters.ts deleted file mode 100644 index 53de60cb87..0000000000 --- a/explorer-nextjs/app/typeDefs/filters.ts +++ /dev/null @@ -1,22 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { Mark } from '@mui/base'; - -export enum EnumFilterKey { - profitMargin = 'profitMargin', - stakeSaturation = 'stakeSaturation', - routingScore = 'routingScore', -} - -export type TFilterItem = { - label: string; - id: EnumFilterKey; - value: number[]; - isSmooth?: boolean; - marks: Mark[]; - min?: number; - max?: number; - scale?: (value: number) => number; - tooltipInfo?: string; -}; - -export type TFilters = { [key in EnumFilterKey]: TFilterItem }; diff --git a/explorer-nextjs/app/typeDefs/network.ts b/explorer-nextjs/app/typeDefs/network.ts deleted file mode 100644 index f8615cd992..0000000000 --- a/explorer-nextjs/app/typeDefs/network.ts +++ /dev/null @@ -1 +0,0 @@ -export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; diff --git a/explorer-nextjs/app/typeDefs/tables.ts b/explorer-nextjs/app/typeDefs/tables.ts deleted file mode 100644 index 1ab2ffead4..0000000000 --- a/explorer-nextjs/app/typeDefs/tables.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type TableHeading = { - id: string; - numeric: boolean; - disablePadding: boolean; - label: string; -}; - -export type TableHeadingsType = TableHeading[]; diff --git a/explorer-nextjs/app/typings/FC.d.ts b/explorer-nextjs/app/typings/FC.d.ts deleted file mode 100644 index 08ebdfa298..0000000000 --- a/explorer-nextjs/app/typings/FC.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare type FCWithChildren<P = {}> = React.FC<React.PropsWithChildren<P>>; diff --git a/explorer-nextjs/app/typings/jpeg.d.ts b/explorer-nextjs/app/typings/jpeg.d.ts deleted file mode 100644 index af2ed72913..0000000000 --- a/explorer-nextjs/app/typings/jpeg.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module '*.jpeg' { - const value: any; - export default value; -} - -declare module '*.jpg' { - const value: any; - export default value; -} diff --git a/explorer-nextjs/app/typings/json.d.ts b/explorer-nextjs/app/typings/json.d.ts deleted file mode 100644 index b72dd46ee5..0000000000 --- a/explorer-nextjs/app/typings/json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.json' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/typings/png.d.ts b/explorer-nextjs/app/typings/png.d.ts deleted file mode 100644 index dd84df40a4..0000000000 --- a/explorer-nextjs/app/typings/png.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.png' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/typings/react-identicons.d.ts b/explorer-nextjs/app/typings/react-identicons.d.ts deleted file mode 100644 index 6c460eff99..0000000000 --- a/explorer-nextjs/app/typings/react-identicons.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare module 'react-identicons' { - import * as React from 'react'; - - interface IdenticonProps { - string: string; - size?: number; - padding?: number; - bg?: string; - fg?: string; - palette?: string[]; - count?: number; - // getColor: Function; - } - - declare function Identicon(props: IdenticonProps): React.ReactElement<IdenticonProps>; - - export default Identicon; -} diff --git a/explorer-nextjs/app/typings/react-tooltip.d.ts b/explorer-nextjs/app/typings/react-tooltip.d.ts deleted file mode 100644 index 98cb6d592d..0000000000 --- a/explorer-nextjs/app/typings/react-tooltip.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'react-tooltip'; diff --git a/explorer-nextjs/app/typings/svg.d.ts b/explorer-nextjs/app/typings/svg.d.ts deleted file mode 100644 index 091d25e210..0000000000 --- a/explorer-nextjs/app/typings/svg.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.svg' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/utils/currency.ts b/explorer-nextjs/app/utils/currency.ts deleted file mode 100644 index 07bdc890de..0000000000 --- a/explorer-nextjs/app/utils/currency.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { printableCoin } from '@nymproject/nym-validator-client'; -import Big from 'big.js'; -import { DecCoin, isValidRawCoin } from '@nymproject/types'; - -const DENOM = process.env.CURRENCY_DENOM || 'unym'; -const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx'; - -export const toDisplay = (val: string | number | Big, dp = 4) => { - let displayValue; - try { - displayValue = Big(val).toFixed(dp); - } catch (e: any) { - console.warn(`${displayValue} not a valid decimal number: ${e}`); - } - return displayValue; -}; - -export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string; dp?: number; denom?: string }) => { - if (!dp) { - printableCoin({ - amount, - denom, - }); - } - - const [printableAmount, printableDenom] = printableCoin({ - amount, - denom, - }).split(/\s+/); - - return `${toDisplay(printableAmount, dp)} ${printableDenom}`; -}; - -function addThousandsSeparator(value: string) { - return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); -} - -export const humanReadableCurrencyToString = ({ amount, denom }: { amount: string, denom: string }) => { - const str = currencyToString({ amount, denom, dp: 2 }); - const parts = str.split('.'); - return [addThousandsSeparator(parts[0]), parts[1]].join('.'); -} - -export const stakingCurrencyToString = (amount: string, denom: string = DENOM_STAKING) => - printableCoin({ - amount, - denom, - }); - -/** - * Converts a decimal number to a pretty representation - * with fixed decimal places. - * - * @param val - a decimal number of string form - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns A prettyfied decimal number - */ - -/** - * Converts a decimal number of μNYM (micro NYM) to NYM. - * - * @param unym - a decimal number of μNYM - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns The corresponding decimal number in NYM - */ -export const unymToNym = (unym: string | number | Big, dp = 4) => { - let nym; - try { - nym = Big(unym).div(1_000_000).toFixed(dp); - } catch (e: any) { - console.warn(`${unym} not a valid decimal number: ${e}`); - } - return nym; -}; - -export const validateAmount = async ( - majorAmountAsString: DecCoin['amount'], - minimumAmountAsString: DecCoin['amount'], -): Promise<boolean> => { - // tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc - if (!Number(majorAmountAsString)) { - return false; - } - - if (!isValidRawCoin(majorAmountAsString)) { - return false; - } - - const majorValueFloat = parseInt(majorAmountAsString, Number(10)); - - return majorValueFloat >= parseInt(minimumAmountAsString, Number(10)); -}; - -/** - * Takes a DecCoin and prettify its amount to a representation - * with fixed decimal places. - * - * @param coin - a DecCoin - * @param dp - number of decimal places to apply to amount (4 by default ie. 0.0000) - * @returns A DecCoin with prettified amount - */ -export const decCoinToDisplay = (coin: DecCoin, dp = 4) => { - const displayCoin = { ...coin }; - try { - displayCoin.amount = Big(coin.amount).toFixed(dp); - } catch (e: any) { - console.warn(`${coin.amount} not a valid decimal number: ${e}`); - } - return displayCoin; -}; diff --git a/explorer-nextjs/app/utils/index.ts b/explorer-nextjs/app/utils/index.ts deleted file mode 100644 index e3c3e4ae19..0000000000 --- a/explorer-nextjs/app/utils/index.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* eslint-disable camelcase */ -import { MutableRefObject } from 'react'; -import { Theme } from '@mui/material/styles'; -import { registerLocale, getName } from 'i18n-iso-countries'; -import Big from 'big.js'; -import { CountryData } from '@/app/typeDefs/explorer-api'; -import { EconomicsRowsType } from '@/app/components/MixNodes/Economics/types'; -import { Network } from '../typeDefs/network'; - -registerLocale(require('i18n-iso-countries/langs/en.json')); - -export function formatNumber(num: number): string { - return new Intl.NumberFormat().format(num); -} - -export function scrollToRef(ref: MutableRefObject<HTMLDivElement | undefined>): void { - if (ref?.current) ref.current.scrollIntoView(); -} - -export type CountryDataRowType = { - id: number; - ISO3: string; - nodes: number; - countryName: string; - percentage: string; -}; - -export function countryDataToGridRow(countriesData: CountryData[]): CountryDataRowType[] { - const totalNodes = countriesData.reduce((acc, obj) => acc + obj.nodes, 0); - const formatted = countriesData.map((each: CountryData, index: number) => { - const updatedCountryRecord: CountryDataRowType = { - ...each, - id: index, - countryName: getName(each.ISO3, 'en', { select: 'alias' }), - percentage: ((each.nodes * 100) / totalNodes).toFixed(1), - }; - return updatedCountryRecord; - }); - - const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1)); - return sorted; -} - -export const splice = (start: number, deleteCount: number, address?: string): string => { - if (address) { - const array = address.split(''); - array.splice(start, deleteCount, '...'); - return array.join(''); - } - return ''; -}; - -export const trimAddress = (address = '', trimBy = 6) => `${address.slice(0, trimBy)}...${address.slice(-trimBy)}`; - -/** - * Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100). - * - * @param value - the percentage to convert - * @returns A stringified integer - */ -export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); -export const toPercentInteger = (value: string) => Math.round(Number(value) * 100); - -export 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; -}; - -export const isGreaterThan = (a: number, b: number) => a > b; - -export const isLessThan = (a: number, b: number) => a < b; - -/** - * - * Checks if the user's balance is enough to pay the fee - * @param balance - The user's current balance - * @param fee - The fee for the tx - * @param tx - The amount of the tx - * @returns boolean - * - */ - -export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => { - console.log('balance', balance, fee, tx); - try { - return Big(balance).gte(Big(fee).plus(Big(tx))); - } catch (e) { - console.log(e); - return false; - } -}; - -export const urls = (networkName?: Network) => - networkName === 'MAINNET' - ? { - mixnetExplorer: 'https://mixnet.explorers.guru/', - blockExplorer: 'https://blocks.nymtech.net', - networkExplorer: 'https://explorer.nymtech.net', - } - : { - blockExplorer: `https://${networkName}-blocks.nymtech.net`, - networkExplorer: `https://${networkName}-explorer.nymtech.net`, - }; diff --git a/explorer-nextjs/next.config.mjs b/explorer-nextjs/next.config.mjs deleted file mode 100644 index d642290145..0000000000 --- a/explorer-nextjs/next.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - async redirects() { - return [ - { - source: '/network-components/mixnode/:id', // Match the old URL - destination: '/network-components/nodes/:id', // Redirect to the new URL - permanent: true, - }, - ]; - }, -}; - -export default nextConfig; diff --git a/explorer-nextjs/package.json b/explorer-nextjs/package.json deleted file mode 100644 index 555749547b..0000000000 --- a/explorer-nextjs/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@nymproject/network-explorer", - "version": "1.0.0", - "scripts": { - "dev": "next dev", - "build": "next build", - "build:prod": "yarn --cwd .. build && next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@nymproject/react": "^1.0.0", - "@nymproject/nym-validator-client": "0.18.0", - "next": "14.2.26", - "react": "^18", - "react-dom": "^18", - "react-error-boundary": "^4.0.13", - "material-react-table": "^2.12.1", - "@mui/x-date-pickers": "7.1.1", - "@mui/x-data-grid": "7.1.1", - "@mui/x-charts": "^7.22.3" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "eslint": "^8", - "eslint-config-next": "14.1.4", - "typescript": "^5" - } -} diff --git a/explorer-nextjs/public/favicon.ico b/explorer-nextjs/public/favicon.ico deleted file mode 100644 index bc951a1c6f..0000000000 Binary files a/explorer-nextjs/public/favicon.ico and /dev/null differ diff --git a/explorer-nextjs/public/next.svg b/explorer-nextjs/public/next.svg deleted file mode 100644 index 5174b28c56..0000000000 --- a/explorer-nextjs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/explorer-nextjs/public/vercel.svg b/explorer-nextjs/public/vercel.svg deleted file mode 100644 index d2f8422273..0000000000 --- a/explorer-nextjs/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg> \ No newline at end of file diff --git a/explorer-nextjs/tsconfig.json b/explorer-nextjs/tsconfig.json deleted file mode 100644 index 701d0e6984..0000000000 --- a/explorer-nextjs/tsconfig.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "CommonJS", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": [ - "./*" - ], - "@assets/*": [ - "../assets/*" - ] - }, - "moduleResolution": "node", - "target": "ES2017" - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/explorer/.babelrc b/explorer/.babelrc deleted file mode 100644 index 85e1f1ba26..0000000000 --- a/explorer/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/env", "@babel/react"] -} \ No newline at end of file diff --git a/explorer/.editorconfig b/explorer/.editorconfig deleted file mode 100644 index c35daeee9b..0000000000 --- a/explorer/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# See http://editorconfig.org/ -# EditorConfig helps developers define and maintain consistent coding styles -# between different editors and IDEs. The EditorConfig project consists of a -# file format for defining coding styles and a collection of text editor plugins -# that enable editors to read the file format and adhere to defined styles. -# EditorConfig files are easily readable and they work nicely with version -# control systems. - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/explorer/.env.dev b/explorer/.env.dev deleted file mode 100644 index 3cc86ae00c..0000000000 --- a/explorer/.env.dev +++ /dev/null @@ -1,9 +0,0 @@ -# When running the explorer API locally -#EXPLORER_API_URL=http://localhost:8000/v1 - -EXPLORER_API_URL=https://sandbox-explorer.nymtech.net/api/v1 -NYM_API_URL=https://sandbox-nym-api1.nymtech.net -VALIDATOR_URL=https://rpc.sandbox.nymtech.net -BIG_DIPPER_URL=https://sandbox-blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/.env.prod b/explorer/.env.prod deleted file mode 100644 index 2e927b2947..0000000000 --- a/explorer/.env.prod +++ /dev/null @@ -1,6 +0,0 @@ -EXPLORER_API_URL=https://explorer.nymtech.net/api/v1 -NYM_API_URL=https://validator.nymtech.net -VALIDATOR_URL=https://rpc.nymtech.net -BIG_DIPPER_URL=https://blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx \ No newline at end of file diff --git a/explorer/.env.qa b/explorer/.env.qa deleted file mode 100644 index dd7052ce4d..0000000000 --- a/explorer/.env.qa +++ /dev/null @@ -1,6 +0,0 @@ -EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1 -NYM_API_URL=https://qa-validator-api.nymtech.net -VALIDATOR_URL=https://qa-validator.nymtech.net -BIG_DIPPER_URL=https://qa-blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/.eslintrc.js b/explorer/.eslintrc.js deleted file mode 100644 index 5feba99084..0000000000 --- a/explorer/.eslintrc.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - extends: [ - '@nymproject/eslint-config-react-typescript' - ], - overrides: [ - { - files: ['*.ts'], - parserOptions: { - project: 'tsconfig.json', - tsconfigRootDir: __dirname, - } - } - ] -} diff --git a/explorer/.gitignore b/explorer/.gitignore deleted file mode 100644 index b8b1827577..0000000000 --- a/explorer/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -dist -.DS_Store -detailAPI.md -!.env.dev diff --git a/explorer/.nvmrc b/explorer/.nvmrc deleted file mode 100644 index 3c032078a4..0000000000 --- a/explorer/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -18 diff --git a/explorer/.prettierrc b/explorer/.prettierrc deleted file mode 100644 index ccf75b89de..0000000000 --- a/explorer/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "trailingComma": "all", - "singleQuote": true, - "printWidth": 120, - "tabWidth": 2 -} diff --git a/explorer/.storybook/main.js b/explorer/.storybook/main.js deleted file mode 100644 index 1c2a975321..0000000000 --- a/explorer/.storybook/main.js +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable no-param-reassign */ -const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); -const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); - -module.exports = { - stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: '@storybook/react', - core: { - builder: 'webpack5', - }, - // webpackFinal: async (config, { configType }) => { - // // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' - // // You can change the configuration based on that. - // // 'PRODUCTION' is used when building the static version of storybook. - webpackFinal: async (config) => { - config.module.rules.forEach((rule) => { - // look for SVG import rule and replace - // NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/ - if (rule.test?.toString().includes('svg')) { - rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/; - } - }); - - // handle asset loading with this - config.module.rules.unshift({ - test: /\.svg(\?.*)?$/i, - issuer: /\.[jt]sx?$/, - use: ['@svgr/webpack'], - }); - - config.resolve.extensions = ['.tsx', '.ts', '.js']; - config.resolve.plugins = [new TsconfigPathsPlugin()]; - - config.resolve.fallback = { - fs: false, - tls: false, - path: false, - http: false, - https: false, - stream: false, - crypto: false, - net: false, - zlib: false, - }; - - config.plugins.push(new ForkTsCheckerWebpackPlugin({ - typescript: { - mode: 'write-references', - diagnosticOptions: { - semantic: true, - syntactic: true, - }, - }, - })); - - // Return the altered config - return config; - }, - features: { - emotionAlias: false, - }, -}; diff --git a/explorer/.storybook/preview.js b/explorer/.storybook/preview.js deleted file mode 100644 index 5d97f3b5ab..0000000000 --- a/explorer/.storybook/preview.js +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable react/react-in-jsx-scope */ -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme'; -import { Box } from '@mui/material'; - -export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -} - -const withThemeProvider = (Story, context) => ( - <div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}> - <div> - <NymNetworkExplorerThemeProvider mode="light"> - <Box - p={4} - sx={{ - display: 'grid', - gridTemplateRows: '80vh 2rem', - background: (theme) => theme.palette.background.default, - color: (theme) => theme.palette.text.primary, - }} - > - <Box sx={{ overflowY: 'auto' }}> - <Story {...context} /> - </Box> - <h4 style={{ textAlign: 'center' }}>Light mode</h4> - </Box> - </NymNetworkExplorerThemeProvider> - </div> - <div> - <NymNetworkExplorerThemeProvider mode="dark"> - <Box - p={4} - sx={{ - display: 'grid', - gridTemplateRows: '80vh 2rem', - background: (theme) => theme.palette.background.default, - color: (theme) => theme.palette.text.primary, - }} - > - <Box sx={{ overflowY: 'auto' }}> - <Story {...context} /> - </Box> - <h4 style={{ textAlign: 'center' }}>Dark mode</h4> - </Box> - </NymNetworkExplorerThemeProvider> - </div> - </div> -); - -export const decorators = [withThemeProvider]; diff --git a/explorer/.vscode/settings.json b/explorer/.vscode/settings.json deleted file mode 100644 index 246e9c6e89..0000000000 --- a/explorer/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - } -} diff --git a/explorer/CHANGELOG.md b/explorer/CHANGELOG.md deleted file mode 100644 index 8446a3c5b2..0000000000 --- a/explorer/CHANGELOG.md +++ /dev/null @@ -1,47 +0,0 @@ -## UNRELEASED - -- nothing yet - -## [nym-explorer-v1.0.6](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.6) (2023-02-21) - -- Allow user to filter gateways but current and old versions -- Add copy buttons to gateway identity keys - -## [nym-explorer-v1.0.5](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.5) (2023-02-14) - -- NE - link `Owner` field on the node detail page to the account details on NG explorer ([#2923]) -- NE - Upgrade Sandbox and make below changes: ([#2332]) - -[#2923]: https://github.com/nymtech/nym/issues/2923 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.4](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.4) (2023-01-31) - -- Add routing score on gateway list ([#2913]) -- Add gateway's last Routing Score to the gateways list page ([#2186]) -- Upgrade Sandbox and make below changes: ([#2332]) - -[#2913]: https://github.com/nymtech/nym/pull/2913 -[#2186]: https://github.com/nymtech/nym/issues/2186 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.3](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.3) (2023-01-24) - -- Stake Saturation tooltip on node list and node pages updated ([#2877]) -- Sandbox Upgrade : Name change from "Network Explorer" to "Sandbox Explorer", button to mainnet explorer and link to faucet ([#2332)]) - -[#2877]: https://github.com/nymtech/nym/issues/2877 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.2](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.2) (2023-01-17) - -- changing the explorers guru link ([#2820]) - -[#2820]: https://github.com/nymtech/nym/pull/2820 - -## [nym-explorer-v1.0.1](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.1) (2023-01-10) - -- Feat/2161 ne gate version by @gala1234 in https://github.com/nymtech/nym/pull/2743 -- fix(explorer): set gateway bond 6 decimals by @doums in https://github.com/nymtech/nym/pull/2741 -- Feat/2130 tables update rebase by @gala1234 in https://github.com/nymtech/nym/pull/2742 -- fix(explorer,explorer-api): mixnode location by @doums in https://github.com/nymtech/nym-api diff --git a/explorer/README.md b/explorer/README.md deleted file mode 100644 index a7bbc2346a..0000000000 --- a/explorer/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Nym Network Explorer - -The network explorer lets you explore the Nym network. - -## Getting started - -You will need: - -- NodeJS (use `nvm install` to automatically install the correct version) -- `npm` -- `yarn` - -> **Note**: This project ipart of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them. - -From the [root of the repository](../README.md) run the following to build shared packages: - -``` -yarn -yarn build -``` - -From the `explorer` directory of the `nym` monorepo, run: - -``` -cd explorer -yarn start -``` - -You can then open a browser to http://localhost:3000 and start development. - -## Development - -Documentation for developers [can be found here](./docs). - -Several environment variables are required. They can be -provisioned via a `.env` file. For convenience a `.env.dev` is -provided, just copy its content into `.env`. - -#### Required env vars - -``` -EXPLORER_API_URL -NYM_API_URL -VALIDATOR_URL -BIG_DIPPER_URL -CURRENCY_DENOM -CURRENCY_STAKING_DENOM -``` - -## Deployment - -Build the UI with (starting in the repository root): - -``` -yarn -yarn build -cd explorer -yarn build -``` - -The output will be in the `dist` directory. Serve this with `nginx` or `httpd`. - -Make sure you have built the [explorer-api](./explorer-api) and are running it as a service proxied through -`nginx` or `httpd` so that both the UI and API are running on the same host. - -## License - -Please see the [project README](./README.md) and the [LICENSES directory](../LICENSES) for license details for all Nym software. - -## Contributing - -If you would like to contribute to the Network Explorer send us a PR or -[raise an issue on GitHub](https://github.com/nymtech/nym/issues) and tag them with `network-explorer`. - -## Development - -Please see [development docs](./docs) here for more information on the structure and design of this app. diff --git a/explorer/docs/README.md b/explorer/docs/README.md deleted file mode 100644 index bb0d0be99a..0000000000 --- a/explorer/docs/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Nym Network Explorer - Development Docs - -## Getting started - -You will need: - -- NodeJS -- `nvm` - -We use the following: - -- Typescript -- `eslint` -- `webpack` -- `jest` -- `react-material-ui` -- `react` 17 - -## Development mode - -Copy the `.env.prod` file to `.env` to configure your environment. Using the live sandbox Explorer API is the best way to do development, so the prod settings are good. - -Run the following: - -``` -npm install -npm run start -``` - -A development server with hot reloading will be running on http://localhost:3000. - -## Linting - -`eslint` and `prettier` are configured. - -You can lint the code by running: - -``` -npm run lint -``` - -> **Note:** this will only show linting errors and will not fix them - -To fix all linting errors automatically run: - -``` -npm run lint:fix -``` diff --git a/explorer/jest.config.js b/explorer/jest.config.js deleted file mode 100644 index e86e13bab9..0000000000 --- a/explorer/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; diff --git a/explorer/package.json b/explorer/package.json deleted file mode 100644 index d2426f6122..0000000000 --- a/explorer/package.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "name": "@nym/network-explorer", - "version": "1.0.7", - "private": true, - "license": "Apache-2.0", - "scripts": { - "start": "webpack serve --progress --port 3000", - "build": "webpack build --progress --config webpack.prod.js", - "ci:dev": "echo 'Building CI for branch preview...' && yarn --cwd .. build && cp .env.dev .env && yarn build", - "ci:prod": "echo 'Building CI for prod preview...' && yarn --cwd .. build && cp .env.prod .env && yarn build", - "ci": "[ \"$CI_PROD\" = true ] && yarn ci:prod || yarn ci:dev", - "build:serve": "npx serve dist", - "test": "jest", - "test:watch": "jest --watch", - "tsc": "tsc --noEmit true", - "tsc:watch": "tsc --watch --noEmit true", - "lint": "eslint src", - "lint:fix": "eslint src --fix", - "prestorybook": "yarn --cwd .. build", - "storybook": "start-storybook -p 6006", - "storybook:build": "build-storybook" - }, - "dependencies": { - "@chain-registry/types": "^0.18.0", - "@cosmjs/cosmwasm-stargate": "^0.32.0", - "@cosmjs/crypto": "^0.32.0", - "@cosmjs/encoding": "^0.32.0", - "@cosmjs/math": "^0.26.2", - "@cosmjs/stargate": "^0.32.0", - "@cosmjs/tendermint-rpc": "^0.32.0", - "@cosmos-kit/core": "^2.8.9", - "@cosmos-kit/keplr-extension": "^2.7.9", - "@cosmos-kit/react": "^2.10.11", - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "@interchain-ui/react": "^1.14.2", - "@mui/icons-material": "^5.0.0", - "@mui/material": "^5.0.1", - "@mui/styles": "^5.0.1", - "@mui/system": "^5.0.1", - "@mui/x-data-grid": "^5.0.0-beta.5", - "@nymproject/contract-clients": "1.2.4-rc.1", - "@nymproject/mui-theme": "^1.0.0", - "@nymproject/node-tester": "^1.0.0", - "@nymproject/nym-validator-client": "^0.18.0", - "@nymproject/react": "^1.0.0", - "@nymproject/types": "^1.0.0", - "@tauri-apps/api": "^1.5.1", - "big.js": "^6.2.1", - "bs58": "^5.0.0", - "buffer": "^6.0.3", - "chain-registry": "^1.29.1", - "cosmjs-types": "^0.9.0", - "d3-scale": "^4.0.0", - "date-fns": "^2.24.0", - "i18n-iso-countries": "^6.8.0", - "lodash": "^4.17.21", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^3.1.4", - "react-google-charts": "^3.0.15", - "react-identicons": "^1.2.5", - "react-router-dom": "6", - "react-simple-maps": "^2.3.0", - "react-tooltip": "^4.2.21", - "semver": "^7.3.8", - "use-clipboard-copy": "^0.2.0" - }, - "devDependencies": { - "@babel/core": "^7.15.0", - "@nymproject/eslint-config-react-typescript": "^1.0.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", - "@storybook/addon-actions": "^6.5.8", - "@storybook/addon-essentials": "^6.5.8", - "@storybook/addon-interactions": "^6.5.8", - "@storybook/addon-links": "^6.5.8", - "@storybook/react": "^6.5.15", - "@storybook/testing-library": "^0.0.9", - "@svgr/webpack": "^6.1.1", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^12.0.0", - "@types/d3-fetch": "^3.0.1", - "@types/d3-geo": "^3.0.2", - "@types/d3-scale": "^4.0.1", - "@types/geojson": "^7946.0.8", - "@types/jest": "^27.0.1", - "@types/node": "^16.7.13", - "@types/react": "^18.0.26", - "@types/react-dom": "^18.0.10", - "@types/react-simple-maps": "^1.0.6", - "@types/react-tooltip": "^4.2.4", - "@types/topojson-client": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.13.0", - "babel-loader": "^8.3.0", - "babel-plugin-root-import": "^6.6.0", - "clean-webpack-plugin": "^4.0.0", - "css-loader": "^6.7.3", - "css-minimizer-webpack-plugin": "^3.0.2", - "dotenv-webpack": "^7.0.3", - "eslint": "^8.10.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^16.1.0", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-root-import": "^1.0.4", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jest": "^26.1.1", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.2", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-storybook": "^0.5.12", - "favicons-webpack-plugin": "^5.0.2", - "file-loader": "^6.2.0", - "fork-ts-checker-webpack-plugin": "^7.2.1", - "html-webpack-plugin": "^5.3.2", - "jest": "^27.1.0", - "mini-css-extract-plugin": "^2.2.2", - "prettier": "^2.8.7", - "react-refresh": "^0.10.0", - "react-refresh-typescript": "^2.0.2", - "style-loader": "^3.3.1", - "ts-jest": "^27.0.5", - "ts-loader": "^9.4.2", - "tsconfig-paths-webpack-plugin": "^3.5.2", - "typescript": "^4.6.2", - "url-loader": "^4.1.1", - "webpack": "^5.75.0", - "webpack-cli": "^4.8.0", - "webpack-dev-server": "^4.5.0", - "webpack-favicons": "^1.3.8", - "webpack-merge": "^5.8.0" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx deleted file mode 100644 index 4e6fcc41e8..0000000000 --- a/explorer/src/App.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { Nav } from './components/Nav'; -import { MobileNav } from './components/MobileNav'; -import { Routes } from './routes/index'; -import { useIsMobile } from './hooks/useIsMobile'; - -export const App: FCWithChildren = () => { - const isMobile = useIsMobile(); - - if (isMobile) { - return ( - <MobileNav> - <Routes /> - </MobileNav> - ); - } - return ( - <Nav> - <Routes /> - </Nav> - ); -}; diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts deleted file mode 100644 index fbb8c18d6b..0000000000 --- a/explorer/src/api/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -// master APIs -export const API_BASE_URL = process.env.EXPLORER_API_URL; -export const NYM_API_BASE_URL = process.env.NYM_API_URL; -export const VALIDATOR_BASE_URL = process.env.VALIDATOR_URL || 'https://rpc.nymtech.net'; -export const BIG_DIPPER = process.env.BIG_DIPPER_URL; - -// specific API routes -export const OVERVIEW_API = `${API_BASE_URL}/overview`; -export const MIXNODE_PING = `${API_BASE_URL}/ping`; -export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; -export const MIXNODE_API = `${API_BASE_URL}/mix-node`; -export const GATEWAYS_EXPLORER_API = `${API_BASE_URL}/gateways`; -export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/status/gateways/detailed`; -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 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."; - -export const NYM_WEBSITE = 'https://nymtech.net'; - -export const NYM_BIG_DIPPER = 'https://mixnet.explorers.guru'; - -export const NYM_MIXNET_CONTRACT = - process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr'; -export const COSMOS_KIT_USE_CHAIN = process.env.COSMOS_KIT_USE_CHAIN || 'sandbox'; -export const WALLET_CONNECT_PROJECT_ID = process.env.WALLET_CONNECT_PROJECT_ID || ''; diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts deleted file mode 100644 index a2b134d4ae..0000000000 --- a/explorer/src/api/index.ts +++ /dev/null @@ -1,173 +0,0 @@ -import keyBy from 'lodash/keyBy'; -import { - API_BASE_URL, - BLOCK_API, - COUNTRY_DATA_API, - GATEWAYS_API, - UPTIME_STORY_API_GATEWAY, - MIXNODE_API, - MIXNODE_PING, - MIXNODES_API, - OVERVIEW_API, - UPTIME_STORY_API, - VALIDATORS_API, - SERVICE_PROVIDERS, - GATEWAYS_EXPLORER_API, -} from './constants'; - -import { - CountryDataResponse, - DelegationsResponse, - UniqDelegationsResponse, - GatewayReportResponse, - UptimeStoryResponse, - MixNodeDescriptionResponse, - MixNodeResponse, - MixNodeResponseItem, - MixnodeStatus, - MixNodeEconomicDynamicsStatsResponse, - StatsResponse, - StatusResponse, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - GatewayBondAnnotated, - GatewayBond, - DirectoryServiceProvider, - LocatedGateway, -} from '../typeDefs/explorer-api'; - -function getFromCache(key: string) { - const ts = Number(localStorage.getItem('ts')); - const hasExpired = Date.now() - ts > 5000; - const curr = localStorage.getItem(key); - if (curr && !hasExpired) { - return JSON.parse(curr); - } - return undefined; -} - -function storeInCache(key: string, data: any) { - localStorage.setItem(key, data); - localStorage.setItem('ts', Date.now().toString()); -} - -export class Api { - static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => { - const cache = getFromCache('overview-summary'); - if (cache) { - return cache; - } - const res = await fetch(`${OVERVIEW_API}/summary`); - const json = await res.json(); - storeInCache('overview-summary', JSON.stringify(json)); - return json; - }; - - static fetchMixnodes = async (): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache('mixnodes'); - if (cachedMixnodes) { - return cachedMixnodes; - } - - const res = await fetch(MIXNODES_API); - const json = await res.json(); - storeInCache('mixnodes', JSON.stringify(json)); - return json; - }; - - static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache(`mixnodes-${status}`); - if (cachedMixnodes) { - return cachedMixnodes; - } - const res = await fetch(`${MIXNODES_API}/active-set/${status}`); - const json = await res.json(); - storeInCache(`mixnodes-${status}`, JSON.stringify(json)); - return json; - }; - - static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => { - const response = await fetch(`${MIXNODE_API}/${id}`); - - // when the mixnode is not found, returned undefined - if (response.status === 404) { - return undefined; - } - - return response.json(); - }; - - static fetchGateways = async (): Promise<GatewayBond[]> => { - const res = await fetch(GATEWAYS_API); - const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); - const res2 = await fetch(GATEWAYS_EXPLORER_API); - const locatedGateways: LocatedGateway[] = await res2.json(); - const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); - return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ - ...gateway_bond, - node_performance, - location: locatedGatewaysByOwner[gateway_bond.owner]?.location, - })); - }; - - static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json(); - - static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json(); - - static fetchValidators = async (): Promise<ValidatorsResponse> => { - const res = await fetch(VALIDATORS_API); - const json = await res.json(); - return json.result; - }; - - static fetchBlock = async (): Promise<number> => { - const res = await fetch(BLOCK_API); - const json = await res.json(); - const { height } = json.result.block.header; - return height; - }; - - static fetchCountryData = async (): Promise<CountryDataResponse> => { - const result: CountryDataResponse = {}; - const res = await fetch(COUNTRY_DATA_API); - const json = await res.json(); - Object.keys(json).forEach((ISO3) => { - result[ISO3] = { ISO3, nodes: json[ISO3] }; - }); - return result; - }; - - static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations`)).json(); - - static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json(); - - static fetchStatsById = async (id: string): Promise<StatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/stats`)).json(); - - static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> => - (await fetch(`${MIXNODE_API}/${id}/description`)).json(); - - static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json(); - - static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json(); - - static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API}/${id}/history`)).json(); - - static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => { - const res = await fetch(SERVICE_PROVIDERS); - const json = await res.json(); - return json; - }; -} - -export const getEnvironment = (): Environment => { - const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env; - return matchEnv('sandbox') || matchEnv('qa') || 'mainnet'; -}; diff --git a/explorer/src/assets/world-110m.json b/explorer/src/assets/world-110m.json deleted file mode 100644 index 5c999c82ca..0000000000 --- a/explorer/src/assets/world-110m.json +++ /dev/null @@ -1,39718 +0,0 @@ -{ - "type": "Topology", - "arcs": [ - [ - [ - 16814, - 15074 - ], - [ - 71, - -45 - ], - [ - 53, - 16 - ], - [ - 15, - 54 - ], - [ - 55, - 18 - ], - [ - 39, - 37 - ], - [ - 14, - 95 - ], - [ - 59, - 24 - ], - [ - 11, - 42 - ], - [ - 32, - -32 - ], - [ - 21, - -3 - ] - ], - [ - [ - 17184, - 15280 - ], - [ - 39, - -1 - ], - [ - 53, - -26 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 21, - -14 - ], - [ - 51, - 38 - ], - [ - 23, - -23 - ], - [ - 23, - 55 - ], - [ - 41, - -2 - ], - [ - 11, - 17 - ], - [ - 7, - 49 - ], - [ - 30, - 41 - ], - [ - 38, - -27 - ], - [ - -8, - -37 - ], - [ - 22, - -5 - ], - [ - -7, - -101 - ], - [ - 28, - -39 - ], - [ - 24, - 25 - ], - [ - 31, - 12 - ], - [ - 43, - 53 - ], - [ - 48, - -8 - ], - [ - 72, - -1 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - 13, - -34 - ] - ], - [ - [ - 17787, - 15252 - ], - [ - -41, - -13 - ], - [ - -35, - -23 - ], - [ - -80, - -14 - ], - [ - -75, - -25 - ], - [ - -41, - -52 - ], - [ - 17, - -51 - ], - [ - 8, - -60 - ], - [ - -35, - -50 - ], - [ - 3, - -46 - ], - [ - -19, - -43 - ], - [ - -67, - 4 - ], - [ - 28, - -80 - ], - [ - -45, - -30 - ], - [ - -29, - -73 - ], - [ - 4, - -72 - ], - [ - -28, - -33 - ], - [ - -26, - 11 - ], - [ - -53, - -16 - ], - [ - -7, - -33 - ], - [ - -52, - 0 - ], - [ - -39, - -68 - ], - [ - -3, - -102 - ], - [ - -90, - -50 - ], - [ - -49, - 10 - ], - [ - -14, - -26 - ], - [ - -42, - 15 - ], - [ - -69, - -17 - ], - [ - -117, - 61 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 63, - 109 - ], - [ - -6, - 77 - ], - [ - -52, - 20 - ], - [ - -6, - 76 - ], - [ - -23, - 96 - ], - [ - 30, - 66 - ], - [ - -30, - 17 - ], - [ - 19, - 88 - ], - [ - 28, - 149 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - -128, - -49 - ], - [ - -169, - 17 - ], - [ - -48, - 58 - ], - [ - -283, - -6 - ], - [ - -11, - -8 - ], - [ - -41, - 54 - ], - [ - -45, - 4 - ], - [ - -42, - -21 - ], - [ - -34, - -22 - ] - ], - [ - [ - 13365, - 8722 - ], - [ - -6, - 75 - ], - [ - 10, - 105 - ], - [ - 24, - 110 - ], - [ - 3, - 52 - ], - [ - 23, - 108 - ], - [ - 16, - 49 - ], - [ - 41, - 79 - ], - [ - 22, - 53 - ], - [ - 7, - 89 - ], - [ - -3, - 68 - ], - [ - -21, - 43 - ], - [ - -19, - 72 - ], - [ - -17, - 72 - ], - [ - 4, - 25 - ], - [ - 21, - 48 - ], - [ - -21, - 116 - ], - [ - -14, - 80 - ], - [ - -35, - 76 - ], - [ - 6, - 23 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - 29, - 16 - ], - [ - 20, - -2 - ], - [ - 25, - 15 - ], - [ - 206, - -2 - ], - [ - 17, - -89 - ], - [ - 20, - -72 - ], - [ - 16, - -39 - ], - [ - 27, - -63 - ], - [ - 46, - 10 - ], - [ - 23, - 17 - ], - [ - 38, - -17 - ], - [ - 11, - 30 - ], - [ - 17, - 70 - ], - [ - 43, - 4 - ], - [ - 4, - 21 - ], - [ - 36, - 1 - ], - [ - -6, - -44 - ], - [ - 84, - 2 - ], - [ - 1, - -76 - ], - [ - 15, - -46 - ], - [ - -11, - -73 - ], - [ - 5, - -73 - ], - [ - 24, - -45 - ], - [ - -4, - -143 - ], - [ - 17, - 11 - ], - [ - 30, - -3 - ], - [ - 44, - 18 - ], - [ - 31, - -7 - ] - ], - [ - [ - 14214, - 9486 - ], - [ - 8, - -37 - ], - [ - -8, - -58 - ], - [ - 12, - -56 - ], - [ - -10, - -45 - ], - [ - 6, - -42 - ], - [ - -146, - 2 - ], - [ - -3, - -382 - ], - [ - 47, - -98 - ], - [ - 46, - -75 - ] - ], - [ - [ - 13397, - 10103 - ], - [ - -19, - 90 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - 28, - 52 - ], - [ - 21, - 20 - ], - [ - 26, - -41 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - -25, - -26 - ], - [ - -11, - -30 - ], - [ - -3, - -53 - ], - [ - -17, - -12 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - -2, - -31 - ], - [ - -22, - -18 - ], - [ - -4, - -39 - ], - [ - -33, - -58 - ] - ], - [ - [ - 13952, - 15551 - ], - [ - -12, - 8 - ], - [ - -1, - 27 - ], - [ - -39, - 40 - ], - [ - -6, - 57 - ], - [ - 6, - 82 - ], - [ - 10, - 37 - ], - [ - -12, - 19 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -5, - 38 - ], - [ - 30, - 59 - ], - [ - 5, - -22 - ], - [ - 19, - 11 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 14, - -33 - ], - [ - 17, - -12 - ], - [ - 5, - -43 - ] - ], - [ - [ - 13983, - 15819 - ], - [ - -9, - -41 - ], - [ - 10, - -52 - ], - [ - 29, - -29 - ] - ], - [ - [ - 16143, - 13706 - ], - [ - 12, - 6 - ], - [ - 3, - -33 - ], - [ - 55, - 19 - ], - [ - 57, - -3 - ], - [ - 42, - -4 - ], - [ - 48, - 81 - ], - [ - 52, - 77 - ], - [ - 44, - 74 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 13, - -41 - ] - ], - [ - [ - 16469, - 13882 - ], - [ - 10, - -95 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - -36, - 0 - ], - [ - -5, - -78 - ], - [ - 12, - -17 - ], - [ - -32, - -24 - ], - [ - 0, - -49 - ], - [ - -20, - -49 - ], - [ - -2, - -49 - ] - ], - [ - [ - 16396, - 13521 - ], - [ - -14, - -25 - ], - [ - -210, - 61 - ], - [ - -26, - 121 - ], - [ - -3, - 28 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -42, - 4 - ], - [ - -75, - 0 - ], - [ - 0, - 267 - ] - ], - [ - [ - 7763, - 4482 - ], - [ - 27, - -55 - ], - [ - 35, - -90 - ], - [ - 90, - -72 - ], - [ - 98, - -30 - ], - [ - -31, - -60 - ], - [ - -67, - -6 - ], - [ - -35, - 42 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -64, - 19 - ], - [ - -169, - 16 - ], - [ - -28, - 70 - ], - [ - 1, - 90 - ], - [ - -47, - -8 - ], - [ - -24, - 43 - ], - [ - -6, - 128 - ], - [ - 53, - 52 - ], - [ - 22, - 76 - ], - [ - -8, - 61 - ], - [ - 37, - 102 - ], - [ - 26, - 159 - ], - [ - -8, - 71 - ], - [ - 31, - 22 - ], - [ - -8, - 46 - ], - [ - -32, - 24 - ], - [ - 23, - 50 - ], - [ - -32, - 46 - ], - [ - -16, - 138 - ], - [ - 28, - 24 - ], - [ - -12, - 147 - ], - [ - 17, - 122 - ], - [ - 18, - 107 - ], - [ - 42, - 44 - ], - [ - -21, - 117 - ], - [ - 0, - 110 - ], - [ - 52, - 79 - ], - [ - -1, - 100 - ], - [ - 40, - 117 - ], - [ - 0, - 110 - ], - [ - -18, - 22 - ], - [ - -32, - 207 - ], - [ - 43, - 124 - ], - [ - -7, - 116 - ], - [ - 25, - 109 - ], - [ - 46, - 113 - ], - [ - 49, - 74 - ], - [ - -21, - 47 - ], - [ - 14, - 39 - ], - [ - -2, - 200 - ], - [ - 76, - 59 - ], - [ - 24, - 125 - ], - [ - -8, - 30 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - 58, - 108 - ], - [ - 91, - -29 - ], - [ - 41, - -87 - ], - [ - 27, - 97 - ], - [ - 80, - -5 - ], - [ - 11, - -26 - ] - ], - [ - [ - 8178, - 8128 - ], - [ - 128, - -196 - ], - [ - 57, - -18 - ], - [ - 85, - -89 - ], - [ - 72, - -47 - ], - [ - 10, - -52 - ], - [ - -69, - -183 - ], - [ - 71, - -32 - ], - [ - 78, - -19 - ], - [ - 55, - 20 - ], - [ - 63, - 91 - ], - [ - 12, - 106 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 34, - 23 - ], - [ - 35, - -69 - ], - [ - -1, - -96 - ], - [ - -59, - -66 - ], - [ - -47, - -49 - ], - [ - -78, - -116 - ], - [ - -93, - -164 - ] - ], - [ - [ - 8531, - 7172 - ], - [ - -18, - -96 - ], - [ - -19, - -123 - ], - [ - 1, - -120 - ], - [ - -15, - -26 - ], - [ - -5, - -78 - ] - ], - [ - [ - 8475, - 6729 - ], - [ - -5, - -63 - ], - [ - 88, - -102 - ], - [ - -9, - -83 - ], - [ - 43, - -52 - ], - [ - -3, - -59 - ], - [ - -67, - -154 - ], - [ - -103, - -64 - ], - [ - -140, - -25 - ], - [ - -77, - 12 - ], - [ - 15, - -71 - ], - [ - -14, - -90 - ], - [ - 12, - -61 - ], - [ - -41, - -42 - ], - [ - -72, - -17 - ], - [ - -67, - 44 - ], - [ - -27, - -31 - ], - [ - 10, - -119 - ], - [ - 47, - -37 - ], - [ - 38, - 38 - ], - [ - 21, - -62 - ], - [ - -64, - -37 - ], - [ - -56, - -75 - ], - [ - -10, - -121 - ], - [ - -17, - -64 - ], - [ - -66, - 0 - ], - [ - -54, - -62 - ], - [ - -20, - -90 - ], - [ - 68, - -87 - ], - [ - 67, - -25 - ], - [ - -24, - -107 - ], - [ - -83, - -68 - ], - [ - -45, - -141 - ], - [ - -63, - -47 - ], - [ - -29, - -56 - ], - [ - 22, - -125 - ], - [ - 47, - -69 - ], - [ - -30, - 6 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - 96, - 19 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 15, - -32 - ], - [ - 26, - -21 - ], - [ - -14, - -30 - ], - [ - 38, - -41 - ], - [ - -20, - -38 - ], - [ - 29, - -33 - ], - [ - 32, - -19 - ], - [ - 1, - -84 - ] - ], - [ - [ - 15789, - 15448 - ], - [ - -25, - -3 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -28, - 69 - ], - [ - 0, - 19 - ], - [ - -31, - 0 - ], - [ - -20, - 32 - ], - [ - -15, - -3 - ] - ], - [ - [ - 15670, - 15562 - ], - [ - -27, - 35 - ], - [ - -52, - 29 - ], - [ - 6, - 59 - ], - [ - -11, - 42 - ] - ], - [ - [ - 8395, - 1195 - ], - [ - -21, - -61 - ], - [ - -20, - -54 - ], - [ - -146, - 16 - ], - [ - -156, - -7 - ], - [ - -87, - 40 - ], - [ - 0, - 5 - ], - [ - -38, - 35 - ], - [ - 157, - -5 - ], - [ - 150, - -11 - ], - [ - 52, - 49 - ], - [ - 36, - 42 - ], - [ - 73, - -49 - ] - ], - [ - [ - 1449, - 1260 - ], - [ - -133, - -16 - ], - [ - -92, - 42 - ], - [ - -41, - 42 - ], - [ - -3, - 7 - ], - [ - -45, - 33 - ], - [ - 43, - 45 - ], - [ - 129, - -19 - ], - [ - 70, - -38 - ], - [ - 53, - -42 - ], - [ - 19, - -54 - ] - ], - [ - [ - 9400, - 1434 - ], - [ - 86, - -52 - ], - [ - 30, - -73 - ], - [ - 8, - -51 - ], - [ - 3, - -61 - ], - [ - -108, - -38 - ], - [ - -113, - -31 - ], - [ - -131, - -28 - ], - [ - -147, - -23 - ], - [ - -165, - 7 - ], - [ - -91, - 40 - ], - [ - 12, - 49 - ], - [ - 149, - 33 - ], - [ - 60, - 40 - ], - [ - 44, - 52 - ], - [ - 31, - 44 - ], - [ - 42, - 43 - ], - [ - 45, - 49 - ], - [ - 36, - 0 - ], - [ - 104, - 26 - ], - [ - 105, - -26 - ] - ], - [ - [ - 4098, - 1979 - ], - [ - 90, - -18 - ], - [ - 83, - 21 - ], - [ - -39, - -43 - ], - [ - -66, - -30 - ], - [ - -97, - 9 - ], - [ - -69, - 43 - ], - [ - 15, - 40 - ], - [ - 83, - -22 - ] - ], - [ - [ - 3795, - 1982 - ], - [ - 106, - -47 - ], - [ - -41, - 4 - ], - [ - -90, - 12 - ], - [ - -95, - 33 - ], - [ - 50, - 26 - ], - [ - 70, - -28 - ] - ], - [ - [ - 5648, - 2167 - ], - [ - 76, - -16 - ], - [ - 77, - 14 - ], - [ - 41, - -68 - ], - [ - -55, - 9 - ], - [ - -85, - -4 - ], - [ - -86, - 4 - ], - [ - -94, - -7 - ], - [ - -71, - 24 - ], - [ - -37, - 49 - ], - [ - 44, - 21 - ], - [ - 89, - -16 - ], - [ - 101, - -10 - ] - ], - [ - [ - 7776, - 2285 - ], - [ - 8, - -54 - ], - [ - -12, - -47 - ], - [ - -19, - -45 - ], - [ - -82, - -16 - ], - [ - -78, - -24 - ], - [ - -92, - 2 - ], - [ - 35, - 47 - ], - [ - -82, - -16 - ], - [ - -78, - -17 - ], - [ - -53, - 36 - ], - [ - -5, - 49 - ], - [ - 77, - 47 - ], - [ - 48, - 14 - ], - [ - 80, - -5 - ], - [ - 21, - 62 - ], - [ - 4, - 44 - ], - [ - -2, - 97 - ], - [ - 40, - 56 - ], - [ - 64, - 19 - ], - [ - 37, - -45 - ], - [ - 17, - -44 - ], - [ - 30, - -55 - ], - [ - 23, - -51 - ], - [ - 19, - -54 - ] - ], - [ - [ - 8462, - 3101 - ], - [ - -30, - -26 - ], - [ - -52, - 19 - ], - [ - -58, - -12 - ], - [ - -47, - -28 - ], - [ - -51, - -31 - ], - [ - -34, - -35 - ], - [ - -10, - -47 - ], - [ - 4, - -45 - ], - [ - 33, - -40 - ], - [ - -48, - -28 - ], - [ - -65, - -9 - ], - [ - -38, - -40 - ], - [ - -41, - -38 - ], - [ - -44, - -51 - ], - [ - -11, - -45 - ], - [ - 25, - -50 - ], - [ - 37, - -37 - ], - [ - 57, - -28 - ], - [ - 53, - -38 - ], - [ - 29, - -47 - ], - [ - 15, - -45 - ], - [ - 20, - -47 - ], - [ - 33, - -40 - ], - [ - 21, - -44 - ], - [ - 9, - -111 - ], - [ - 21, - -44 - ], - [ - 5, - -47 - ], - [ - 22, - -47 - ], - [ - -10, - -64 - ], - [ - -38, - -49 - ], - [ - -41, - -40 - ], - [ - -93, - -17 - ], - [ - -31, - -42 - ], - [ - -42, - -40 - ], - [ - -106, - -45 - ], - [ - -92, - -18 - ], - [ - -88, - -26 - ], - [ - -94, - -26 - ], - [ - -56, - -50 - ], - [ - -112, - -4 - ], - [ - -123, - 4 - ], - [ - -110, - -9 - ], - [ - -118, - 0 - ], - [ - 22, - -47 - ], - [ - 107, - -21 - ], - [ - 77, - -33 - ], - [ - 44, - -42 - ], - [ - -78, - -38 - ], - [ - -120, - 12 - ], - [ - -100, - -31 - ], - [ - -4, - -49 - ], - [ - -2, - -47 - ], - [ - 82, - -40 - ], - [ - 15, - -45 - ], - [ - 88, - -44 - ], - [ - 148, - -19 - ], - [ - 125, - -33 - ], - [ - 100, - -38 - ], - [ - 127, - -37 - ], - [ - 173, - -19 - ], - [ - 171, - -33 - ], - [ - 119, - -35 - ], - [ - 130, - -40 - ], - [ - 68, - -57 - ], - [ - 34, - -44 - ], - [ - 85, - 42 - ], - [ - 114, - 35 - ], - [ - 122, - 38 - ], - [ - 144, - 30 - ], - [ - 125, - 33 - ], - [ - 173, - 3 - ], - [ - 171, - -17 - ], - [ - 140, - -28 - ], - [ - 45, - 52 - ], - [ - 97, - 35 - ], - [ - 177, - 2 - ], - [ - 137, - 26 - ], - [ - 131, - 26 - ], - [ - 145, - 16 - ], - [ - 154, - 22 - ], - [ - 108, - 30 - ], - [ - -49, - 42 - ], - [ - -30, - 43 - ], - [ - 0, - 44 - ], - [ - -135, - -4 - ], - [ - -143, - -19 - ], - [ - -137, - 0 - ], - [ - -19, - 45 - ], - [ - 10, - 89 - ], - [ - 31, - 26 - ], - [ - 100, - 28 - ], - [ - 117, - 28 - ], - [ - 85, - 35 - ], - [ - 84, - 36 - ], - [ - 63, - 47 - ], - [ - 96, - 21 - ], - [ - 94, - 16 - ], - [ - 48, - 10 - ], - [ - 108, - 4 - ], - [ - 102, - 17 - ], - [ - 86, - 23 - ], - [ - 85, - 29 - ], - [ - 76, - 28 - ], - [ - 97, - 37 - ], - [ - 61, - 40 - ], - [ - 66, - 36 - ], - [ - 20, - 47 - ], - [ - -73, - 28 - ], - [ - 24, - 49 - ], - [ - 47, - 38 - ], - [ - 72, - 23 - ], - [ - 77, - 29 - ], - [ - 71, - 37 - ], - [ - 54, - 47 - ], - [ - 34, - 57 - ], - [ - 51, - 33 - ], - [ - 83, - -7 - ], - [ - 34, - -40 - ], - [ - 83, - -5 - ], - [ - 3, - 45 - ], - [ - 36, - 47 - ], - [ - 75, - -12 - ], - [ - 18, - -45 - ], - [ - 83, - -7 - ], - [ - 90, - 21 - ], - [ - 87, - 14 - ], - [ - 80, - -7 - ], - [ - 30, - -49 - ], - [ - 76, - 40 - ], - [ - 71, - 21 - ], - [ - 79, - 16 - ], - [ - 78, - 17 - ], - [ - 71, - 28 - ], - [ - 78, - 19 - ], - [ - 60, - 26 - ], - [ - 42, - 42 - ], - [ - 52, - -30 - ], - [ - 72, - 16 - ], - [ - 51, - -56 - ], - [ - 40, - -43 - ], - [ - 79, - 24 - ], - [ - 31, - 47 - ], - [ - 71, - 33 - ], - [ - 92, - -7 - ], - [ - 27, - -45 - ], - [ - 57, - 45 - ], - [ - 75, - 14 - ], - [ - 82, - 4 - ], - [ - 74, - -2 - ], - [ - 78, - -14 - ], - [ - 75, - -7 - ], - [ - 33, - -40 - ], - [ - 45, - -35 - ], - [ - 76, - 21 - ], - [ - 82, - 5 - ], - [ - 79, - 0 - ], - [ - 78, - 2 - ], - [ - 70, - 16 - ], - [ - 74, - 15 - ], - [ - 61, - 32 - ], - [ - 65, - 22 - ], - [ - 71, - 11 - ], - [ - 54, - 33 - ], - [ - 38, - 66 - ], - [ - 40, - 40 - ], - [ - 72, - -19 - ], - [ - 27, - -42 - ], - [ - 60, - -28 - ], - [ - 73, - 9 - ], - [ - 49, - -42 - ], - [ - 52, - -31 - ], - [ - 71, - 28 - ], - [ - 24, - 52 - ], - [ - 63, - 21 - ], - [ - 72, - 40 - ], - [ - 69, - 17 - ], - [ - 82, - 23 - ], - [ - 54, - 26 - ], - [ - 58, - 28 - ], - [ - 54, - 26 - ], - [ - 66, - -14 - ], - [ - 63, - 42 - ], - [ - 45, - 33 - ], - [ - 65, - -2 - ], - [ - 57, - 28 - ], - [ - 14, - 42 - ], - [ - 59, - 33 - ], - [ - 57, - 24 - ], - [ - 70, - 19 - ], - [ - 64, - 9 - ], - [ - 61, - -7 - ], - [ - 66, - -12 - ], - [ - 56, - -33 - ], - [ - 7, - -51 - ], - [ - 61, - -40 - ], - [ - 42, - -33 - ], - [ - 84, - -14 - ], - [ - 46, - -33 - ], - [ - 58, - -33 - ], - [ - 66, - -7 - ], - [ - 56, - 23 - ], - [ - 60, - 50 - ], - [ - 66, - -26 - ], - [ - 68, - -14 - ], - [ - 66, - -14 - ], - [ - 68, - -10 - ], - [ - 70, - 0 - ], - [ - 57, - -124 - ], - [ - -3, - -31 - ], - [ - -8, - -54 - ], - [ - -67, - -31 - ], - [ - -54, - -44 - ], - [ - 9, - -47 - ], - [ - 78, - 2 - ], - [ - -10, - -47 - ], - [ - -35, - -45 - ], - [ - -33, - -49 - ], - [ - 53, - -38 - ], - [ - 81, - -11 - ], - [ - 81, - 21 - ], - [ - 38, - 47 - ], - [ - 23, - 45 - ], - [ - 38, - 37 - ], - [ - 44, - 35 - ], - [ - 18, - 43 - ], - [ - 36, - 58 - ], - [ - 44, - 12 - ], - [ - 79, - 5 - ], - [ - 70, - 14 - ], - [ - 71, - 19 - ], - [ - 34, - 47 - ], - [ - 21, - 45 - ], - [ - 47, - 44 - ], - [ - 69, - 31 - ], - [ - 58, - 23 - ], - [ - 39, - 40 - ], - [ - 39, - 21 - ], - [ - 51, - 19 - ], - [ - 69, - -12 - ], - [ - 63, - 12 - ], - [ - 68, - 14 - ], - [ - 77, - -7 - ], - [ - 50, - 33 - ], - [ - 36, - 80 - ], - [ - 26, - -33 - ], - [ - 33, - -56 - ], - [ - 58, - -24 - ], - [ - 67, - -9 - ], - [ - 67, - 14 - ], - [ - 71, - -9 - ], - [ - 66, - -3 - ], - [ - 43, - 12 - ], - [ - 59, - -7 - ], - [ - 53, - -26 - ], - [ - 63, - 16 - ], - [ - 75, - 0 - ], - [ - 64, - 17 - ], - [ - 73, - -17 - ], - [ - 46, - 40 - ], - [ - 36, - 40 - ], - [ - 47, - 33 - ], - [ - 88, - 90 - ], - [ - 45, - -17 - ], - [ - 53, - -33 - ], - [ - 46, - -42 - ], - [ - 89, - -73 - ], - [ - 69, - -2 - ], - [ - 64, - 0 - ], - [ - 75, - 14 - ], - [ - 75, - 16 - ], - [ - 57, - 33 - ], - [ - 48, - 35 - ], - [ - 78, - 5 - ], - [ - 52, - 26 - ], - [ - 54, - -23 - ], - [ - 36, - -38 - ], - [ - 49, - -38 - ], - [ - 76, - 5 - ], - [ - 48, - -31 - ], - [ - 83, - -30 - ], - [ - 88, - -12 - ], - [ - 72, - 10 - ], - [ - 55, - 37 - ], - [ - 46, - 38 - ], - [ - 63, - 9 - ], - [ - 63, - -16 - ], - [ - 72, - -12 - ], - [ - 66, - 19 - ], - [ - 63, - 0 - ], - [ - 61, - -12 - ], - [ - 64, - -12 - ], - [ - 63, - 21 - ], - [ - 75, - 19 - ], - [ - 71, - 5 - ], - [ - 79, - 0 - ], - [ - 64, - 12 - ], - [ - 63, - 9 - ], - [ - 19, - 59 - ], - [ - 3, - 49 - ], - [ - 44, - -33 - ], - [ - 12, - -54 - ], - [ - 23, - -49 - ], - [ - 29, - -40 - ], - [ - 59, - -21 - ], - [ - 79, - 7 - ], - [ - 91, - 2 - ], - [ - 63, - 7 - ], - [ - 92, - 0 - ], - [ - 65, - 3 - ], - [ - 92, - -5 - ], - [ - 77, - -10 - ], - [ - 50, - -37 - ], - [ - -14, - -45 - ], - [ - 45, - -35 - ], - [ - 75, - -28 - ], - [ - 78, - -31 - ], - [ - 90, - -21 - ], - [ - 94, - -19 - ], - [ - 71, - -19 - ], - [ - 79, - -2 - ], - [ - 45, - 40 - ], - [ - 62, - -33 - ], - [ - 53, - -38 - ], - [ - 62, - -28 - ], - [ - 84, - -12 - ], - [ - 81, - -14 - ], - [ - 34, - -47 - ], - [ - 79, - -28 - ], - [ - 53, - -42 - ], - [ - 78, - -19 - ], - [ - 81, - 2 - ], - [ - 75, - -7 - ], - [ - 83, - 3 - ], - [ - 83, - -10 - ], - [ - 78, - -16 - ], - [ - 73, - -28 - ], - [ - 72, - -24 - ], - [ - 49, - -35 - ], - [ - -8, - -47 - ], - [ - -37, - -42 - ], - [ - -31, - -55 - ], - [ - -25, - -42 - ], - [ - -33, - -49 - ], - [ - -91, - -19 - ], - [ - -41, - -42 - ], - [ - -90, - -26 - ], - [ - -32, - -47 - ], - [ - -47, - -45 - ], - [ - -51, - -38 - ], - [ - -29, - -49 - ], - [ - -17, - -45 - ], - [ - -7, - -54 - ], - [ - 1, - -44 - ], - [ - 40, - -47 - ], - [ - 15, - -45 - ], - [ - 32, - -42 - ], - [ - 130, - -17 - ], - [ - 27, - -51 - ], - [ - -125, - -19 - ], - [ - -107, - -26 - ], - [ - -132, - -5 - ], - [ - -59, - -68 - ], - [ - -12, - -56 - ], - [ - -30, - -45 - ], - [ - -37, - -45 - ], - [ - 93, - -40 - ], - [ - 35, - -49 - ], - [ - 60, - -45 - ], - [ - 85, - -40 - ], - [ - 97, - -37 - ], - [ - 105, - -38 - ], - [ - 160, - -38 - ], - [ - 35, - -58 - ], - [ - 201, - -26 - ], - [ - 14, - -9 - ], - [ - 52, - -36 - ], - [ - 192, - 31 - ], - [ - 160, - -38 - ], - [ - 120, - -29 - ], - [ - 0, - -634 - ], - [ - -25095, - 0 - ], - [ - 0, - 634 - ], - [ - 4, - -1 - ], - [ - 62, - 70 - ], - [ - 125, - -38 - ], - [ - 8, - 5 - ], - [ - 74, - 38 - ], - [ - 10, - -1 - ], - [ - 8, - -1 - ], - [ - 101, - -50 - ], - [ - 88, - 50 - ], - [ - 16, - 6 - ], - [ - 204, - 22 - ], - [ - 67, - -28 - ], - [ - 33, - -15 - ], - [ - 105, - -40 - ], - [ - 198, - -30 - ], - [ - 157, - -38 - ], - [ - 269, - -28 - ], - [ - 200, - 33 - ], - [ - 297, - -24 - ], - [ - 168, - -37 - ], - [ - 184, - 35 - ], - [ - 194, - 33 - ], - [ - 15, - 56 - ], - [ - -275, - 5 - ], - [ - -225, - 28 - ], - [ - -59, - 47 - ], - [ - -187, - 26 - ], - [ - 13, - 54 - ], - [ - 25, - 50 - ], - [ - 26, - 44 - ], - [ - -13, - 50 - ], - [ - -116, - 33 - ], - [ - -54, - 42 - ], - [ - -107, - 37 - ], - [ - 169, - -7 - ], - [ - 161, - 19 - ], - [ - 101, - -40 - ], - [ - 124, - 36 - ], - [ - 115, - 44 - ], - [ - 56, - 40 - ], - [ - -25, - 50 - ], - [ - -90, - 32 - ], - [ - -102, - 36 - ], - [ - -143, - 7 - ], - [ - -126, - 16 - ], - [ - -135, - 12 - ], - [ - -45, - 45 - ], - [ - -90, - 37 - ], - [ - -55, - 43 - ], - [ - -22, - 136 - ], - [ - 34, - -12 - ], - [ - 63, - -37 - ], - [ - 115, - 11 - ], - [ - 110, - 17 - ], - [ - 58, - -52 - ], - [ - 110, - 12 - ], - [ - 93, - 26 - ], - [ - 87, - 33 - ], - [ - 80, - 39 - ], - [ - 105, - 12 - ], - [ - -3, - 45 - ], - [ - -24, - 45 - ], - [ - 20, - 42 - ], - [ - 90, - 21 - ], - [ - 41, - -40 - ], - [ - 107, - 24 - ], - [ - 80, - 30 - ], - [ - 100, - 3 - ], - [ - 94, - 11 - ], - [ - 94, - 28 - ], - [ - 75, - 26 - ], - [ - 85, - 26 - ], - [ - 55, - -7 - ], - [ - 47, - -9 - ], - [ - 104, - 16 - ], - [ - 93, - -21 - ], - [ - 95, - 2 - ], - [ - 92, - 17 - ], - [ - 94, - -12 - ], - [ - 104, - -12 - ], - [ - 97, - 5 - ], - [ - 101, - -2 - ], - [ - 104, - -3 - ], - [ - 95, - 5 - ], - [ - 71, - 35 - ], - [ - 85, - 19 - ], - [ - 87, - -26 - ], - [ - 84, - 21 - ], - [ - 75, - 43 - ], - [ - 45, - -38 - ], - [ - 24, - -42 - ], - [ - 45, - -40 - ], - [ - 73, - 35 - ], - [ - 83, - -45 - ], - [ - 94, - -14 - ], - [ - 81, - -33 - ], - [ - 98, - 7 - ], - [ - 89, - 22 - ], - [ - 105, - -5 - ], - [ - 94, - -17 - ], - [ - 96, - -21 - ], - [ - 37, - 52 - ], - [ - -46, - 40 - ], - [ - -34, - 42 - ], - [ - -90, - 10 - ], - [ - -39, - 44 - ], - [ - -15, - 45 - ], - [ - -25, - 89 - ], - [ - 53, - -16 - ], - [ - 92, - -7 - ], - [ - 90, - 7 - ], - [ - 82, - -19 - ], - [ - 71, - -35 - ], - [ - 30, - -42 - ], - [ - 94, - -8 - ], - [ - 90, - 17 - ], - [ - 96, - 23 - ], - [ - 86, - 15 - ], - [ - 71, - -29 - ], - [ - 93, - 10 - ], - [ - 60, - 91 - ], - [ - 56, - -54 - ], - [ - 80, - -21 - ], - [ - 88, - 12 - ], - [ - 57, - -47 - ], - [ - 91, - -5 - ], - [ - 85, - -14 - ], - [ - 83, - -26 - ], - [ - 55, - 45 - ], - [ - 27, - 42 - ], - [ - 70, - -47 - ], - [ - 95, - 12 - ], - [ - 71, - -26 - ], - [ - 48, - -40 - ], - [ - 93, - 12 - ], - [ - 72, - 26 - ], - [ - 71, - 30 - ], - [ - 85, - 17 - ], - [ - 98, - 14 - ], - [ - 89, - 16 - ], - [ - 68, - 26 - ], - [ - 41, - 38 - ], - [ - 17, - 52 - ], - [ - -8, - 49 - ], - [ - -22, - 47 - ], - [ - -25, - 47 - ], - [ - -22, - 47 - ], - [ - -18, - 42 - ], - [ - -4, - 47 - ], - [ - 7, - 47 - ], - [ - 33, - 45 - ], - [ - 27, - 49 - ], - [ - 11, - 47 - ], - [ - -13, - 52 - ], - [ - -9, - 47 - ], - [ - 35, - 54 - ], - [ - 38, - 35 - ], - [ - 45, - 45 - ], - [ - 48, - 38 - ], - [ - 56, - 35 - ], - [ - 27, - 52 - ], - [ - 38, - 33 - ], - [ - 44, - 30 - ], - [ - 67, - 7 - ], - [ - 43, - 38 - ], - [ - 50, - 23 - ], - [ - 57, - 14 - ], - [ - 50, - 31 - ], - [ - 40, - 38 - ], - [ - 55, - 14 - ], - [ - 41, - -31 - ], - [ - -26, - -40 - ], - [ - -71, - -35 - ] - ], - [ - [ - 17353, - 4964 - ], - [ - 45, - -38 - ], - [ - 66, - -15 - ], - [ - 2, - -23 - ], - [ - -19, - -54 - ], - [ - -107, - -8 - ], - [ - -2, - 64 - ], - [ - 10, - 49 - ], - [ - 5, - 25 - ] - ], - [ - [ - 22683, - 5903 - ], - [ - 67, - -41 - ], - [ - 38, - 16 - ], - [ - 55, - 23 - ], - [ - 41, - -8 - ], - [ - 5, - -142 - ], - [ - -23, - -41 - ], - [ - -8, - -97 - ], - [ - -24, - 33 - ], - [ - -48, - -84 - ], - [ - -15, - 7 - ], - [ - -43, - 4 - ], - [ - -43, - 102 - ], - [ - -9, - 79 - ], - [ - -40, - 105 - ], - [ - 1, - 55 - ], - [ - 46, - -11 - ] - ], - [ - [ - 22555, - 9146 - ], - [ - 25, - -94 - ], - [ - 45, - 45 - ], - [ - 23, - -51 - ], - [ - 33, - -47 - ], - [ - -7, - -53 - ], - [ - 15, - -103 - ], - [ - 11, - -59 - ], - [ - 17, - -15 - ], - [ - 19, - -103 - ], - [ - -7, - -62 - ], - [ - 23, - -81 - ], - [ - 75, - -63 - ], - [ - 50, - -57 - ], - [ - 46, - -52 - ], - [ - -9, - -29 - ], - [ - 40, - -75 - ], - [ - 27, - -130 - ], - [ - 28, - 26 - ], - [ - 28, - -52 - ], - [ - 17, - 19 - ], - [ - 12, - -128 - ], - [ - 50, - -73 - ], - [ - 32, - -46 - ], - [ - 55, - -97 - ], - [ - 19, - -97 - ], - [ - 2, - -68 - ], - [ - -5, - -74 - ], - [ - 34, - -102 - ], - [ - -4, - -106 - ], - [ - -12, - -56 - ], - [ - -19, - -107 - ], - [ - 1, - -69 - ], - [ - -14, - -86 - ], - [ - -30, - -109 - ], - [ - -52, - -59 - ], - [ - -26, - -93 - ], - [ - -23, - -59 - ], - [ - -20, - -104 - ], - [ - -27, - -59 - ], - [ - -18, - -90 - ], - [ - -9, - -83 - ], - [ - 4, - -38 - ], - [ - -40, - -41 - ], - [ - -78, - -5 - ], - [ - -65, - -49 - ], - [ - -32, - -46 - ], - [ - -42, - -52 - ], - [ - -58, - 53 - ], - [ - -42, - 21 - ], - [ - 10, - 63 - ], - [ - -38, - -23 - ], - [ - -61, - -87 - ], - [ - -60, - 33 - ], - [ - -39, - 19 - ], - [ - -40, - 8 - ], - [ - -68, - 35 - ], - [ - -45, - 74 - ], - [ - -13, - 91 - ], - [ - -16, - 61 - ], - [ - -34, - 48 - ], - [ - -67, - 15 - ], - [ - 23, - 58 - ], - [ - -17, - 89 - ], - [ - -34, - -83 - ], - [ - -62, - -22 - ], - [ - 36, - 66 - ], - [ - 11, - 70 - ], - [ - 27, - 58 - ], - [ - -6, - 89 - ], - [ - -57, - -102 - ], - [ - -43, - -41 - ], - [ - -27, - -96 - ], - [ - -54, - 50 - ], - [ - 2, - 63 - ], - [ - -44, - 87 - ], - [ - -37, - 45 - ], - [ - 14, - 28 - ], - [ - -90, - 73 - ], - [ - -49, - 3 - ], - [ - -67, - 59 - ], - [ - -125, - -12 - ], - [ - -90, - -43 - ], - [ - -79, - -40 - ], - [ - -67, - 8 - ], - [ - -74, - -61 - ], - [ - -60, - -28 - ], - [ - -14, - -63 - ], - [ - -25, - -49 - ], - [ - -60, - -2 - ], - [ - -43, - -11 - ], - [ - -62, - 22 - ], - [ - -50, - -13 - ], - [ - -48, - -6 - ], - [ - -41, - -64 - ], - [ - -21, - 6 - ], - [ - -35, - -34 - ], - [ - -33, - -38 - ], - [ - -51, - 4 - ], - [ - -47, - 0 - ], - [ - -74, - 77 - ], - [ - -37, - 23 - ], - [ - 1, - 68 - ], - [ - 35, - 17 - ], - [ - 12, - 27 - ], - [ - -3, - 43 - ], - [ - 9, - 84 - ], - [ - -8, - 71 - ], - [ - -37, - 121 - ], - [ - -11, - 68 - ], - [ - 3, - 69 - ], - [ - -28, - 78 - ], - [ - -2, - 35 - ], - [ - -31, - 48 - ], - [ - -8, - 94 - ], - [ - -40, - 95 - ], - [ - -10, - 51 - ], - [ - 31, - -52 - ], - [ - -24, - 111 - ], - [ - 35, - -34 - ], - [ - 20, - -47 - ], - [ - -1, - 62 - ], - [ - -34, - 94 - ], - [ - -7, - 38 - ], - [ - -16, - 36 - ], - [ - 8, - 69 - ], - [ - 14, - 30 - ], - [ - 9, - 60 - ], - [ - -7, - 70 - ], - [ - 29, - 86 - ], - [ - 5, - -91 - ], - [ - 29, - 82 - ], - [ - 57, - 40 - ], - [ - 34, - 52 - ], - [ - 53, - 44 - ], - [ - 32, - 9 - ], - [ - 19, - -15 - ], - [ - 55, - 45 - ], - [ - 42, - 13 - ], - [ - 11, - 27 - ], - [ - 18, - 10 - ], - [ - 39, - -2 - ], - [ - 73, - 35 - ], - [ - 38, - 53 - ], - [ - 18, - 64 - ], - [ - 41, - 61 - ], - [ - 3, - 48 - ], - [ - 2, - 65 - ], - [ - 49, - 102 - ], - [ - 29, - -103 - ], - [ - 30, - 23 - ], - [ - -25, - 57 - ], - [ - 22, - 58 - ], - [ - 30, - -26 - ], - [ - 9, - 92 - ], - [ - 38, - 59 - ], - [ - 17, - 47 - ], - [ - 35, - 20 - ], - [ - 1, - 34 - ], - [ - 30, - -14 - ], - [ - 2, - 30 - ], - [ - 30, - 17 - ], - [ - 34, - 16 - ], - [ - 52, - -55 - ], - [ - 38, - -71 - ], - [ - 44, - 0 - ], - [ - 44, - -12 - ], - [ - -15, - 66 - ], - [ - 34, - 96 - ], - [ - 31, - 32 - ], - [ - -11, - 30 - ], - [ - 31, - 68 - ], - [ - 42, - 43 - ], - [ - 36, - -15 - ], - [ - 58, - 23 - ], - [ - -1, - 61 - ], - [ - -51, - 40 - ], - [ - 37, - 17 - ], - [ - 46, - -30 - ], - [ - 37, - -49 - ], - [ - 59, - -31 - ], - [ - 20, - 13 - ], - [ - 43, - -37 - ], - [ - 41, - 34 - ], - [ - 26, - -10 - ], - [ - 16, - 23 - ], - [ - 32, - -60 - ], - [ - -18, - -64 - ], - [ - -27, - -48 - ], - [ - -24, - -4 - ], - [ - 8, - -48 - ], - [ - -20, - -60 - ], - [ - -25, - -59 - ], - [ - 5, - -34 - ], - [ - 55, - -66 - ], - [ - 54, - -39 - ], - [ - 36, - -41 - ], - [ - 50, - -71 - ], - [ - 20, - 0 - ], - [ - 37, - -31 - ], - [ - 10, - -37 - ], - [ - 67, - -41 - ], - [ - 46, - 41 - ], - [ - 13, - 65 - ], - [ - 14, - 53 - ], - [ - 9, - 66 - ], - [ - 21, - 95 - ], - [ - -9, - 58 - ], - [ - 5, - 35 - ], - [ - -8, - 69 - ], - [ - 9, - 90 - ], - [ - 13, - 25 - ], - [ - -11, - 40 - ], - [ - 17, - 63 - ], - [ - 13, - 66 - ], - [ - 2, - 34 - ], - [ - 26, - 45 - ], - [ - 20, - -58 - ], - [ - 5, - -76 - ], - [ - 17, - -14 - ], - [ - 3, - -51 - ], - [ - 25, - -61 - ], - [ - 5, - -67 - ], - [ - -2, - -44 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - -5, - -50 - ], - [ - -39, - 0 - ], - [ - 13, - -26 - ], - [ - -23, - -77 - ] - ], - [ - [ - 13677, - 16418 - ], - [ - -13, - -20 - ], - [ - -61, - -3 - ], - [ - -35, - -27 - ], - [ - -58, - 9 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -100, - 31 - ], - [ - -15, - 42 - ], - [ - -69, - -21 - ], - [ - -8, - -23 - ], - [ - -43, - 17 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -35, - 3 - ], - [ - -32, - 22 - ], - [ - 11, - 29 - ], - [ - -3, - 22 - ] - ], - [ - [ - 13216, - 16499 - ], - [ - 21, - 6 - ], - [ - 36, - -33 - ], - [ - 10, - 32 - ], - [ - 61, - -5 - ], - [ - 50, - 21 - ], - [ - 33, - -4 - ], - [ - 22, - -24 - ], - [ - 7, - 20 - ], - [ - -10, - 78 - ], - [ - 25, - 16 - ], - [ - 24, - 55 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - 52, - -39 - ], - [ - 39, - 49 - ], - [ - 25, - 9 - ], - [ - 54, - -36 - ], - [ - 33, - 6 - ], - [ - 32, - -23 - ] - ], - [ - [ - 13730, - 16627 - ], - [ - -6, - -15 - ], - [ - 7, - -41 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 18, - 19 - ], - [ - 51, - -34 - ], - [ - 38, - -7 - ], - [ - 10, - 14 - ], - [ - -35, - 65 - ], - [ - 18, - 16 - ] - ], - [ - [ - 15782, - 15819 - ], - [ - 20, - -4 - ], - [ - 48, - -73 - ], - [ - 31, - -8 - ], - [ - 12, - 31 - ], - [ - 41, - 48 - ] - ], - [ - [ - 15934, - 15813 - ], - [ - 37, - -63 - ], - [ - 35, - -85 - ], - [ - 33, - -6 - ], - [ - 21, - -32 - ], - [ - -57, - -10 - ], - [ - -12, - -93 - ], - [ - -12, - -42 - ], - [ - -26, - -28 - ], - [ - 2, - -60 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - -17, - -6 - ], - [ - -44, - 63 - ], - [ - 24, - 60 - ], - [ - -20, - 35 - ], - [ - -26, - -9 - ], - [ - -83, - -89 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -48, - 16 - ], - [ - -35, - 55 - ], - [ - -11, - 46 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - -5, - 145 - ], - [ - -17, - 55 - ] - ], - [ - [ - 14571, - 10457 - ], - [ - 42, - -10 - ], - [ - 21, - 68 - ], - [ - 37, - -7 - ] - ], - [ - [ - 14671, - 10508 - ], - [ - 5, - -48 - ], - [ - 15, - -27 - ], - [ - 0, - -39 - ], - [ - -17, - -25 - ], - [ - -27, - -62 - ], - [ - -25, - -44 - ], - [ - -29, - -6 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -8, - -81 - ] - ], - [ - [ - 12969, - 16811 - ], - [ - -18, - -5 - ], - [ - -8, - -67 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - -61, - 55 - ], - [ - -36, - -9 - ], - [ - -48, - 56 - ], - [ - -33, - 48 - ], - [ - -32, - 2 - ], - [ - -10, - 42 - ] - ], - [ - [ - 12723, - 16933 - ], - [ - 56, - 24 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 51, - -9 - ], - [ - 64, - 25 - ], - [ - 44, - -53 - ], - [ - 39, - -28 - ] - ], - [ - [ - 12735, - 11548 - ], - [ - -57, - -14 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -18, - 83 - ], - [ - 4, - 275 - ], - [ - -15, - 25 - ], - [ - -2, - 59 - ], - [ - -24, - 42 - ], - [ - -22, - 35 - ], - [ - 9, - 64 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - 24, - 13 - ], - [ - 14, - 53 - ], - [ - 34, - 11 - ], - [ - 16, - 36 - ] - ], - [ - [ - 12698, - 12230 - ], - [ - 23, - 35 - ], - [ - 25, - 0 - ], - [ - 53, - -69 - ] - ], - [ - [ - 12799, - 12196 - ], - [ - -2, - -40 - ], - [ - 15, - -71 - ], - [ - -14, - -48 - ], - [ - 8, - -33 - ], - [ - -34, - -74 - ], - [ - -21, - -37 - ], - [ - -14, - -75 - ], - [ - 2, - -77 - ], - [ - -4, - -193 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - -61, - 2 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -32, - 10 - ], - [ - -23, - -20 - ], - [ - -30, - 9 - ], - [ - -121, - -6 - ], - [ - -2, - -68 - ], - [ - 9, - -90 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - -47, - 31 - ], - [ - -33, - -5 - ], - [ - -24, - -30 - ], - [ - -32, - 26 - ], - [ - -12, - 39 - ], - [ - -31, - 26 - ] - ], - [ - [ - 12171, - 12041 - ], - [ - -5, - 70 - ], - [ - 19, - 51 - ], - [ - -1, - 40 - ], - [ - 55, - 100 - ], - [ - 10, - 82 - ], - [ - 19, - 29 - ], - [ - 34, - -16 - ], - [ - 29, - 25 - ], - [ - 10, - 31 - ], - [ - 54, - 53 - ], - [ - 13, - 38 - ], - [ - 65, - 50 - ], - [ - 39, - 17 - ], - [ - 17, - -23 - ], - [ - 45, - 0 - ] - ], - [ - [ - 12574, - 12588 - ], - [ - -6, - -58 - ], - [ - 9, - -55 - ], - [ - 40, - -78 - ], - [ - 2, - -58 - ], - [ - 80, - -27 - ], - [ - -1, - -82 - ] - ], - [ - [ - 19008, - 13441 - ], - [ - -2, - -86 - ], - [ - -24, - 19 - ], - [ - 4, - -97 - ] - ], - [ - [ - 18986, - 13277 - ], - [ - -20, - 63 - ], - [ - -4, - 61 - ], - [ - -13, - 57 - ], - [ - -29, - 70 - ], - [ - -64, - 5 - ], - [ - 6, - -49 - ], - [ - -22, - -67 - ], - [ - -29, - 24 - ], - [ - -11, - -22 - ], - [ - -19, - 13 - ], - [ - -27, - 11 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -11, - 99 - ], - [ - -24, - 90 - ], - [ - 12, - 72 - ], - [ - -43, - 33 - ], - [ - 15, - 43 - ], - [ - 44, - 45 - ], - [ - -51, - 64 - ], - [ - 25, - 81 - ], - [ - 55, - -52 - ], - [ - 34, - -6 - ], - [ - 6, - -83 - ], - [ - 66, - -17 - ], - [ - 65, - 2 - ], - [ - 40, - -20 - ], - [ - -32, - -102 - ], - [ - -31, - -7 - ], - [ - -22, - -68 - ], - [ - 38, - -62 - ], - [ - 12, - 76 - ], - [ - 19, - 1 - ], - [ - 37, - -191 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - 20, - -49 - ], - [ - 27, - 8 - ], - [ - 54, - -18 - ], - [ - 102, - -7 - ], - [ - 34, - 31 - ], - [ - 83, - 28 - ], - [ - 50, - -44 - ], - [ - 41, - -12 - ] - ], - [ - [ - 14538, - 16041 - ], - [ - -36, - -50 - ], - [ - -25, - -86 - ], - [ - 22, - -68 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - -60, - 16 - ], - [ - -71, - -38 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - -1, - -60 - ], - [ - -63, - -11 - ], - [ - -49, - 42 - ], - [ - -56, - -33 - ], - [ - -52, - 3 - ] - ], - [ - [ - 14147, - 15756 - ], - [ - -4, - 80 - ], - [ - -35, - 38 - ] - ], - [ - [ - 14108, - 15874 - ], - [ - 11, - 17 - ], - [ - -7, - 15 - ], - [ - 11, - 38 - ], - [ - 27, - 37 - ], - [ - -34, - 52 - ], - [ - -6, - 44 - ], - [ - 17, - 27 - ] - ], - [ - [ - 7143, - 13648 - ], - [ - -17, - -6 - ], - [ - -18, - 69 - ], - [ - -26, - 35 - ], - [ - 15, - 76 - ], - [ - 21, - -5 - ], - [ - 24, - -100 - ], - [ - 1, - -69 - ] - ], - [ - [ - 7123, - 13986 - ], - [ - -76, - -19 - ], - [ - -5, - 44 - ], - [ - 33, - 10 - ], - [ - 46, - -4 - ], - [ - 2, - -31 - ] - ], - [ - [ - 7180, - 13987 - ], - [ - -12, - -85 - ], - [ - -13, - 15 - ], - [ - 1, - 63 - ], - [ - -31, - 47 - ], - [ - 0, - 14 - ], - [ - 55, - -54 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - -13, - -11 - ], - [ - -23, - -28 - ], - [ - -10, - -66 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -61, - 45 - ], - [ - -27, - 50 - ], - [ - -26, - 27 - ], - [ - -32, - 45 - ], - [ - -15, - 37 - ], - [ - -35, - 56 - ], - [ - 15, - 50 - ], - [ - 25, - -28 - ], - [ - 15, - 25 - ], - [ - 33, - 3 - ], - [ - 60, - -20 - ], - [ - 48, - 2 - ], - [ - 31, - -27 - ] - ], - [ - [ - 13872, - 16179 - ], - [ - 26, - 0 - ], - [ - -18, - -52 - ], - [ - 34, - -47 - ], - [ - -10, - -56 - ], - [ - -17, - -5 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - 67, - -1 - ], - [ - 76, - 45 - ], - [ - 16, - 68 - ], - [ - 57, - 39 - ], - [ - -7, - 53 - ] - ], - [ - [ - 14394, - 17469 - ], - [ - 43, - 20 - ], - [ - 75, - 47 - ] - ], - [ - [ - 14512, - 17536 - ], - [ - 73, - -30 - ], - [ - 10, - -30 - ], - [ - 37, - 14 - ], - [ - 68, - -28 - ], - [ - 6, - -57 - ], - [ - -14, - -32 - ], - [ - 43, - -79 - ], - [ - 29, - -22 - ], - [ - -5, - -21 - ], - [ - 47, - -21 - ], - [ - 21, - -32 - ], - [ - -28, - -27 - ], - [ - -56, - 5 - ], - [ - -13, - -12 - ], - [ - 16, - -39 - ], - [ - 17, - -77 - ] - ], - [ - [ - 14763, - 17048 - ], - [ - -60, - -7 - ], - [ - -21, - -27 - ], - [ - -5, - -60 - ], - [ - -27, - 12 - ], - [ - -63, - -6 - ], - [ - -18, - 28 - ], - [ - -27, - -21 - ], - [ - -26, - 17 - ], - [ - -55, - 3 - ], - [ - -78, - 28 - ], - [ - -70, - 10 - ], - [ - -54, - -3 - ], - [ - -38, - -32 - ], - [ - -33, - -5 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - -2, - 53 - ], - [ - -21, - 56 - ], - [ - 42, - 24 - ], - [ - 0, - 48 - ], - [ - -19, - 46 - ], - [ - -3, - 53 - ] - ], - [ - [ - 6333, - 12934 - ], - [ - 0, - 17 - ], - [ - 8, - 6 - ], - [ - 13, - -14 - ], - [ - 25, - 72 - ], - [ - 13, - 2 - ] - ], - [ - [ - 6392, - 13017 - ], - [ - 1, - -18 - ], - [ - 13, - -1 - ], - [ - -1, - -32 - ], - [ - -12, - -52 - ], - [ - 6, - -19 - ], - [ - -7, - -43 - ], - [ - 4, - -11 - ], - [ - -8, - -61 - ], - [ - -13, - -31 - ], - [ - -13, - -4 - ], - [ - -14, - -42 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - -21, - 0 - ], - [ - 6, - 136 - ], - [ - 0, - 95 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - -51, - -17 - ], - [ - -27, - 166 - ], - [ - -37, - 134 - ], - [ - 22, - 116 - ], - [ - -37, - 51 - ], - [ - -9, - 87 - ], - [ - -35, - 81 - ] - ], - [ - [ - 7696, - 8688 - ], - [ - 44, - 130 - ], - [ - -30, - 100 - ], - [ - 16, - 41 - ], - [ - -12, - 44 - ], - [ - 27, - 60 - ], - [ - 2, - 102 - ], - [ - 3, - 85 - ], - [ - 15, - 40 - ], - [ - -60, - 193 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - 52, - -10 - ], - [ - 35, - 3 - ], - [ - 16, - 36 - ], - [ - 61, - 49 - ], - [ - 37, - 45 - ], - [ - 91, - 20 - ], - [ - -8, - -90 - ], - [ - 9, - -46 - ], - [ - -6, - -80 - ], - [ - 76, - -108 - ], - [ - 78, - -20 - ], - [ - 28, - -44 - ], - [ - 47, - -24 - ], - [ - 29, - -35 - ], - [ - 43, - 1 - ], - [ - 41, - -35 - ], - [ - 3, - -70 - ], - [ - 14, - -35 - ], - [ - 0, - -52 - ], - [ - -20, - -2 - ], - [ - 27, - -139 - ], - [ - 134, - -5 - ], - [ - -11, - -70 - ], - [ - 8, - -47 - ], - [ - 38, - -34 - ], - [ - 16, - -74 - ], - [ - -12, - -95 - ], - [ - -19, - -52 - ], - [ - 7, - -69 - ], - [ - -22, - -24 - ] - ], - [ - [ - 8493, - 8377 - ], - [ - -1, - 37 - ], - [ - -65, - 61 - ], - [ - -65, - 2 - ], - [ - -122, - -35 - ], - [ - -33, - -106 - ], - [ - -2, - -64 - ], - [ - -27, - -144 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 13, - 70 - ], - [ - 10, - 70 - ], - [ - 0, - 66 - ], - [ - -25, - 22 - ], - [ - -26, - -19 - ], - [ - -26, - 5 - ], - [ - -9, - 46 - ], - [ - -6, - 110 - ], - [ - -13, - 36 - ], - [ - -47, - 33 - ], - [ - -29, - -24 - ], - [ - -73, - 23 - ], - [ - 4, - 163 - ], - [ - -20, - 67 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - -40, - -20 - ], - [ - -31, - 13 - ], - [ - 4, - 183 - ], - [ - -57, - -71 - ], - [ - -61, - 3 - ], - [ - -27, - 64 - ], - [ - -46, - 7 - ], - [ - 15, - 52 - ], - [ - -39, - 73 - ], - [ - -29, - 108 - ], - [ - 18, - 22 - ], - [ - 0, - 50 - ], - [ - 42, - 35 - ], - [ - -7, - 65 - ], - [ - 18, - 41 - ], - [ - 5, - 56 - ], - [ - 80, - 82 - ], - [ - 57, - 23 - ], - [ - 10, - 18 - ], - [ - 62, - -5 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - 32, - 328 - ], - [ - 1, - 53 - ], - [ - -11, - 68 - ], - [ - -31, - 44 - ], - [ - 1, - 87 - ], - [ - 39, - 20 - ], - [ - 14, - -13 - ], - [ - 2, - 46 - ], - [ - -40, - 13 - ], - [ - -1, - 75 - ], - [ - 135, - -3 - ], - [ - 24, - 42 - ], - [ - 19, - -38 - ], - [ - 14, - -71 - ], - [ - 13, - 15 - ] - ], - [ - [ - 7886, - 10948 - ], - [ - 38, - -64 - ], - [ - 54, - 8 - ], - [ - 14, - 37 - ], - [ - 52, - 28 - ], - [ - 28, - 19 - ], - [ - 8, - 51 - ], - [ - 50, - 34 - ], - [ - -4, - 25 - ], - [ - -59, - 11 - ], - [ - -9, - 75 - ], - [ - 2, - 81 - ], - [ - -31, - 31 - ], - [ - 13, - 11 - ], - [ - 52, - -15 - ], - [ - 55, - -30 - ], - [ - 21, - 28 - ], - [ - 50, - 19 - ], - [ - 78, - 44 - ], - [ - 25, - 46 - ], - [ - -9, - 34 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - 36, - 5 - ], - [ - 16, - -27 - ], - [ - -9, - -53 - ], - [ - 24, - -18 - ], - [ - 16, - -56 - ], - [ - -19, - -42 - ], - [ - -11, - -102 - ], - [ - 18, - -61 - ], - [ - 5, - -55 - ], - [ - 43, - -57 - ], - [ - 34, - -6 - ], - [ - 7, - 24 - ], - [ - 23, - 5 - ], - [ - 31, - 21 - ], - [ - 23, - 32 - ], - [ - 38, - -10 - ], - [ - 17, - 4 - ] - ], - [ - [ - 8606, - 11025 - ], - [ - 38, - -10 - ], - [ - 6, - 25 - ], - [ - -11, - 24 - ], - [ - 7, - 34 - ], - [ - 28, - -10 - ], - [ - 33, - 12 - ], - [ - 40, - -25 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 30, - -25 - ], - [ - 22, - 32 - ], - [ - 15, - -5 - ], - [ - 10, - -33 - ], - [ - 33, - 8 - ], - [ - 27, - 46 - ], - [ - 21, - 88 - ], - [ - 42, - 110 - ] - ], - [ - [ - 8947, - 11296 - ], - [ - 23, - 5 - ], - [ - 18, - -66 - ], - [ - 39, - -210 - ], - [ - 37, - -19 - ], - [ - 2, - -83 - ], - [ - -53, - -99 - ], - [ - 22, - -36 - ], - [ - 123, - -19 - ], - [ - 3, - -120 - ], - [ - 53, - 78 - ], - [ - 87, - -43 - ], - [ - 116, - -73 - ], - [ - 34, - -70 - ], - [ - -11, - -67 - ], - [ - 81, - 37 - ], - [ - 136, - -63 - ], - [ - 104, - 5 - ], - [ - 103, - -100 - ], - [ - 89, - -134 - ], - [ - 53, - -35 - ], - [ - 60, - -5 - ], - [ - 25, - -37 - ], - [ - 24, - -153 - ], - [ - 12, - -73 - ], - [ - -28, - -198 - ], - [ - -36, - -78 - ], - [ - -98, - -167 - ], - [ - -44, - -136 - ], - [ - -52, - -104 - ], - [ - -17, - -2 - ], - [ - -20, - -89 - ], - [ - 5, - -224 - ], - [ - -19, - -185 - ], - [ - -8, - -79 - ], - [ - -22, - -48 - ], - [ - -12, - -160 - ], - [ - -71, - -157 - ], - [ - -12, - -124 - ], - [ - -56, - -52 - ], - [ - -16, - -71 - ], - [ - -76, - 0 - ], - [ - -110, - -46 - ], - [ - -49, - -54 - ], - [ - -78, - -35 - ], - [ - -82, - -95 - ], - [ - -59, - -119 - ], - [ - -10, - -90 - ], - [ - 11, - -66 - ], - [ - -13, - -121 - ], - [ - -15, - -59 - ], - [ - -49, - -66 - ], - [ - -77, - -211 - ], - [ - -62, - -95 - ], - [ - -47, - -56 - ], - [ - -32, - -114 - ], - [ - -46, - -69 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -19, - 68 - ], - [ - 30, - 57 - ], - [ - -40, - 82 - ], - [ - -55, - 66 - ], - [ - -71, - 77 - ], - [ - -26, - -4 - ], - [ - -70, - 93 - ], - [ - -45, - -13 - ] - ], - [ - [ - 20508, - 11340 - ], - [ - 28, - 45 - ], - [ - 59, - 66 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - -3, - -59 - ], - [ - -4, - -77 - ], - [ - -33, - 4 - ], - [ - -15, - -41 - ], - [ - -32, - 62 - ] - ], - [ - [ - 18940, - 14129 - ], - [ - 28, - -38 - ], - [ - -5, - -74 - ], - [ - -57, - -4 - ], - [ - -59, - 8 - ], - [ - -44, - -18 - ], - [ - -63, - 45 - ], - [ - -1, - 24 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - 46, - 89 - ], - [ - 37, - 31 - ], - [ - 50, - -28 - ], - [ - 37, - -3 - ], - [ - 31, - -32 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - -98, - -88 - ], - [ - -63, - -90 - ], - [ - -23, - -80 - ], - [ - -21, - -45 - ], - [ - -38, - -10 - ], - [ - -12, - -57 - ], - [ - -7, - -37 - ], - [ - -45, - -28 - ], - [ - -57, - 6 - ], - [ - -33, - 33 - ], - [ - -29, - 15 - ], - [ - -34, - -28 - ], - [ - -18, - -58 - ], - [ - -33, - -36 - ], - [ - -34, - -53 - ], - [ - -50, - -12 - ], - [ - -16, - 42 - ], - [ - 7, - 73 - ], - [ - -42, - 114 - ], - [ - -19, - 18 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - 350 - ], - [ - 69, - 4 - ], - [ - 2, - 427 - ], - [ - 52, - 4 - ], - [ - 108, - 42 - ], - [ - 26, - -49 - ], - [ - 45, - 47 - ], - [ - 21, - 0 - ], - [ - 39, - 27 - ] - ], - [ - [ - 14296, - 8678 - ], - [ - 13, - -9 - ] - ], - [ - [ - 14309, - 8669 - ], - [ - 26, - -96 - ], - [ - 14, - -21 - ], - [ - 22, - -69 - ], - [ - 79, - -132 - ], - [ - 30, - -13 - ], - [ - 0, - -42 - ], - [ - 21, - -76 - ], - [ - 54, - -19 - ], - [ - 44, - -54 - ] - ], - [ - [ - 13613, - 11688 - ], - [ - 57, - 9 - ], - [ - 13, - 30 - ], - [ - 12, - -2 - ], - [ - 17, - -27 - ], - [ - 88, - 46 - ], - [ - 29, - 47 - ], - [ - 37, - 42 - ], - [ - -7, - 42 - ], - [ - 20, - 11 - ], - [ - 67, - -8 - ], - [ - 65, - 56 - ], - [ - 51, - 131 - ], - [ - 35, - 48 - ], - [ - 44, - 21 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 8, - -51 - ], - [ - 40, - -75 - ], - [ - 1, - -49 - ], - [ - -12, - -50 - ], - [ - 5, - -38 - ], - [ - 24, - -34 - ], - [ - 53, - -53 - ] - ], - [ - [ - 14260, - 11784 - ], - [ - 38, - -48 - ], - [ - 1, - -39 - ], - [ - 47, - -63 - ], - [ - 29, - -51 - ], - [ - 17, - -72 - ], - [ - 53, - -48 - ], - [ - 11, - -38 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - -23, - -13 - ], - [ - -45, - 3 - ], - [ - -52, - 13 - ], - [ - -26, - -11 - ], - [ - -11, - -29 - ], - [ - -22, - -3 - ], - [ - -28, - 25 - ], - [ - -77, - -60 - ], - [ - -32, - 12 - ], - [ - -10, - -9 - ], - [ - -21, - -72 - ], - [ - -52, - 23 - ], - [ - -51, - 12 - ], - [ - -44, - 44 - ], - [ - -57, - 41 - ], - [ - -38, - -39 - ], - [ - -27, - -61 - ], - [ - -6, - -83 - ] - ], - [ - [ - 13834, - 11218 - ], - [ - -45, - 6 - ], - [ - -47, - 20 - ], - [ - -42, - -63 - ], - [ - -36, - -112 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -8, - 35 - ], - [ - -3, - 55 - ], - [ - -32, - 38 - ], - [ - -25, - 62 - ], - [ - -6, - 43 - ], - [ - -33, - 63 - ], - [ - 5, - 36 - ], - [ - -7, - 50 - ], - [ - 6, - 93 - ], - [ - 17, - 22 - ], - [ - 35, - 122 - ] - ], - [ - [ - 8110, - 16382 - ], - [ - 50, - -16 - ], - [ - 65, - 3 - ], - [ - -35, - -49 - ], - [ - -25, - -8 - ], - [ - -89, - 51 - ], - [ - -17, - 40 - ], - [ - 26, - 37 - ], - [ - 25, - -58 - ] - ], - [ - [ - 8239, - 16688 - ], - [ - -34, - -2 - ], - [ - -90, - 38 - ], - [ - -65, - 56 - ], - [ - 24, - 10 - ], - [ - 92, - -30 - ], - [ - 71, - -50 - ], - [ - 2, - -22 - ] - ], - [ - [ - 3938, - 16617 - ], - [ - -35, - -17 - ], - [ - -115, - 55 - ], - [ - -21, - 42 - ], - [ - -62, - 42 - ], - [ - -13, - 34 - ], - [ - -71, - 22 - ], - [ - -27, - 65 - ], - [ - 6, - 28 - ], - [ - 73, - -26 - ], - [ - 43, - -18 - ], - [ - 65, - -13 - ], - [ - 24, - -41 - ], - [ - 34, - -57 - ], - [ - 70, - -50 - ], - [ - 29, - -66 - ] - ], - [ - [ - 8634, - 16878 - ], - [ - -46, - -105 - ], - [ - 46, - 41 - ], - [ - 47, - -26 - ], - [ - -25, - -42 - ], - [ - 62, - -33 - ], - [ - 32, - 29 - ], - [ - 70, - -36 - ], - [ - -22, - -88 - ], - [ - 49, - 20 - ], - [ - 9, - -63 - ], - [ - 21, - -75 - ], - [ - -29, - -106 - ], - [ - -31, - -4 - ], - [ - -46, - 23 - ], - [ - 15, - 98 - ], - [ - -20, - 15 - ], - [ - -80, - -104 - ], - [ - -42, - 4 - ], - [ - 49, - 56 - ], - [ - -67, - 30 - ], - [ - -75, - -8 - ], - [ - -135, - 4 - ], - [ - -11, - 36 - ], - [ - 44, - 42 - ], - [ - -30, - 32 - ], - [ - 58, - 73 - ], - [ - 72, - 191 - ], - [ - 43, - 68 - ], - [ - 61, - 41 - ], - [ - 32, - -5 - ], - [ - -13, - -32 - ], - [ - -38, - -76 - ] - ], - [ - [ - 3264, - 17296 - ], - [ - 33, - -16 - ], - [ - 66, - 10 - ], - [ - -20, - -136 - ], - [ - 60, - -97 - ], - [ - -28, - 0 - ], - [ - -42, - 55 - ], - [ - -25, - 56 - ], - [ - -36, - 37 - ], - [ - -12, - 53 - ], - [ - 4, - 38 - ] - ], - [ - [ - 7022, - 18254 - ], - [ - -27, - -63 - ], - [ - -31, - 10 - ], - [ - -18, - 36 - ], - [ - 3, - 9 - ], - [ - 27, - 36 - ], - [ - 28, - -3 - ], - [ - 18, - -25 - ] - ], - [ - [ - 6839, - 18321 - ], - [ - -82, - -67 - ], - [ - -49, - 3 - ], - [ - -16, - 33 - ], - [ - 52, - 55 - ], - [ - 96, - -1 - ], - [ - -1, - -23 - ] - ], - [ - [ - 6611, - 18674 - ], - [ - 13, - -53 - ], - [ - 36, - 19 - ], - [ - 40, - -32 - ], - [ - 77, - -41 - ], - [ - 79, - -37 - ], - [ - 7, - -57 - ], - [ - 51, - 9 - ], - [ - 50, - -40 - ], - [ - -62, - -37 - ], - [ - -109, - 28 - ], - [ - -39, - 54 - ], - [ - -69, - -63 - ], - [ - -99, - -62 - ], - [ - -24, - 70 - ], - [ - -95, - -12 - ], - [ - 61, - 59 - ], - [ - 9, - 95 - ], - [ - 24, - 110 - ], - [ - 50, - -10 - ] - ], - [ - [ - 7259, - 18853 - ], - [ - -78, - -6 - ], - [ - -18, - 59 - ], - [ - 30, - 67 - ], - [ - 64, - 17 - ], - [ - 54, - -34 - ], - [ - 1, - -51 - ], - [ - -8, - -17 - ], - [ - -45, - -35 - ] - ], - [ - [ - 5880, - 19088 - ], - [ - -43, - -42 - ], - [ - -94, - 36 - ], - [ - -57, - -13 - ], - [ - -95, - 54 - ], - [ - 61, - 37 - ], - [ - 49, - 52 - ], - [ - 74, - -34 - ], - [ - 42, - -21 - ], - [ - 21, - -23 - ], - [ - 42, - -46 - ] - ], - [ - [ - 3985, - 16676 - ], - [ - -10, - 0 - ], - [ - -135, - 118 - ], - [ - -50, - 52 - ], - [ - -126, - 49 - ], - [ - -39, - 106 - ], - [ - 10, - 74 - ], - [ - -89, - 51 - ], - [ - -12, - 97 - ], - [ - -84, - 87 - ], - [ - -2, - 62 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - 39, - 58 - ], - [ - -2, - 75 - ], - [ - -119, - 77 - ], - [ - -71, - 137 - ], - [ - -43, - 86 - ], - [ - -64, - 54 - ], - [ - -47, - 49 - ], - [ - -37, - 62 - ], - [ - -70, - -39 - ], - [ - -68, - -67 - ], - [ - -62, - 79 - ], - [ - -49, - 52 - ], - [ - -68, - 34 - ], - [ - -68, - 3 - ], - [ - 0, - 683 - ], - [ - 1, - 445 - ] - ], - [ - [ - 2720, - 19160 - ], - [ - 130, - -28 - ], - [ - 109, - -58 - ], - [ - 73, - -11 - ], - [ - 61, - 50 - ], - [ - 85, - 37 - ], - [ - 103, - -14 - ], - [ - 105, - 52 - ], - [ - 114, - 30 - ], - [ - 48, - -49 - ], - [ - 52, - 28 - ], - [ - 15, - 56 - ], - [ - 48, - -13 - ], - [ - 118, - -107 - ], - [ - 93, - 81 - ], - [ - 9, - -91 - ], - [ - 86, - 20 - ], - [ - 26, - 35 - ], - [ - 85, - -7 - ], - [ - 106, - -51 - ], - [ - 164, - -44 - ], - [ - 96, - -20 - ], - [ - 68, - 8 - ], - [ - 94, - -61 - ], - [ - -98, - -60 - ], - [ - 126, - -25 - ], - [ - 188, - 14 - ], - [ - 59, - 21 - ], - [ - 75, - -72 - ], - [ - 75, - 61 - ], - [ - -71, - 50 - ], - [ - 45, - 42 - ], - [ - 85, - 5 - ], - [ - 56, - 12 - ], - [ - 56, - -29 - ], - [ - 70, - -65 - ], - [ - 78, - 10 - ], - [ - 123, - -54 - ], - [ - 109, - 19 - ], - [ - 101, - -3 - ], - [ - -8, - 75 - ], - [ - 62, - 20 - ], - [ - 108, - -40 - ], - [ - 0, - -114 - ], - [ - 44, - 96 - ], - [ - 56, - -3 - ], - [ - 32, - 120 - ], - [ - -75, - 74 - ], - [ - -81, - 49 - ], - [ - 5, - 132 - ], - [ - 83, - 87 - ], - [ - 92, - -19 - ], - [ - 70, - -53 - ], - [ - 95, - -135 - ], - [ - -62, - -59 - ], - [ - 130, - -24 - ], - [ - -1, - -123 - ], - [ - 93, - 94 - ], - [ - 84, - -77 - ], - [ - -21, - -89 - ], - [ - 67, - -81 - ], - [ - 73, - 87 - ], - [ - 51, - 103 - ], - [ - 4, - 132 - ], - [ - 99, - -9 - ], - [ - 103, - -18 - ], - [ - 94, - -60 - ], - [ - 4, - -59 - ], - [ - -52, - -64 - ], - [ - 49, - -64 - ], - [ - -9, - -59 - ], - [ - -136, - -83 - ], - [ - -97, - -19 - ], - [ - -72, - 36 - ], - [ - -21, - -60 - ], - [ - -67, - -101 - ], - [ - -21, - -53 - ], - [ - -80, - -81 - ], - [ - -100, - -8 - ], - [ - -55, - -51 - ], - [ - -5, - -78 - ], - [ - -81, - -15 - ], - [ - -85, - -97 - ], - [ - -76, - -135 - ], - [ - -27, - -94 - ], - [ - -4, - -140 - ], - [ - 103, - -20 - ], - [ - 31, - -112 - ], - [ - 33, - -91 - ], - [ - 97, - 24 - ], - [ - 130, - -52 - ], - [ - 69, - -46 - ], - [ - 50, - -57 - ], - [ - 88, - -33 - ], - [ - 73, - -50 - ], - [ - 116, - -7 - ], - [ - 75, - -12 - ], - [ - -11, - -104 - ], - [ - 22, - -120 - ], - [ - 50, - -134 - ], - [ - 104, - -114 - ], - [ - 54, - 39 - ], - [ - 37, - 123 - ], - [ - -36, - 189 - ], - [ - -49, - 64 - ], - [ - 111, - 56 - ], - [ - 79, - 84 - ], - [ - 39, - 84 - ], - [ - -6, - 80 - ], - [ - -47, - 102 - ], - [ - -85, - 90 - ], - [ - 82, - 126 - ], - [ - -30, - 108 - ], - [ - -23, - 188 - ], - [ - 48, - 27 - ], - [ - 120, - -32 - ], - [ - 72, - -12 - ], - [ - 57, - 32 - ], - [ - 65, - -41 - ], - [ - 86, - -70 - ], - [ - 21, - -46 - ], - [ - 124, - -9 - ], - [ - -2, - -101 - ], - [ - 24, - -152 - ], - [ - 63, - -19 - ], - [ - 51, - -70 - ], - [ - 101, - 66 - ], - [ - 66, - 133 - ], - [ - 46, - 56 - ], - [ - 55, - -108 - ], - [ - 91, - -153 - ], - [ - 77, - -143 - ], - [ - -28, - -76 - ], - [ - 92, - -67 - ], - [ - 63, - -69 - ], - [ - 111, - -31 - ], - [ - 45, - -38 - ], - [ - 28, - -102 - ], - [ - 54, - -16 - ], - [ - 28, - -45 - ], - [ - 5, - -135 - ], - [ - -51, - -45 - ], - [ - -50, - -42 - ], - [ - -115, - -43 - ], - [ - -87, - -98 - ], - [ - -118, - -20 - ], - [ - -149, - 26 - ], - [ - -105, - 0 - ], - [ - -72, - -8 - ], - [ - -58, - -86 - ], - [ - -89, - -53 - ], - [ - -101, - -159 - ], - [ - -80, - -111 - ], - [ - 59, - 20 - ], - [ - 112, - 158 - ], - [ - 146, - 100 - ], - [ - 105, - 12 - ], - [ - 61, - -59 - ], - [ - -66, - -81 - ], - [ - 23, - -129 - ], - [ - 22, - -91 - ], - [ - 91, - -60 - ], - [ - 115, - 18 - ], - [ - 70, - 135 - ], - [ - 5, - -87 - ], - [ - 45, - -44 - ], - [ - -86, - -78 - ], - [ - -155, - -72 - ], - [ - -69, - -48 - ], - [ - -78, - -87 - ], - [ - -53, - 9 - ], - [ - -3, - 102 - ], - [ - 122, - 99 - ], - [ - -112, - -4 - ], - [ - -78, - -15 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - -45, - 68 - ], - [ - 0, - 164 - ], - [ - -31, - 34 - ], - [ - -47, - -20 - ], - [ - -23, - 31 - ], - [ - -53, - -90 - ], - [ - -21, - -93 - ], - [ - -25, - -55 - ], - [ - -30, - -19 - ], - [ - -22, - -6 - ], - [ - -7, - -29 - ], - [ - -128, - 0 - ], - [ - -106, - -1 - ], - [ - -32, - -22 - ], - [ - -73, - -87 - ], - [ - -9, - -9 - ], - [ - -22, - -47 - ], - [ - -64, - 0 - ], - [ - -69, - 0 - ], - [ - -31, - -19 - ], - [ - 11, - -24 - ], - [ - 6, - -36 - ], - [ - -1, - -13 - ], - [ - -91, - -59 - ], - [ - -72, - -19 - ], - [ - -81, - -64 - ], - [ - -18, - 0 - ], - [ - -23, - 19 - ], - [ - -8, - 17 - ], - [ - 1, - 12 - ], - [ - 16, - 42 - ], - [ - 32, - 66 - ], - [ - 21, - 71 - ], - [ - -14, - 105 - ], - [ - -15, - 108 - ], - [ - -73, - 57 - ], - [ - 9, - 21 - ], - [ - -10, - 15 - ], - [ - -19, - 0 - ], - [ - -14, - 19 - ], - [ - -4, - 28 - ], - [ - -13, - -12 - ], - [ - -19, - 3 - ], - [ - 4, - 12 - ], - [ - -16, - 12 - ], - [ - -7, - 32 - ], - [ - -54, - 38 - ], - [ - -57, - 40 - ], - [ - -68, - 46 - ], - [ - -65, - 44 - ], - [ - -63, - -34 - ], - [ - -22, - -1 - ], - [ - -86, - 31 - ], - [ - -57, - -16 - ], - [ - -67, - 38 - ], - [ - -71, - 19 - ], - [ - -49, - 7 - ], - [ - -22, - 20 - ], - [ - -12, - 66 - ], - [ - -24, - 0 - ], - [ - 0, - -46 - ], - [ - -144, - 0 - ], - [ - -239, - 0 - ], - [ - -237, - 0 - ], - [ - -209, - 0 - ], - [ - -209, - 0 - ], - [ - -206, - 0 - ], - [ - -212, - 0 - ], - [ - -69, - 0 - ], - [ - -207, - 0 - ], - [ - -197, - 0 - ] - ], - [ - [ - 4589, - 19569 - ], - [ - -35, - -56 - ], - [ - 155, - 37 - ], - [ - 97, - -61 - ], - [ - 79, - 61 - ], - [ - 64, - -39 - ], - [ - 57, - -118 - ], - [ - 35, - 50 - ], - [ - -50, - 123 - ], - [ - 62, - 17 - ], - [ - 69, - -19 - ], - [ - 78, - -48 - ], - [ - 44, - -117 - ], - [ - 21, - -85 - ], - [ - 118, - -59 - ], - [ - 125, - -57 - ], - [ - -7, - -53 - ], - [ - -115, - -9 - ], - [ - 45, - -47 - ], - [ - -24, - -44 - ], - [ - -126, - 19 - ], - [ - -120, - 33 - ], - [ - -81, - -8 - ], - [ - -131, - -40 - ], - [ - -176, - -18 - ], - [ - -124, - -12 - ], - [ - -38, - 57 - ], - [ - -95, - 33 - ], - [ - -62, - -14 - ], - [ - -86, - 95 - ], - [ - 46, - 13 - ], - [ - 108, - 20 - ], - [ - 98, - -5 - ], - [ - 91, - 21 - ], - [ - -135, - 28 - ], - [ - -149, - -10 - ], - [ - -98, - 3 - ], - [ - -37, - 44 - ], - [ - 161, - 48 - ], - [ - -107, - -2 - ], - [ - -122, - 32 - ], - [ - 59, - 90 - ], - [ - 48, - 48 - ], - [ - 187, - 73 - ], - [ - 71, - -24 - ] - ], - [ - [ - 5263, - 19605 - ], - [ - -61, - -79 - ], - [ - -109, - 84 - ], - [ - 24, - 17 - ], - [ - 93, - 5 - ], - [ - 53, - -27 - ] - ], - [ - [ - 7226, - 19567 - ], - [ - 6, - -33 - ], - [ - -74, - 4 - ], - [ - -75, - 2 - ], - [ - -76, - -16 - ], - [ - -21, - 7 - ], - [ - -76, - 64 - ], - [ - 3, - 43 - ], - [ - 33, - 8 - ], - [ - 160, - -13 - ], - [ - 120, - -66 - ] - ], - [ - [ - 6513, - 19574 - ], - [ - 55, - -75 - ], - [ - 65, - 97 - ], - [ - 176, - 49 - ], - [ - 120, - -124 - ], - [ - -10, - -79 - ], - [ - 138, - 35 - ], - [ - 65, - 48 - ], - [ - 155, - -61 - ], - [ - 96, - -57 - ], - [ - 9, - -52 - ], - [ - 130, - 27 - ], - [ - 72, - -77 - ], - [ - 169, - -47 - ], - [ - 60, - -48 - ], - [ - 66, - -113 - ], - [ - -128, - -56 - ], - [ - 164, - -78 - ], - [ - 111, - -26 - ], - [ - 100, - -110 - ], - [ - 110, - -8 - ], - [ - -22, - -85 - ], - [ - -122, - -139 - ], - [ - -86, - 51 - ], - [ - -110, - 116 - ], - [ - -90, - -15 - ], - [ - -9, - -69 - ], - [ - 74, - -70 - ], - [ - 94, - -55 - ], - [ - 29, - -32 - ], - [ - 46, - -119 - ], - [ - -25, - -86 - ], - [ - -87, - 33 - ], - [ - -175, - 96 - ], - [ - 98, - -104 - ], - [ - 73, - -72 - ], - [ - 11, - -42 - ], - [ - -189, - 48 - ], - [ - -149, - 70 - ], - [ - -85, - 58 - ], - [ - 24, - 34 - ], - [ - -104, - 61 - ], - [ - -101, - 59 - ], - [ - 1, - -35 - ], - [ - -202, - -19 - ], - [ - -59, - 41 - ], - [ - 46, - 88 - ], - [ - 131, - 2 - ], - [ - 144, - 16 - ], - [ - -23, - 43 - ], - [ - 24, - 59 - ], - [ - 90, - 117 - ], - [ - -19, - 53 - ], - [ - -27, - 41 - ], - [ - -107, - 59 - ], - [ - -141, - 40 - ], - [ - 45, - 31 - ], - [ - -74, - 74 - ], - [ - -62, - 7 - ], - [ - -54, - 41 - ], - [ - -38, - -35 - ], - [ - -126, - -16 - ], - [ - -254, - 27 - ], - [ - -147, - 35 - ], - [ - -113, - 18 - ], - [ - -58, - 42 - ], - [ - 73, - 55 - ], - [ - -99, - 1 - ], - [ - -23, - 121 - ], - [ - 54, - 107 - ], - [ - 72, - 49 - ], - [ - 180, - 32 - ], - [ - -52, - -77 - ] - ], - [ - [ - 5552, - 19656 - ], - [ - 83, - -25 - ], - [ - 124, - 15 - ], - [ - 18, - -35 - ], - [ - -65, - -57 - ], - [ - 106, - -52 - ], - [ - -13, - -108 - ], - [ - -114, - -46 - ], - [ - -67, - 10 - ], - [ - -48, - 46 - ], - [ - -174, - 92 - ], - [ - 2, - 39 - ], - [ - 142, - -15 - ], - [ - -77, - 78 - ], - [ - 83, - 58 - ] - ], - [ - [ - 6051, - 19528 - ], - [ - -75, - -90 - ], - [ - -79, - 4 - ], - [ - -44, - 106 - ], - [ - 1, - 59 - ], - [ - 37, - 51 - ], - [ - 69, - 33 - ], - [ - 145, - -4 - ], - [ - 133, - -29 - ], - [ - -104, - -107 - ], - [ - -83, - -23 - ] - ], - [ - [ - 4150, - 19361 - ], - [ - -183, - -58 - ], - [ - -37, - 53 - ], - [ - -161, - 63 - ], - [ - 30, - 51 - ], - [ - 48, - 88 - ], - [ - 61, - 78 - ], - [ - -68, - 74 - ], - [ - 235, - 19 - ], - [ - 100, - -25 - ], - [ - 178, - -7 - ], - [ - 68, - -35 - ], - [ - 74, - -50 - ], - [ - -87, - -30 - ], - [ - -171, - -85 - ], - [ - -87, - -84 - ], - [ - 0, - -52 - ] - ], - [ - [ - 6022, - 19792 - ], - [ - -38, - -46 - ], - [ - -101, - 9 - ], - [ - -85, - 31 - ], - [ - 37, - 54 - ], - [ - 101, - 32 - ], - [ - 60, - -42 - ], - [ - 26, - -38 - ] - ], - [ - [ - 5681, - 20001 - ], - [ - 54, - -55 - ], - [ - 2, - -62 - ], - [ - -32, - -89 - ], - [ - -115, - -12 - ], - [ - -75, - 19 - ], - [ - 2, - 70 - ], - [ - -115, - -10 - ], - [ - -4, - 93 - ], - [ - 75, - -4 - ], - [ - 105, - 41 - ], - [ - 98, - -7 - ], - [ - 5, - 16 - ] - ], - [ - [ - 5004, - 19939 - ], - [ - 28, - -43 - ], - [ - 62, - 20 - ], - [ - 73, - -5 - ], - [ - 12, - -59 - ], - [ - -42, - -57 - ], - [ - -237, - -18 - ], - [ - -175, - -52 - ], - [ - -106, - -3 - ], - [ - -9, - 39 - ], - [ - 145, - 53 - ], - [ - -315, - -14 - ], - [ - -98, - 22 - ], - [ - 95, - 117 - ], - [ - 66, - 33 - ], - [ - 196, - -40 - ], - [ - 124, - -71 - ], - [ - 122, - -9 - ], - [ - -100, - 114 - ], - [ - 64, - 44 - ], - [ - 72, - -14 - ], - [ - 23, - -57 - ] - ], - [ - [ - 5947, - 20047 - ], - [ - 78, - -39 - ], - [ - 137, - 0 - ], - [ - 60, - -39 - ], - [ - -16, - -45 - ], - [ - 80, - -27 - ], - [ - 44, - -29 - ], - [ - 94, - -5 - ], - [ - 102, - -10 - ], - [ - 111, - 26 - ], - [ - 142, - 10 - ], - [ - 113, - -8 - ], - [ - 75, - -46 - ], - [ - 15, - -49 - ], - [ - -43, - -32 - ], - [ - -104, - -26 - ], - [ - -89, - 15 - ], - [ - -200, - -19 - ], - [ - -143, - -2 - ], - [ - -113, - 15 - ], - [ - -185, - 38 - ], - [ - -24, - 66 - ], - [ - -9, - 60 - ], - [ - -70, - 52 - ], - [ - -144, - 15 - ], - [ - -81, - 37 - ], - [ - 27, - 49 - ], - [ - 143, - -7 - ] - ], - [ - [ - 4447, - 20112 - ], - [ - -9, - -92 - ], - [ - -54, - -42 - ], - [ - -65, - -5 - ], - [ - -129, - -52 - ], - [ - -112, - -18 - ], - [ - -95, - 26 - ], - [ - 119, - 90 - ], - [ - 143, - 77 - ], - [ - 107, - -1 - ], - [ - 95, - 17 - ] - ], - [ - [ - 6006, - 20097 - ], - [ - -32, - -3 - ], - [ - -130, - 7 - ], - [ - -19, - 34 - ], - [ - 140, - -2 - ], - [ - 49, - -22 - ], - [ - -8, - -14 - ] - ], - [ - [ - 4867, - 20118 - ], - [ - -130, - -34 - ], - [ - -104, - 39 - ], - [ - 57, - 38 - ], - [ - 101, - 12 - ], - [ - 99, - -19 - ], - [ - -23, - -36 - ] - ], - [ - [ - 4903, - 20227 - ], - [ - -85, - -23 - ], - [ - -116, - 0 - ], - [ - 2, - 17 - ], - [ - 71, - 36 - ], - [ - 37, - -6 - ], - [ - 91, - -24 - ] - ], - [ - [ - 5867, - 20162 - ], - [ - -103, - -25 - ], - [ - -57, - 28 - ], - [ - -29, - 45 - ], - [ - -6, - 49 - ], - [ - 90, - -4 - ], - [ - 41, - -8 - ], - [ - 83, - -42 - ], - [ - -19, - -43 - ] - ], - [ - [ - 5572, - 20194 - ], - [ - 28, - -50 - ], - [ - -114, - 13 - ], - [ - -115, - 39 - ], - [ - -155, - 4 - ], - [ - 67, - 36 - ], - [ - -84, - 29 - ], - [ - -5, - 46 - ], - [ - 137, - -16 - ], - [ - 188, - -44 - ], - [ - 53, - -57 - ] - ], - [ - [ - 6481, - 20354 - ], - [ - 85, - -39 - ], - [ - -96, - -36 - ], - [ - -129, - -90 - ], - [ - -123, - -8 - ], - [ - -145, - 15 - ], - [ - -75, - 49 - ], - [ - 1, - 43 - ], - [ - 56, - 32 - ], - [ - -128, - -1 - ], - [ - -77, - 40 - ], - [ - -44, - 55 - ], - [ - 48, - 53 - ], - [ - 49, - 37 - ], - [ - 71, - 8 - ], - [ - -30, - 27 - ], - [ - 162, - 7 - ], - [ - 89, - -65 - ], - [ - 117, - -25 - ], - [ - 114, - -23 - ], - [ - 55, - -79 - ] - ], - [ - [ - 7772, - 20767 - ], - [ - 187, - -9 - ], - [ - 149, - -15 - ], - [ - 128, - -33 - ], - [ - -3, - -32 - ], - [ - -170, - -52 - ], - [ - -169, - -24 - ], - [ - -63, - -27 - ], - [ - 152, - 0 - ], - [ - -165, - -72 - ], - [ - -113, - -34 - ], - [ - -119, - -98 - ], - [ - -144, - -20 - ], - [ - -45, - -25 - ], - [ - -211, - -13 - ], - [ - 96, - -15 - ], - [ - -48, - -21 - ], - [ - 58, - -59 - ], - [ - -66, - -41 - ], - [ - -108, - -34 - ], - [ - -33, - -47 - ], - [ - -97, - -36 - ], - [ - 9, - -27 - ], - [ - 119, - 4 - ], - [ - 2, - -29 - ], - [ - -186, - -72 - ], - [ - -182, - 33 - ], - [ - -205, - -18 - ], - [ - -104, - 14 - ], - [ - -132, - 6 - ], - [ - -8, - 58 - ], - [ - 128, - 27 - ], - [ - -34, - 87 - ], - [ - 43, - 8 - ], - [ - 186, - -52 - ], - [ - -95, - 77 - ], - [ - -113, - 23 - ], - [ - 56, - 47 - ], - [ - 124, - 28 - ], - [ - 20, - 42 - ], - [ - -99, - 47 - ], - [ - -29, - 62 - ], - [ - 190, - -5 - ], - [ - 55, - -13 - ], - [ - 109, - 43 - ], - [ - -157, - 14 - ], - [ - -244, - -7 - ], - [ - -123, - 40 - ], - [ - -58, - 49 - ], - [ - -82, - 35 - ], - [ - -15, - 41 - ], - [ - 104, - 23 - ], - [ - 81, - 4 - ], - [ - 137, - 19 - ], - [ - 102, - 45 - ], - [ - 87, - -6 - ], - [ - 75, - -34 - ], - [ - 53, - 65 - ], - [ - 92, - 19 - ], - [ - 125, - 13 - ], - [ - 213, - 5 - ], - [ - 37, - -13 - ], - [ - 202, - 21 - ], - [ - 151, - -8 - ], - [ - 150, - -8 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -5, - -49 - ], - [ - -31, - -20 - ], - [ - -51, - 15 - ], - [ - -15, - -49 - ], - [ - -34, - -4 - ], - [ - -12, - 19 - ], - [ - -39, - -40 - ], - [ - -33, - -6 - ], - [ - -30, - 26 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -24, - 52 - ], - [ - -34, - -18 - ], - [ - 1, - 54 - ], - [ - 51, - 67 - ], - [ - -2, - 31 - ], - [ - 32, - -11 - ], - [ - 19, - 20 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 59, - -1 - ], - [ - 15, - 26 - ], - [ - 74, - -36 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -23, - -48 - ], - [ - -60, - -37 - ], - [ - -34, - 3 - ], - [ - -42, - 10 - ], - [ - -50, - 36 - ], - [ - -73, - 17 - ], - [ - -88, - 67 - ], - [ - -71, - 65 - ], - [ - -96, - 134 - ], - [ - 57, - -25 - ], - [ - 98, - -80 - ], - [ - 93, - -43 - ], - [ - 36, - 55 - ], - [ - 22, - 82 - ], - [ - 65, - 50 - ], - [ - 49, - -15 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -62, - 1 - ], - [ - -33, - -30 - ], - [ - -63, - -43 - ], - [ - -11, - -112 - ], - [ - -30, - -3 - ], - [ - -78, - 39 - ], - [ - -80, - 84 - ], - [ - -87, - 68 - ], - [ - -22, - 76 - ], - [ - 20, - 71 - ], - [ - -35, - 79 - ], - [ - -9, - 205 - ], - [ - 30, - 115 - ], - [ - 73, - 93 - ], - [ - -106, - 35 - ], - [ - 67, - 106 - ], - [ - 24, - 199 - ], - [ - 77, - -42 - ], - [ - 36, - 249 - ], - [ - -46, - 31 - ], - [ - -22, - -149 - ], - [ - -44, - 17 - ], - [ - 22, - 171 - ], - [ - 24, - 222 - ], - [ - 32, - 82 - ], - [ - -20, - 117 - ], - [ - -6, - 136 - ], - [ - 29, - 3 - ], - [ - 43, - 194 - ], - [ - 48, - 192 - ], - [ - 30, - 179 - ], - [ - -16, - 180 - ], - [ - 20, - 99 - ], - [ - -8, - 148 - ], - [ - 41, - 146 - ], - [ - 12, - 232 - ], - [ - 23, - 249 - ], - [ - 22, - 269 - ], - [ - -6, - 196 - ], - [ - -14, - 169 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - 36, - 31 - ], - [ - 18, - 61 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - -10, - 69 - ], - [ - 2, - 46 - ], - [ - -42, - 28 - ], - [ - -23, - -12 - ], - [ - -18, - 111 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - 20, - 27 - ], - [ - -9, - 28 - ], - [ - 66, - 57 - ], - [ - 48, - 23 - ], - [ - 74, - -16 - ], - [ - 26, - 77 - ], - [ - 90, - 14 - ], - [ - 25, - 48 - ], - [ - 109, - 65 - ], - [ - 10, - 27 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -5, - 68 - ], - [ - 48, - 31 - ], - [ - -63, - 209 - ], - [ - 138, - 48 - ], - [ - 36, - 27 - ], - [ - 50, - 214 - ], - [ - 138, - -39 - ], - [ - 39, - 54 - ], - [ - 3, - 120 - ], - [ - 58, - 12 - ], - [ - 53, - 79 - ] - ], - [ - [ - 18637, - 16701 - ], - [ - 27, - 10 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 19, - -83 - ], - [ - 58, - -64 - ], - [ - 100, - -45 - ], - [ - 48, - -97 - ], - [ - -27, - -140 - ], - [ - 25, - -52 - ], - [ - 83, - -20 - ], - [ - 94, - -17 - ], - [ - 84, - -75 - ], - [ - 43, - -13 - ], - [ - 32, - -111 - ], - [ - 41, - -71 - ], - [ - 77, - 3 - ], - [ - 144, - -27 - ], - [ - 92, - 17 - ], - [ - 69, - -18 - ], - [ - 103, - -73 - ], - [ - 85, - 0 - ], - [ - 30, - -37 - ], - [ - 82, - 64 - ], - [ - 112, - 42 - ], - [ - 105, - 4 - ], - [ - 81, - 42 - ], - [ - 50, - 65 - ], - [ - 49, - 40 - ], - [ - -11, - 40 - ], - [ - -23, - 46 - ], - [ - 37, - 77 - ], - [ - 39, - -11 - ], - [ - 72, - -24 - ], - [ - 69, - 64 - ], - [ - 107, - 46 - ], - [ - 51, - 79 - ], - [ - 49, - 34 - ], - [ - 101, - 16 - ], - [ - 55, - -13 - ], - [ - 8, - 42 - ], - [ - -64, - 84 - ], - [ - -55, - 39 - ], - [ - -54, - -45 - ], - [ - -69, - 19 - ], - [ - -39, - -15 - ], - [ - -18, - 49 - ], - [ - 49, - 120 - ], - [ - 34, - 90 - ] - ], - [ - [ - 20681, - 16782 - ], - [ - 84, - -45 - ], - [ - 98, - 76 - ], - [ - -1, - 53 - ], - [ - 63, - 127 - ], - [ - 39, - 38 - ], - [ - -1, - 67 - ], - [ - -38, - 28 - ], - [ - 57, - 60 - ], - [ - 87, - 21 - ], - [ - 92, - 4 - ], - [ - 105, - -36 - ], - [ - 61, - -44 - ], - [ - 43, - -121 - ], - [ - 26, - -52 - ], - [ - 24, - -74 - ], - [ - 26, - -117 - ], - [ - 122, - -38 - ], - [ - 82, - -86 - ], - [ - 28, - -112 - ], - [ - 106, - -1 - ], - [ - 61, - 48 - ], - [ - 115, - 35 - ], - [ - -37, - -108 - ], - [ - -27, - -44 - ], - [ - -24, - -131 - ], - [ - -47, - -117 - ], - [ - -84, - 21 - ], - [ - -60, - -42 - ], - [ - 18, - -103 - ], - [ - -10, - -142 - ], - [ - -35, - -3 - ], - [ - 0, - -61 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - -45, - 71 - ], - [ - -28, - -67 - ], - [ - -107, - -52 - ], - [ - 11, - -63 - ], - [ - -61, - 4 - ], - [ - -33, - 38 - ], - [ - -48, - -85 - ], - [ - -76, - -65 - ], - [ - -57, - -77 - ] - ], - [ - [ - 21210, - 15587 - ], - [ - -98, - -35 - ], - [ - -51, - -56 - ], - [ - -75, - -32 - ], - [ - 37, - 55 - ], - [ - -15, - 47 - ], - [ - 56, - 81 - ], - [ - -37, - 62 - ], - [ - -61, - -42 - ], - [ - -79, - -83 - ], - [ - -43, - -78 - ], - [ - -68, - -6 - ], - [ - -35, - -55 - ], - [ - 36, - -82 - ], - [ - 57, - -19 - ], - [ - 3, - -54 - ], - [ - 55, - -35 - ], - [ - 78, - 85 - ], - [ - 62, - -46 - ], - [ - 45, - -3 - ], - [ - 11, - -63 - ], - [ - -99, - -34 - ], - [ - -32, - -65 - ], - [ - -68, - -60 - ], - [ - -36, - -84 - ], - [ - 75, - -66 - ], - [ - 28, - -118 - ], - [ - 42, - -110 - ], - [ - 48, - -92 - ], - [ - -2, - -89 - ], - [ - -43, - -33 - ], - [ - 16, - -64 - ], - [ - 41, - -37 - ], - [ - -10, - -98 - ], - [ - -18, - -95 - ], - [ - -39, - -10 - ], - [ - -51, - -130 - ], - [ - -56, - -158 - ], - [ - -65, - -143 - ], - [ - -96, - -111 - ], - [ - -97, - -101 - ], - [ - -79, - -13 - ], - [ - -42, - -54 - ], - [ - -24, - 39 - ], - [ - -40, - -59 - ], - [ - -97, - -60 - ], - [ - -74, - -19 - ], - [ - -24, - -127 - ], - [ - -38, - -7 - ], - [ - -19, - 88 - ], - [ - 17, - 46 - ], - [ - -94, - 38 - ], - [ - -33, - -19 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -70, - 31 - ], - [ - -33, - 49 - ], - [ - 11, - 69 - ], - [ - -64, - 22 - ], - [ - -33, - 45 - ], - [ - -60, - -64 - ], - [ - -67, - -14 - ], - [ - -56, - 1 - ], - [ - -37, - -30 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - -37, - -17 - ], - [ - 11, - -138 - ], - [ - -37, - 4 - ], - [ - -6, - 28 - ] - ], - [ - [ - 19601, - 13369 - ], - [ - -2, - 50 - ], - [ - -52, - -35 - ], - [ - -30, - 22 - ], - [ - -52, - 45 - ], - [ - 21, - 99 - ], - [ - -44, - 24 - ], - [ - -17, - 110 - ], - [ - -74, - -20 - ], - [ - 9, - 142 - ], - [ - 66, - 101 - ], - [ - 3, - 99 - ], - [ - -2, - 91 - ], - [ - -31, - 29 - ], - [ - -23, - 71 - ], - [ - -41, - -9 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - -75, - 18 - ], - [ - 23, - 50 - ], - [ - -32, - 75 - ], - [ - -50, - -51 - ], - [ - -58, - 30 - ], - [ - -81, - -77 - ], - [ - -63, - -89 - ], - [ - -56, - -15 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - -6, - 95 - ], - [ - -43, - -25 - ] - ], - [ - [ - 18690, - 14142 - ], - [ - -81, - 11 - ], - [ - -79, - 28 - ], - [ - -56, - 52 - ], - [ - -55, - 24 - ], - [ - -23, - 58 - ], - [ - -39, - 17 - ], - [ - -71, - 78 - ], - [ - -55, - 37 - ], - [ - -29, - -29 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -97, - 84 - ], - [ - -69, - 76 - ], - [ - -19, - 132 - ], - [ - 50, - -16 - ], - [ - 2, - 61 - ], - [ - -28, - 62 - ], - [ - 7, - 98 - ], - [ - -75, - 140 - ] - ], - [ - [ - 17973, - 15055 - ], - [ - -114, - 49 - ], - [ - -21, - 92 - ], - [ - -51, - 56 - ] - ], - [ - [ - 20239, - 13038 - ], - [ - -60, - -58 - ], - [ - -57, - 38 - ], - [ - -2, - 103 - ], - [ - 34, - 54 - ], - [ - 76, - 34 - ], - [ - 40, - -3 - ], - [ - 16, - -46 - ], - [ - -31, - -53 - ], - [ - -16, - -69 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - 19, - -171 - ], - [ - -29, - -100 - ], - [ - -19, - -136 - ], - [ - 31, - -103 - ], - [ - -4, - -48 - ] - ], - [ - [ - 12348, - 11396 - ], - [ - -31, - -1 - ], - [ - -49, - 24 - ], - [ - -45, - -2 - ], - [ - -82, - -21 - ], - [ - -49, - -34 - ], - [ - -69, - -44 - ], - [ - -13, - 3 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - 5, - 99 - ], - [ - 7, - 15 - ], - [ - -2, - 47 - ], - [ - -30, - 50 - ], - [ - -22, - 8 - ], - [ - -20, - 33 - ], - [ - 15, - 53 - ], - [ - -7, - 58 - ], - [ - 3, - 35 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - 11, - 0 - ], - [ - 4, - 53 - ], - [ - -5, - 23 - ], - [ - 7, - 17 - ], - [ - 26, - 14 - ], - [ - -18, - 96 - ], - [ - -16, - 50 - ], - [ - 6, - 40 - ], - [ - 14, - 10 - ] - ], - [ - [ - 11988, - 12022 - ], - [ - 9, - 11 - ], - [ - 19, - -18 - ], - [ - 54, - -1 - ], - [ - 13, - 35 - ], - [ - 12, - -3 - ], - [ - 20, - 14 - ], - [ - 11, - -52 - ], - [ - 16, - 16 - ], - [ - 29, - 17 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -5, - -65 - ], - [ - -56, - 29 - ], - [ - -56, - 31 - ], - [ - -88, - 5 - ] - ], - [ - [ - 13459, - 11069 - ], - [ - -9, - 7 - ], - [ - -41, - -16 - ], - [ - -42, - 16 - ], - [ - -33, - -8 - ] - ], - [ - [ - 13334, - 11068 - ], - [ - -114, - 3 - ] - ], - [ - [ - 13220, - 11071 - ], - [ - 10, - 95 - ], - [ - -27, - 79 - ], - [ - -32, - 21 - ], - [ - -14, - 53 - ], - [ - -18, - 18 - ], - [ - 1, - 33 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - 18, - 85 - ], - [ - 33, - 115 - ], - [ - 20, - 1 - ], - [ - 42, - 71 - ], - [ - 26, - 2 - ], - [ - 39, - -50 - ], - [ - 48, - 41 - ], - [ - 7, - 50 - ], - [ - 15, - 48 - ], - [ - 11, - 61 - ], - [ - 38, - 49 - ], - [ - 14, - 84 - ], - [ - 14, - 27 - ], - [ - 10, - 62 - ], - [ - 19, - 77 - ], - [ - 58, - 93 - ], - [ - 4, - 39 - ], - [ - 8, - 22 - ], - [ - -28, - 48 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - 2, - 38 - ], - [ - 20, - 7 - ] - ], - [ - [ - 13558, - 12340 - ], - [ - 28, - -77 - ], - [ - 4, - -79 - ], - [ - -2, - -80 - ], - [ - 38, - -109 - ], - [ - -39, - 1 - ], - [ - -20, - -9 - ], - [ - -32, - 12 - ], - [ - -15, - -56 - ], - [ - 41, - -70 - ], - [ - 31, - -21 - ], - [ - 10, - -49 - ], - [ - 22, - -83 - ], - [ - -11, - -32 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - -9, - 38 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - 19, - -13 - ], - [ - 24, - 46 - ], - [ - 38, - -1 - ], - [ - 4, - -34 - ], - [ - 26, - -21 - ], - [ - 41, - 75 - ], - [ - 41, - 59 - ], - [ - 17, - 38 - ], - [ - -2, - 99 - ], - [ - 30, - 116 - ], - [ - 32, - 62 - ], - [ - 46, - 58 - ], - [ - 8, - 38 - ], - [ - 2, - 44 - ], - [ - 11, - 42 - ], - [ - -3, - 68 - ], - [ - 8, - 106 - ], - [ - 14, - 75 - ], - [ - 21, - 64 - ], - [ - 4, - 73 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - 42, - -99 - ], - [ - 31, - -14 - ], - [ - 19, - 20 - ], - [ - 32, - -8 - ], - [ - 39, - 25 - ], - [ - 17, - -51 - ], - [ - 61, - -80 - ] - ], - [ - [ - 14697, - 11218 - ], - [ - -4, - -140 - ], - [ - 28, - -16 - ], - [ - -23, - -43 - ], - [ - -27, - -32 - ], - [ - -26, - -62 - ], - [ - -15, - -56 - ], - [ - -4, - -96 - ], - [ - -16, - -46 - ], - [ - -1, - -91 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - -20, - -33 - ], - [ - -2, - -72 - ], - [ - -10, - -9 - ], - [ - -6, - -65 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - 12, - -110 - ], - [ - -7, - -62 - ], - [ - 14, - -70 - ], - [ - 41, - -67 - ], - [ - 37, - -151 - ] - ], - [ - [ - 14690, - 9797 - ], - [ - -27, - 12 - ], - [ - -94, - -20 - ], - [ - -18, - -15 - ], - [ - -20, - -76 - ], - [ - 15, - -53 - ], - [ - -12, - -142 - ], - [ - -9, - -121 - ], - [ - 19, - -21 - ], - [ - 49, - -47 - ], - [ - 19, - 22 - ], - [ - 6, - -129 - ], - [ - -54, - 1 - ], - [ - -28, - 66 - ], - [ - -26, - 51 - ], - [ - -53, - 17 - ], - [ - -16, - 63 - ], - [ - -43, - -38 - ], - [ - -55, - 16 - ], - [ - -24, - 55 - ], - [ - -44, - 11 - ], - [ - -33, - -3 - ], - [ - -4, - 37 - ], - [ - -24, - 3 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - -57, - 127 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - 53, - 66 - ], - [ - -26, - 79 - ], - [ - 24, - 31 - ], - [ - 47, - 14 - ], - [ - 5, - 53 - ], - [ - 37, - -57 - ], - [ - 62, - -5 - ], - [ - 21, - 56 - ], - [ - 9, - 80 - ], - [ - -8, - 94 - ], - [ - -33, - 71 - ], - [ - 31, - 139 - ], - [ - -18, - 24 - ], - [ - -52, - -10 - ], - [ - -19, - 62 - ], - [ - 5, - 52 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - -35, - 63 - ], - [ - -20, - 3 - ], - [ - 45, - 122 - ], - [ - -54, - 56 - ], - [ - -42, - -10 - ], - [ - -25, - 21 - ], - [ - -38, - -32 - ], - [ - -52, - 15 - ], - [ - -41, - 126 - ], - [ - -32, - 31 - ], - [ - -23, - 57 - ], - [ - -46, - 56 - ], - [ - -19, - -11 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - -29, - 28 - ], - [ - -35, - 40 - ], - [ - -20, - -19 - ], - [ - -59, - 17 - ], - [ - -17, - 51 - ], - [ - -13, - -2 - ], - [ - -69, - 69 - ] - ], - [ - [ - 7051, - 10963 - ], - [ - -10, - 37 - ], - [ - 26, - 9 - ], - [ - -3, - 60 - ], - [ - 16, - 44 - ], - [ - 35, - 8 - ], - [ - 29, - 75 - ], - [ - 27, - 63 - ], - [ - -26, - 29 - ], - [ - 14, - 69 - ], - [ - -16, - 110 - ], - [ - 15, - 31 - ], - [ - -11, - 102 - ], - [ - -28, - 64 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - 8, - 58 - ], - [ - 23, - -8 - ], - [ - 13, - 35 - ], - [ - -16, - 71 - ], - [ - 8, - 17 - ] - ], - [ - [ - 7155, - 11837 - ], - [ - 36, - -3 - ], - [ - 53, - 83 - ], - [ - 28, - 13 - ], - [ - 1, - 40 - ], - [ - 13, - 101 - ], - [ - 40, - 56 - ], - [ - 44, - 2 - ], - [ - 5, - 25 - ], - [ - 55, - -10 - ], - [ - 55, - 61 - ], - [ - 27, - 26 - ], - [ - 34, - 58 - ], - [ - 24, - -7 - ], - [ - 19, - -32 - ], - [ - -14, - -40 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -45, - -20 - ], - [ - -17, - -60 - ], - [ - -27, - -35 - ], - [ - -21, - -44 - ], - [ - -8, - -86 - ], - [ - -19, - -70 - ], - [ - 36, - -8 - ], - [ - 8, - -55 - ], - [ - 16, - -26 - ], - [ - 5, - -49 - ], - [ - -8, - -44 - ], - [ - 3, - -25 - ], - [ - 17, - -10 - ], - [ - 16, - -42 - ], - [ - 90, - 12 - ], - [ - 40, - -16 - ], - [ - 49, - -103 - ], - [ - 29, - 13 - ], - [ - 50, - -7 - ], - [ - 40, - 14 - ], - [ - 24, - -21 - ], - [ - -12, - -64 - ], - [ - -16, - -40 - ], - [ - -5, - -86 - ], - [ - 14, - -80 - ], - [ - 20, - -36 - ], - [ - 2, - -27 - ], - [ - -35, - -59 - ], - [ - 25, - -27 - ], - [ - 18, - -42 - ], - [ - 22, - -119 - ] - ], - [ - [ - 6764, - 11784 - ], - [ - -38, - 27 - ], - [ - -14, - 25 - ], - [ - 8, - 21 - ], - [ - -2, - 26 - ], - [ - -20, - 29 - ], - [ - -27, - 23 - ], - [ - -24, - 16 - ], - [ - -5, - 35 - ], - [ - -18, - 21 - ], - [ - 4, - -35 - ], - [ - -13, - -28 - ], - [ - -16, - 33 - ], - [ - -23, - 12 - ], - [ - -9, - 24 - ], - [ - 0, - 37 - ], - [ - 9, - 37 - ], - [ - -19, - 17 - ], - [ - 16, - 23 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - 10, - 16 - ], - [ - 46, - -32 - ], - [ - 16, - 16 - ], - [ - 22, - -10 - ], - [ - 12, - -25 - ], - [ - 20, - -8 - ], - [ - 17, - 26 - ] - ], - [ - [ - 6716, - 12110 - ], - [ - 18, - -66 - ], - [ - 27, - -48 - ], - [ - 32, - -51 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - -27, - -11 - ], - [ - 1, - -48 - ], - [ - 14, - -18 - ], - [ - -10, - -14 - ], - [ - 3, - -22 - ], - [ - -6, - -24 - ], - [ - -4, - -24 - ] - ], - [ - [ - 6813, - 13579 - ], - [ - 60, - -8 - ], - [ - 55, - -2 - ], - [ - 65, - -41 - ], - [ - 28, - -44 - ], - [ - 65, - 14 - ], - [ - 25, - -28 - ], - [ - 59, - -75 - ], - [ - 43, - -54 - ], - [ - 23, - 2 - ], - [ - 42, - -24 - ], - [ - -5, - -34 - ], - [ - 51, - -5 - ], - [ - 53, - -49 - ], - [ - -9, - -28 - ], - [ - -46, - -16 - ], - [ - -47, - -6 - ], - [ - -48, - 10 - ], - [ - -100, - -12 - ], - [ - 47, - 67 - ], - [ - -28, - 31 - ], - [ - -45, - 8 - ], - [ - -24, - 35 - ], - [ - -17, - 68 - ], - [ - -39, - -4 - ], - [ - -65, - 32 - ], - [ - -21, - 25 - ], - [ - -91, - 19 - ], - [ - -24, - 23 - ], - [ - 26, - 30 - ], - [ - -69, - 6 - ], - [ - -50, - -62 - ], - [ - -29, - -2 - ], - [ - -10, - -29 - ], - [ - -34, - -13 - ], - [ - -30, - 11 - ], - [ - 37, - 37 - ], - [ - 15, - 43 - ], - [ - 31, - 27 - ], - [ - 36, - 23 - ], - [ - 53, - 12 - ], - [ - 17, - 13 - ] - ], - [ - [ - 14829, - 15013 - ], - [ - 5, - 1 - ], - [ - 10, - 28 - ], - [ - 50, - -1 - ], - [ - 64, - 36 - ], - [ - -47, - -51 - ], - [ - 5, - -23 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - -8, - 4 - ], - [ - -13, - -9 - ], - [ - -10, - 3 - ], - [ - -4, - -5 - ], - [ - -1, - 12 - ], - [ - -5, - 8 - ], - [ - -14, - 1 - ], - [ - -19, - -10 - ], - [ - -13, - 6 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - 2, - -10 - ], - [ - -72, - -48 - ], - [ - -34, - 15 - ], - [ - -16, - 48 - ], - [ - 33, - 5 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - -39, - 52 - ], - [ - -36, - 28 - ], - [ - -7, - 51 - ], - [ - -12, - 36 - ], - [ - 50, - 26 - ], - [ - 26, - 30 - ], - [ - 50, - 23 - ], - [ - 18, - 23 - ], - [ - 18, - -14 - ], - [ - 31, - 12 - ] - ], - [ - [ - 13594, - 16928 - ], - [ - 33, - -38 - ], - [ - 52, - -11 - ], - [ - -4, - -33 - ], - [ - 38, - -24 - ], - [ - 10, - 30 - ], - [ - 48, - -13 - ], - [ - 7, - -37 - ], - [ - 52, - -8 - ], - [ - 32, - -59 - ] - ], - [ - [ - 13862, - 16735 - ], - [ - -21, - 0 - ], - [ - -11, - -22 - ], - [ - -16, - -5 - ], - [ - -4, - -27 - ], - [ - -14, - -6 - ], - [ - -2, - -11 - ], - [ - -23, - -12 - ], - [ - -31, - 2 - ], - [ - -10, - -27 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 9, - 86 - ], - [ - 35, - 82 - ], - [ - -100, - 22 - ], - [ - -33, - 31 - ] - ], - [ - [ - 12979, - 16731 - ], - [ - 4, - 53 - ], - [ - -14, - 27 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -12, - 126 - ], - [ - 42, - 0 - ], - [ - 18, - 45 - ], - [ - 17, - 110 - ], - [ - -13, - 40 - ] - ], - [ - [ - 13029, - 17213 - ], - [ - 13, - 26 - ], - [ - 59, - 6 - ], - [ - 13, - -26 - ], - [ - 47, - 59 - ], - [ - -16, - 45 - ], - [ - -3, - 68 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - 53, - -16 - ], - [ - 44, - 18 - ] - ], - [ - [ - 13239, - 17393 - ], - [ - 1, - -46 - ], - [ - 71, - -28 - ], - [ - -1, - -42 - ], - [ - 71, - 22 - ], - [ - 39, - 33 - ], - [ - 79, - -47 - ], - [ - 33, - -39 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 16, - -61 - ], - [ - -19, - -32 - ], - [ - 25, - -42 - ], - [ - 17, - -65 - ], - [ - -5, - -41 - ], - [ - 28, - -77 - ] - ], - [ - [ - 15551, - 12321 - ], - [ - 16, - -37 - ], - [ - -2, - -50 - ], - [ - -40, - -29 - ], - [ - 30, - -33 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - -26, - -64 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - 21 - ], - [ - -17, - -8 - ], - [ - -39, - 2 - ], - [ - -1, - 36 - ], - [ - -5, - 34 - ], - [ - 23, - 56 - ], - [ - 25, - 53 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - 30, - -11 - ], - [ - 21, - 30 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - -28, - 67 - ], - [ - -3, - 122 - ], - [ - 12, - 33 - ], - [ - 20, - 36 - ], - [ - 61, - 7 - ], - [ - 25, - 33 - ], - [ - 56, - 34 - ], - [ - -2, - -62 - ], - [ - -21, - -39 - ], - [ - 8, - -33 - ], - [ - 38, - -19 - ], - [ - -17, - -45 - ], - [ - -21, - 13 - ], - [ - -50, - -86 - ], - [ - 19, - -59 - ] - ], - [ - [ - 13432, - 17469 - ], - [ - -42, - -98 - ], - [ - -73, - 68 - ], - [ - -9, - 50 - ], - [ - 102, - 40 - ], - [ - 22, - -60 - ] - ], - [ - [ - 7549, - 13162 - ], - [ - 8, - 21 - ], - [ - 55, - -1 - ], - [ - 41, - -31 - ], - [ - 18, - 3 - ], - [ - 13, - -42 - ], - [ - 38, - 2 - ], - [ - -2, - -36 - ], - [ - 31, - -4 - ], - [ - 34, - -44 - ], - [ - -26, - -49 - ], - [ - -33, - 26 - ], - [ - -32, - -5 - ], - [ - -23, - 6 - ], - [ - -12, - -22 - ], - [ - -27, - -7 - ], - [ - -11, - 29 - ], - [ - -23, - -17 - ], - [ - -28, - -83 - ], - [ - -18, - 20 - ], - [ - -3, - 34 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - 1, - 33 - ], - [ - -18, - 36 - ], - [ - 17, - 20 - ], - [ - 6, - 46 - ], - [ - -6, - 65 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - -77, - -12 - ], - [ - -1, - 77 - ], - [ - -32, - 19 - ], - [ - -44, - 35 - ], - [ - -16, - 56 - ], - [ - -236, - 262 - ], - [ - -235, - 261 - ] - ], - [ - [ - 12204, - 13793 - ], - [ - -262, - 291 - ] - ], - [ - [ - 11942, - 14084 - ], - [ - 1, - 23 - ], - [ - 0, - 8 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - 0, - 142 - ], - [ - 112, - 89 - ], - [ - 70, - 18 - ], - [ - 57, - 32 - ], - [ - 27, - 60 - ], - [ - 81, - 48 - ], - [ - 3, - 89 - ], - [ - 41, - 10 - ], - [ - 31, - 45 - ], - [ - 91, - 20 - ], - [ - 13, - 46 - ], - [ - -18, - 26 - ], - [ - -24, - 127 - ], - [ - -4, - 72 - ], - [ - -27, - 77 - ] - ], - [ - [ - 12396, - 15016 - ], - [ - 67, - 66 - ], - [ - 76, - 21 - ], - [ - 44, - 49 - ], - [ - 67, - 37 - ], - [ - 118, - 21 - ], - [ - 115, - 10 - ], - [ - 35, - -18 - ], - [ - 66, - 47 - ], - [ - 74, - 1 - ], - [ - 29, - -28 - ], - [ - 48, - 8 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - -15, - -62 - ], - [ - 11, - -114 - ], - [ - -16, - -99 - ], - [ - -43, - -67 - ], - [ - 6, - -91 - ], - [ - 57, - -71 - ], - [ - 1, - -29 - ], - [ - 43, - -48 - ], - [ - 29, - -216 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 23, - -106 - ], - [ - 4, - -56 - ], - [ - -12, - -97 - ], - [ - 5, - -55 - ], - [ - -9, - -66 - ], - [ - 6, - -75 - ], - [ - -28, - -50 - ], - [ - 41, - -88 - ], - [ - 3, - -51 - ], - [ - 25, - -67 - ], - [ - 32, - 22 - ], - [ - 55, - -56 - ], - [ - 31, - -75 - ] - ], - [ - [ - 13384, - 13613 - ], - [ - -239, - -229 - ], - [ - -202, - -235 - ], - [ - -98, - -54 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - 10, - -91 - ], - [ - -22, - -78 - ], - [ - -76, - -126 - ], - [ - -83, - -47 - ], - [ - -43, - -104 - ], - [ - -13, - -81 - ], - [ - -40, - -50 - ], - [ - -29, - 61 - ], - [ - -28, - 13 - ], - [ - -29, - -10 - ], - [ - -2, - 44 - ], - [ - 20, - 29 - ], - [ - -8, - 50 - ] - ], - [ - [ - 6950, - 10389 - ], - [ - 37, - 89 - ], - [ - -15, - 53 - ], - [ - -27, - -56 - ], - [ - -42, - 53 - ], - [ - 15, - 33 - ], - [ - -12, - 109 - ], - [ - 24, - 18 - ], - [ - 13, - 75 - ], - [ - 26, - 77 - ], - [ - -4, - 49 - ], - [ - 38, - 26 - ], - [ - 48, - 48 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - -276, - 0 - ], - [ - -271, - 0 - ], - [ - -280, - 0 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - 441 - ], - [ - 0, - 427 - ], - [ - -21, - 97 - ], - [ - 18, - 74 - ], - [ - -11, - 51 - ], - [ - 26, - 58 - ] - ], - [ - [ - 14302, - 14585 - ], - [ - 92, - 1 - ], - [ - 68, - -31 - ], - [ - 69, - -36 - ], - [ - 32, - -18 - ], - [ - 54, - 38 - ], - [ - 28, - 34 - ], - [ - 62, - 10 - ], - [ - 49, - -15 - ], - [ - 19, - -60 - ], - [ - 17, - 39 - ], - [ - 55, - -28 - ], - [ - 55, - -7 - ], - [ - 34, - 31 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 39, - -175 - ], - [ - 7, - -32 - ] - ], - [ - [ - 14982, - 14336 - ], - [ - -20, - -48 - ], - [ - -15, - -90 - ], - [ - -19, - -63 - ], - [ - -16, - -21 - ], - [ - -23, - 39 - ], - [ - -32, - 53 - ], - [ - -49, - 172 - ], - [ - -7, - -10 - ], - [ - 28, - -127 - ], - [ - 43, - -121 - ], - [ - 53, - -187 - ], - [ - 26, - -65 - ], - [ - 22, - -68 - ], - [ - 63, - -132 - ], - [ - -14, - -21 - ], - [ - 2, - -78 - ], - [ - 81, - -108 - ], - [ - 12, - -24 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - -24, - 39 - ], - [ - -29, - 70 - ], - [ - -31, - 39 - ], - [ - -18, - 41 - ], - [ - -60, - 48 - ], - [ - -48, - 2 - ], - [ - -17, - 25 - ], - [ - -41, - -29 - ], - [ - -42, - 55 - ], - [ - -22, - -90 - ], - [ - -81, - 25 - ] - ], - [ - [ - 15087, - 12527 - ], - [ - -7, - 48 - ], - [ - 30, - 177 - ], - [ - 6, - 79 - ], - [ - 22, - 37 - ], - [ - 52, - 20 - ], - [ - 35, - 68 - ] - ], - [ - [ - 15225, - 12956 - ], - [ - 40, - -138 - ], - [ - 20, - -111 - ], - [ - 38, - -58 - ], - [ - 95, - -113 - ], - [ - 39, - -69 - ], - [ - 38, - -69 - ], - [ - 21, - -41 - ], - [ - 35, - -36 - ] - ], - [ - [ - 11918, - 15822 - ], - [ - 3, - 85 - ], - [ - -28, - 52 - ], - [ - 98, - 87 - ], - [ - 86, - -22 - ], - [ - 93, - 1 - ], - [ - 74, - -21 - ], - [ - 58, - 7 - ], - [ - 113, - -4 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 28, - -47 - ], - [ - 128, - -55 - ], - [ - 25, - 26 - ], - [ - 79, - -54 - ], - [ - 81, - 16 - ] - ], - [ - [ - 12756, - 15893 - ], - [ - 3, - -70 - ], - [ - -66, - -80 - ], - [ - -89, - -25 - ], - [ - -6, - -41 - ], - [ - -43, - -66 - ], - [ - -27, - -98 - ], - [ - 27, - -68 - ], - [ - -40, - -54 - ], - [ - -15, - -78 - ], - [ - -53, - -24 - ], - [ - -49, - -92 - ], - [ - -89, - -2 - ], - [ - -66, - 2 - ], - [ - -44, - -42 - ], - [ - -26, - -45 - ], - [ - -34, - 10 - ], - [ - -26, - 40 - ], - [ - -20, - 69 - ], - [ - -65, - 19 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -6, - 39 - ], - [ - 26, - 45 - ], - [ - 10, - 33 - ], - [ - -25, - 36 - ], - [ - 20, - 79 - ], - [ - -28, - 72 - ], - [ - 30, - 9 - ], - [ - 3, - 57 - ], - [ - 11, - 18 - ], - [ - 1, - 93 - ], - [ - 32, - 33 - ], - [ - -19, - 60 - ], - [ - -41, - 4 - ], - [ - -12, - -15 - ], - [ - -41, - 0 - ], - [ - -18, - 59 - ], - [ - -28, - -18 - ], - [ - -25, - -30 - ] - ], - [ - [ - 14242, - 17731 - ], - [ - 8, - 70 - ], - [ - -25, - -15 - ], - [ - -44, - 43 - ], - [ - -7, - 69 - ], - [ - 89, - 33 - ], - [ - 87, - 18 - ], - [ - 76, - -20 - ], - [ - 72, - 3 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 11, - -21 - ], - [ - -50, - -69 - ], - [ - 21, - -112 - ], - [ - -30, - -38 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - -58, - 1 - ], - [ - -60, - 44 - ], - [ - -30, - 15 - ], - [ - -60, - -21 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - -42 - ], - [ - 26, - -66 - ], - [ - 26, - -58 - ], - [ - 26, - -43 - ], - [ - 228, - -142 - ], - [ - 59, - 0 - ] - ], - [ - [ - 15879, - 11757 - ], - [ - -197, - -360 - ], - [ - -91, - -5 - ], - [ - -62, - -85 - ], - [ - -45, - -2 - ], - [ - -19, - -38 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -47, - 0 - ], - [ - -29, - 41 - ], - [ - -63, - -50 - ], - [ - -21, - -50 - ], - [ - -46, - 9 - ], - [ - -16, - 14 - ], - [ - -16, - -3 - ], - [ - -22, - 1 - ], - [ - -88, - 102 - ], - [ - -49, - 0 - ], - [ - -24, - 39 - ], - [ - 0, - 68 - ], - [ - -36, - 20 - ] - ], - [ - [ - 15008, - 11458 - ], - [ - -41, - 130 - ], - [ - -32, - 28 - ], - [ - -12, - 48 - ], - [ - -36, - 59 - ], - [ - -42, - 8 - ], - [ - 23, - 68 - ], - [ - 37, - 3 - ], - [ - 11, - 37 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 108 - ], - [ - 21, - 125 - ], - [ - 33, - 34 - ], - [ - 7, - 49 - ], - [ - 29, - 92 - ], - [ - 42, - 59 - ], - [ - 29, - 118 - ], - [ - 11, - 103 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -24, - 47 - ], - [ - -2, - 184 - ], - [ - -108, - 82 - ], - [ - -93, - 59 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - 41, - 31 - ], - [ - 78, - -63 - ], - [ - 91, - 6 - ], - [ - 75, - -29 - ], - [ - 66, - 53 - ], - [ - 34, - 88 - ], - [ - 109, - 41 - ], - [ - 89, - -48 - ], - [ - -29, - -84 - ] - ], - [ - [ - 14541, - 19083 - ], - [ - -11, - -84 - ], - [ - 107, - -80 - ], - [ - -64, - -91 - ], - [ - 81, - -136 - ], - [ - -47, - -103 - ], - [ - 63, - -89 - ], - [ - -29, - -78 - ], - [ - 103, - -83 - ], - [ - -26, - -61 - ], - [ - -65, - -69 - ], - [ - -149, - -153 - ] - ], - [ - [ - 14504, - 18056 - ], - [ - -126, - -10 - ], - [ - -123, - -44 - ], - [ - -113, - -25 - ], - [ - -41, - 65 - ], - [ - -67, - 40 - ], - [ - 15, - 118 - ], - [ - -33, - 108 - ], - [ - 33, - 70 - ], - [ - 63, - 75 - ], - [ - 159, - 130 - ], - [ - 47, - 26 - ], - [ - -7, - 50 - ], - [ - -97, - 57 - ] - ], - [ - [ - 24982, - 8717 - ], - [ - 24, - -35 - ], - [ - -12, - -62 - ], - [ - -43, - -17 - ], - [ - -39, - 15 - ], - [ - -6, - 53 - ], - [ - 27, - 41 - ], - [ - 31, - -15 - ], - [ - 18, - 20 - ] - ], - [ - [ - 25051, - 8782 - ], - [ - -45, - -26 - ], - [ - -9, - 45 - ], - [ - 35, - 25 - ], - [ - 22, - 6 - ], - [ - 41, - 38 - ], - [ - 0, - -59 - ], - [ - -44, - -29 - ] - ], - [ - [ - 6, - 8817 - ], - [ - -6, - -6 - ], - [ - 0, - 59 - ], - [ - 14, - 5 - ], - [ - -8, - -58 - ] - ], - [ - [ - 8281, - 4577 - ], - [ - 84, - 72 - ], - [ - 59, - -30 - ], - [ - 42, - 48 - ], - [ - 56, - -54 - ], - [ - -21, - -42 - ], - [ - -94, - -36 - ], - [ - -32, - 42 - ], - [ - -59, - -54 - ], - [ - -35, - 54 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - 16, - -10 - ], - [ - 20, - 2 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -3, - -34 - ], - [ - 20, - -45 - ], - [ - -24, - -37 - ], - [ - 18, - -93 - ], - [ - 38, - -15 - ], - [ - -8, - -52 - ] - ], - [ - [ - 13066, - 16039 - ], - [ - -63, - -68 - ], - [ - -138, - 33 - ], - [ - -101, - -39 - ], - [ - -8, - -72 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 36, - 72 - ], - [ - 13, - 239 - ], - [ - -72, - 125 - ], - [ - -51, - 61 - ], - [ - -107, - 46 - ], - [ - -7, - 88 - ], - [ - 91, - 26 - ], - [ - 117, - -31 - ], - [ - -22, - 136 - ], - [ - 66, - -52 - ], - [ - 162, - 94 - ], - [ - 21, - 98 - ], - [ - 61, - 24 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 17, - 51 - ], - [ - 6, - 54 - ], - [ - 12, - 52 - ], - [ - -27, - 71 - ], - [ - -5, - 82 - ], - [ - 36, - 103 - ] - ], - [ - [ - 8786, - 11488 - ], - [ - 24, - -13 - ], - [ - 51, - -29 - ], - [ - 74, - -101 - ], - [ - 12, - -49 - ] - ], - [ - [ - 13214, - 15854 - ], - [ - -23, - -92 - ], - [ - -32, - 24 - ], - [ - -16, - 81 - ], - [ - 14, - 44 - ], - [ - 45, - 46 - ], - [ - 12, - -103 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - -72, - 121 - ], - [ - -46, - 99 - ], - [ - -42, - 124 - ], - [ - 2, - 40 - ], - [ - 15, - 38 - ], - [ - 17, - 87 - ], - [ - 14, - 89 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - 24, - 7 - ], - [ - 101, - -1 - ], - [ - 0, - 144 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - -52, - 24 - ], - [ - -43, - -1 - ], - [ - 14, - 64 - ], - [ - -14, - 64 - ] - ], - [ - [ - 12020, - 17411 - ], - [ - 58, - 5 - ], - [ - 75, - -74 - ], - [ - -38, - -82 - ] - ], - [ - [ - 12338, - 17832 - ], - [ - -74, - -130 - ], - [ - 71, - 16 - ], - [ - 76, - 0 - ], - [ - -18, - -98 - ], - [ - -63, - -108 - ], - [ - 72, - -7 - ], - [ - 6, - -13 - ], - [ - 62, - -142 - ], - [ - 47, - -19 - ], - [ - 43, - -136 - ], - [ - 20, - -48 - ], - [ - 85, - -23 - ], - [ - -9, - -76 - ], - [ - -35, - -36 - ], - [ - 28, - -62 - ], - [ - -63, - -63 - ], - [ - -93, - 2 - ], - [ - -119, - -33 - ], - [ - -33, - 23 - ], - [ - -46, - -56 - ], - [ - -64, - 14 - ], - [ - -49, - -46 - ], - [ - -37, - 24 - ], - [ - 102, - 126 - ], - [ - 62, - 26 - ], - [ - 0, - 0 - ], - [ - -109, - 20 - ], - [ - -20, - 48 - ], - [ - 73, - 37 - ], - [ - -38, - 64 - ], - [ - 13, - 79 - ], - [ - 104, - -11 - ], - [ - 10, - 70 - ], - [ - -46, - 74 - ], - [ - -2, - 1 - ], - [ - -84, - 21 - ], - [ - -17, - 33 - ], - [ - 26, - 53 - ], - [ - -23, - 34 - ], - [ - -38, - -57 - ], - [ - -4, - 115 - ], - [ - -35, - 62 - ], - [ - 25, - 124 - ], - [ - 54, - 97 - ], - [ - 56, - -10 - ], - [ - 84, - 11 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - -68, - 59 - ], - [ - -74, - -6 - ] - ], - [ - [ - 15444, - 15780 - ], - [ - 11, - 51 - ], - [ - -18, - 82 - ], - [ - -40, - 44 - ], - [ - -39, - 14 - ], - [ - -25, - 37 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - 8, - 14 - ], - [ - 59, - -20 - ], - [ - 103, - -20 - ], - [ - 95, - -57 - ], - [ - 12, - -23 - ], - [ - 42, - 19 - ], - [ - 65, - -25 - ], - [ - 21, - -49 - ], - [ - 44, - -28 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -5, - -37 - ], - [ - 29, - -62 - ], - [ - 0, - -87 - ], - [ - 7, - -95 - ], - [ - 17, - -44 - ], - [ - -15, - -108 - ], - [ - 5, - -59 - ], - [ - 19, - -76 - ], - [ - 15, - -43 - ] - ], - [ - [ - 12621, - 11508 - ], - [ - -109, - -70 - ], - [ - -39, - -41 - ], - [ - -62, - -35 - ], - [ - -63, - 34 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - -20, - 3 - ], - [ - -14, - -48 - ], - [ - -19, - 1 - ], - [ - -14, - 25 - ], - [ - 5, - 48 - ], - [ - -30, - 74 - ], - [ - -18, - -14 - ], - [ - -15, - -2 - ] - ], - [ - [ - 11834, - 11806 - ], - [ - -19, - -7 - ], - [ - 1, - 44 - ], - [ - -11, - 31 - ], - [ - 2, - 35 - ], - [ - -15, - 50 - ], - [ - -19, - 43 - ], - [ - -56, - 1 - ], - [ - -16, - -23 - ], - [ - -20, - -3 - ], - [ - -12, - -26 - ], - [ - -8, - -33 - ], - [ - -37, - -53 - ] - ], - [ - [ - 11624, - 11865 - ], - [ - -30, - 71 - ], - [ - -28, - 47 - ], - [ - -17, - 16 - ], - [ - -18, - 24 - ], - [ - -8, - 53 - ], - [ - -10, - 26 - ], - [ - -20, - 20 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - 31, - 58 - ], - [ - 21, - -2 - ], - [ - 18, - 20 - ], - [ - 15, - 0 - ], - [ - 11, - 16 - ], - [ - -5, - 40 - ], - [ - 7, - 12 - ], - [ - 1, - 41 - ] - ], - [ - [ - 11592, - 12307 - ], - [ - 34, - -1 - ], - [ - 50, - -29 - ], - [ - 16, - 2 - ], - [ - 5, - 14 - ], - [ - 38, - -10 - ], - [ - 10, - 7 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 4, - -44 - ], - [ - 11, - 0 - ], - [ - 18, - 16 - ], - [ - 12, - -4 - ], - [ - 19, - -30 - ], - [ - 30, - -10 - ], - [ - 19, - 26 - ], - [ - 23, - 16 - ], - [ - 16, - 17 - ], - [ - 14, - -3 - ], - [ - 16, - -27 - ], - [ - 8, - -33 - ], - [ - 29, - -50 - ], - [ - -15, - -31 - ], - [ - -2, - -39 - ], - [ - 14, - 12 - ], - [ - 9, - -14 - ], - [ - -4, - -36 - ], - [ - 22, - -34 - ] - ], - [ - [ - 11374, - 12375 - ], - [ - 8, - 53 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - 76, - 4 - ], - [ - 16, - 28 - ], - [ - 22, - 2 - ], - [ - 28, - -30 - ], - [ - 21, - 0 - ], - [ - 23, - 20 - ], - [ - 14, - -35 - ], - [ - -30, - -27 - ], - [ - -30, - 3 - ], - [ - -30, - 25 - ], - [ - -26, - -28 - ], - [ - -12, - -1 - ], - [ - -17, - -17 - ], - [ - -63, - 3 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - -37, - 50 - ], - [ - -30, - 8 - ], - [ - -16, - 34 - ], - [ - 1, - 18 - ], - [ - -22, - 25 - ], - [ - -4, - 26 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - 37, - 20 - ], - [ - 23, - -4 - ], - [ - 19, - 13 - ], - [ - 128, - -5 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - -13, - 18 - ], - [ - 24, - 135 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - 45, - 11 - ], - [ - 27, - 26 - ], - [ - 38, - -2 - ], - [ - 11, - 20 - ], - [ - 13, - 4 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - 34, - -32 - ], - [ - -22, - -75 - ], - [ - -16, - -13 - ] - ], - [ - [ - 14364, - 15695 - ], - [ - -43, - 3 - ], - [ - -36, - 12 - ], - [ - -84, - -32 - ], - [ - 48, - -67 - ], - [ - -35, - -20 - ], - [ - -39, - 0 - ], - [ - -37, - 62 - ], - [ - -13, - -26 - ], - [ - 15, - -72 - ], - [ - 35, - -56 - ], - [ - -26, - -27 - ], - [ - 39, - -55 - ], - [ - 34, - -35 - ], - [ - 1, - -67 - ], - [ - -64, - 31 - ], - [ - 20, - -61 - ], - [ - -44, - -12 - ], - [ - 27, - -106 - ], - [ - -47, - -2 - ], - [ - -57, - 52 - ], - [ - -26, - 96 - ], - [ - -12, - 80 - ], - [ - -27, - 55 - ], - [ - -36, - 69 - ], - [ - -5, - 34 - ] - ], - [ - [ - 14200, - 15081 - ], - [ - 38, - -41 - ], - [ - 54, - 7 - ], - [ - 52, - -8 - ], - [ - -2, - -21 - ], - [ - 38, - 14 - ], - [ - -9, - -35 - ], - [ - -100, - -10 - ], - [ - 1, - 19 - ], - [ - -85, - 24 - ], - [ - 13, - 51 - ] - ], - [ - [ - 9288, - 20710 - ], - [ - 234, - 72 - ], - [ - 244, - -6 - ], - [ - 89, - 44 - ], - [ - 247, - 12 - ], - [ - 556, - -15 - ], - [ - 436, - -95 - ], - [ - -128, - -46 - ], - [ - -267, - -6 - ], - [ - -375, - -11 - ], - [ - 35, - -22 - ], - [ - 247, - 13 - ], - [ - 210, - -41 - ], - [ - 135, - 37 - ], - [ - 58, - -43 - ], - [ - -77, - -70 - ], - [ - 178, - 45 - ], - [ - 338, - 46 - ], - [ - 209, - -23 - ], - [ - 39, - -51 - ], - [ - -284, - -86 - ], - [ - -39, - -27 - ], - [ - -223, - -21 - ], - [ - 162, - -6 - ], - [ - -82, - -87 - ], - [ - -56, - -78 - ], - [ - 2, - -134 - ], - [ - 84, - -78 - ], - [ - -109, - -5 - ], - [ - -115, - -38 - ], - [ - 129, - -63 - ], - [ - 16, - -102 - ], - [ - -74, - -11 - ], - [ - 90, - -104 - ], - [ - -155, - -8 - ], - [ - 81, - -49 - ], - [ - -23, - -42 - ], - [ - -98, - -19 - ], - [ - -97, - 0 - ], - [ - 87, - -82 - ], - [ - 1, - -53 - ], - [ - -138, - 50 - ], - [ - -36, - -32 - ], - [ - 94, - -30 - ], - [ - 92, - -74 - ], - [ - 26, - -96 - ], - [ - -124, - -23 - ], - [ - -54, - 46 - ], - [ - -86, - 69 - ], - [ - 24, - -82 - ], - [ - -81, - -63 - ], - [ - 184, - -5 - ], - [ - 96, - -6 - ], - [ - -187, - -105 - ], - [ - -190, - -94 - ], - [ - -204, - -42 - ], - [ - -77, - 0 - ], - [ - -72, - -47 - ], - [ - -97, - -126 - ], - [ - -150, - -84 - ], - [ - -48, - -5 - ], - [ - -93, - -30 - ], - [ - -100, - -28 - ], - [ - -59, - -74 - ], - [ - -1, - -84 - ], - [ - -36, - -79 - ], - [ - -113, - -96 - ], - [ - 28, - -94 - ], - [ - -32, - -99 - ], - [ - -35, - -117 - ], - [ - -99, - -7 - ], - [ - -102, - 98 - ], - [ - -140, - 0 - ], - [ - -67, - 66 - ], - [ - -47, - 117 - ], - [ - -121, - 149 - ], - [ - -35, - 79 - ], - [ - -10, - 107 - ], - [ - -96, - 111 - ], - [ - 25, - 88 - ], - [ - -47, - 43 - ], - [ - 69, - 140 - ], - [ - 105, - 45 - ], - [ - 28, - 50 - ], - [ - 14, - 94 - ], - [ - -79, - -43 - ], - [ - -38, - -18 - ], - [ - -63, - -17 - ], - [ - -85, - 39 - ], - [ - -5, - 82 - ], - [ - 27, - 64 - ], - [ - 65, - 1 - ], - [ - 142, - -32 - ], - [ - -120, - 77 - ], - [ - -62, - 41 - ], - [ - -69, - -17 - ], - [ - -59, - 29 - ], - [ - 78, - 112 - ], - [ - -42, - 45 - ], - [ - -56, - 83 - ], - [ - -83, - 127 - ], - [ - -89, - 47 - ], - [ - 1, - 50 - ], - [ - -187, - 70 - ], - [ - -148, - 9 - ], - [ - -187, - -5 - ], - [ - -170, - -9 - ], - [ - -81, - 38 - ], - [ - -121, - 76 - ], - [ - 183, - 38 - ], - [ - 140, - 6 - ], - [ - -298, - 31 - ], - [ - -157, - 49 - ], - [ - 10, - 47 - ], - [ - 264, - 57 - ], - [ - 255, - 58 - ], - [ - 27, - 44 - ], - [ - -188, - 43 - ], - [ - 60, - 48 - ], - [ - 242, - 83 - ], - [ - 101, - 13 - ], - [ - -29, - 54 - ], - [ - 165, - 32 - ], - [ - 215, - 19 - ], - [ - 214, - 1 - ], - [ - 76, - -38 - ], - [ - 185, - 66 - ], - [ - 166, - -45 - ], - [ - 98, - -9 - ], - [ - 145, - -39 - ], - [ - -166, - 65 - ], - [ - 10, - 51 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - 23, - -22 - ], - [ - 6, - 18 - ], - [ - 20, - -15 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - -31, - -46 - ], - [ - -33, - -33 - ], - [ - -5, - -23 - ], - [ - 5, - -24 - ], - [ - -14, - -30 - ] - ], - [ - [ - 6319, - 12528 - ], - [ - -16, - -8 - ], - [ - 3, - -14 - ], - [ - -13, - -13 - ], - [ - -24, - -30 - ], - [ - -2, - -18 - ] - ], - [ - [ - 6267, - 12445 - ], - [ - -36, - 21 - ], - [ - -43, - 2 - ], - [ - -32, - 24 - ], - [ - -38, - 49 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - 2, - 35 - ], - [ - 8, - 28 - ], - [ - -10, - 23 - ], - [ - 34, - 98 - ], - [ - 89, - 0 - ], - [ - 2, - 41 - ], - [ - -11, - 7 - ], - [ - -8, - 26 - ], - [ - -26, - 28 - ], - [ - -26, - 40 - ], - [ - 32, - 0 - ], - [ - 0, - 68 - ], - [ - 65, - 0 - ], - [ - 64, - -1 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - -47, - 91 - ], - [ - 19, - 33 - ], - [ - -2, - 56 - ], - [ - 43, - 19 - ], - [ - 17, - 22 - ], - [ - -23, - 45 - ], - [ - 6, - 44 - ], - [ - 55, - 70 - ] - ], - [ - [ - 8382, - 11801 - ], - [ - 46, - -44 - ], - [ - 43, - -78 - ], - [ - 2, - -62 - ], - [ - 26, - -3 - ], - [ - 37, - -58 - ], - [ - 28, - -42 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - -11, - -108 - ], - [ - -43, - -31 - ], - [ - 4, - -29 - ], - [ - -13, - -62 - ], - [ - 31, - -87 - ], - [ - 23, - 0 - ], - [ - 9, - -68 - ], - [ - 42, - -104 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - 8, - -5 - ], - [ - 15, - 21 - ], - [ - 20, - 2 - ], - [ - 6, - -10 - ], - [ - 11, - 6 - ], - [ - 33, - -10 - ], - [ - 32, - 3 - ], - [ - 22, - 13 - ], - [ - 8, - 13 - ], - [ - 23, - -6 - ], - [ - 16, - -8 - ], - [ - 19, - 3 - ], - [ - 13, - 10 - ], - [ - 32, - -16 - ], - [ - 11, - -3 - ], - [ - 22, - -23 - ], - [ - 20, - -26 - ], - [ - 25, - -19 - ], - [ - 18, - -33 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -23, - 3 - ], - [ - -10, - -17 - ], - [ - -24, - -15 - ], - [ - -18, - 0 - ], - [ - -15, - -16 - ], - [ - -14, - 6 - ], - [ - -12, - 18 - ], - [ - -7, - -3 - ], - [ - -9, - -29 - ], - [ - -7, - 1 - ], - [ - -1, - -25 - ], - [ - -25, - -33 - ], - [ - -12, - -14 - ], - [ - -8, - -15 - ], - [ - -20, - 24 - ], - [ - -15, - -32 - ], - [ - -15, - 1 - ], - [ - -16, - -3 - ], - [ - 1, - -59 - ], - [ - -10, - -1 - ], - [ - -9, - -27 - ], - [ - -21, - -5 - ] - ], - [ - [ - 6461, - 12355 - ], - [ - -12, - 37 - ], - [ - -21, - 11 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - 4, - 48 - ], - [ - -9, - 13 - ], - [ - -14, - 9 - ], - [ - -31, - -15 - ], - [ - -3, - 16 - ], - [ - -21, - 20 - ], - [ - -15, - 24 - ], - [ - -20, - 10 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -7, - -21 - ] - ], - [ - [ - 13834, - 15893 - ], - [ - -66, - 45 - ], - [ - -40, - 43 - ], - [ - -64, - 36 - ], - [ - -59, - 88 - ], - [ - 14, - 9 - ], - [ - -31, - 50 - ], - [ - -2, - 41 - ], - [ - -45, - 19 - ], - [ - -21, - -52 - ], - [ - -20, - 40 - ], - [ - 1, - 42 - ], - [ - 3, - 2 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 48, - -4 - ], - [ - 13, - 20 - ], - [ - 24, - -20 - ], - [ - 27, - -2 - ], - [ - 0, - 34 - ], - [ - 24, - 12 - ], - [ - 7, - 48 - ], - [ - 55, - 32 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - 22, - -15 - ], - [ - 52, - -51 - ], - [ - 58, - -23 - ], - [ - 26, - 18 - ] - ], - [ - [ - 13860, - 16305 - ], - [ - 17, - -47 - ], - [ - 22, - -34 - ], - [ - -27, - -45 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - -46, - 20 - ], - [ - -33, - -8 - ], - [ - -43, - 9 - ], - [ - -33, - -23 - ], - [ - -37, - 38 - ], - [ - 6, - 38 - ], - [ - 64, - -16 - ], - [ - 53, - -10 - ], - [ - 25, - 27 - ], - [ - -32, - 52 - ], - [ - 1, - 46 - ], - [ - -44, - 18 - ], - [ - 16, - 33 - ], - [ - 42, - -5 - ], - [ - 61, - -19 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - 36, - -31 - ], - [ - 25, - -13 - ], - [ - 59, - 14 - ], - [ - 5, - 25 - ], - [ - 28, - 3 - ], - [ - 34, - 19 - ], - [ - 8, - -8 - ], - [ - 32, - 15 - ], - [ - 17, - 28 - ], - [ - 23, - 8 - ], - [ - 74, - -37 - ], - [ - 15, - 12 - ] - ], - [ - [ - 14087, - 16606 - ], - [ - 39, - -32 - ], - [ - 5, - -32 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - -43, - -26 - ], - [ - -33, - -81 - ], - [ - -42, - -81 - ], - [ - -56, - -23 - ] - ], - [ - [ - 13957, - 16331 - ], - [ - -43, - 5 - ], - [ - -54, - -31 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - -13, - 41 - ], - [ - -12, - 1 - ] - ], - [ - [ - 20962, - 9569 - ], - [ - -29, - -3 - ], - [ - -92, - 85 - ], - [ - 65, - 23 - ], - [ - 36, - -36 - ], - [ - 25, - -37 - ], - [ - -5, - -32 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 7, - -23 - ], - [ - 1, - -37 - ] - ], - [ - [ - 21267, - 9670 - ], - [ - -45, - -89 - ], - [ - -60, - -27 - ], - [ - -8, - 15 - ], - [ - 6, - 40 - ], - [ - 30, - 74 - ], - [ - 69, - 47 - ] - ], - [ - [ - 20766, - 9826 - ], - [ - 25, - -32 - ], - [ - 43, - 10 - ], - [ - 18, - -51 - ], - [ - -81, - -24 - ], - [ - -48, - -16 - ], - [ - -38, - 1 - ], - [ - 24, - 69 - ], - [ - 38, - 1 - ], - [ - 19, - 42 - ] - ], - [ - [ - 21115, - 9826 - ], - [ - -10, - -67 - ], - [ - -105, - -34 - ], - [ - -93, - 15 - ], - [ - 0, - 44 - ], - [ - 55, - 25 - ], - [ - 44, - -36 - ], - [ - 46, - 9 - ], - [ - 63, - 44 - ] - ], - [ - [ - 20119, - 9984 - ], - [ - 134, - -12 - ], - [ - 15, - 50 - ], - [ - 130, - -58 - ], - [ - 25, - -78 - ], - [ - 105, - -22 - ], - [ - 85, - -71 - ], - [ - -79, - -46 - ], - [ - -77, - 49 - ], - [ - -63, - -4 - ], - [ - -72, - 9 - ], - [ - -66, - 22 - ], - [ - -80, - 46 - ], - [ - -52, - 11 - ], - [ - -29, - -15 - ], - [ - -127, - 50 - ], - [ - -12, - 51 - ], - [ - -64, - 9 - ], - [ - 48, - 115 - ], - [ - 85, - -7 - ], - [ - 56, - -47 - ], - [ - 29, - -9 - ], - [ - 9, - -43 - ] - ], - [ - [ - 21939, - 10052 - ], - [ - -36, - -82 - ], - [ - -7, - 90 - ], - [ - 13, - 43 - ], - [ - 14, - 41 - ], - [ - 16, - -35 - ], - [ - 0, - -57 - ] - ], - [ - [ - 21418, - 10382 - ], - [ - -26, - -40 - ], - [ - -48, - 22 - ], - [ - -14, - 52 - ], - [ - 71, - 6 - ], - [ - 17, - -40 - ] - ], - [ - [ - 21642, - 10426 - ], - [ - 26, - -92 - ], - [ - -59, - 50 - ], - [ - -58, - 10 - ], - [ - -40, - -8 - ], - [ - -48, - 4 - ], - [ - 17, - 66 - ], - [ - 86, - 5 - ], - [ - 76, - -35 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 2, - -391 - ], - [ - 1, - -391 - ] - ], - [ - [ - 22379, - 9703 - ], - [ - -62, - 99 - ], - [ - -71, - 24 - ], - [ - -17, - -34 - ], - [ - -89, - -4 - ], - [ - 30, - 98 - ], - [ - 44, - 33 - ], - [ - -18, - 130 - ], - [ - -34, - 101 - ], - [ - -135, - 102 - ], - [ - -57, - 10 - ], - [ - -105, - 111 - ], - [ - -21, - -59 - ], - [ - -26, - -10 - ], - [ - -16, - 44 - ], - [ - 0, - 52 - ], - [ - -54, - 59 - ], - [ - 75, - 43 - ], - [ - 50, - -2 - ], - [ - -6, - 32 - ], - [ - -102, - 0 - ], - [ - -27, - 71 - ], - [ - -63, - 22 - ], - [ - -29, - 60 - ], - [ - 94, - 29 - ], - [ - 35, - 39 - ], - [ - 112, - -49 - ], - [ - 11, - -45 - ], - [ - 20, - -194 - ], - [ - 72, - -72 - ], - [ - 58, - 127 - ], - [ - 80, - 73 - ], - [ - 62, - 0 - ], - [ - 60, - -42 - ], - [ - 52, - -43 - ], - [ - 74, - -23 - ] - ], - [ - [ - 21278, - 10968 - ], - [ - -56, - -119 - ], - [ - -53, - -24 - ], - [ - -67, - 24 - ], - [ - -116, - -6 - ], - [ - -61, - -17 - ], - [ - -10, - -91 - ], - [ - 63, - -107 - ], - [ - 37, - 55 - ], - [ - 130, - 40 - ], - [ - -5, - -55 - ], - [ - -31, - 18 - ], - [ - -30, - -71 - ], - [ - -61, - -46 - ], - [ - 66, - -154 - ], - [ - -13, - -41 - ], - [ - 63, - -139 - ], - [ - -1, - -79 - ], - [ - -37, - -35 - ], - [ - -28, - 42 - ], - [ - 34, - 99 - ], - [ - -68, - -47 - ], - [ - -18, - 33 - ], - [ - 9, - 47 - ], - [ - -50, - 70 - ], - [ - 5, - 117 - ], - [ - -46, - -37 - ], - [ - 6, - -139 - ], - [ - 3, - -172 - ], - [ - -45, - -17 - ], - [ - -30, - 35 - ], - [ - 20, - 110 - ], - [ - -10, - 116 - ], - [ - -30, - 1 - ], - [ - -21, - 82 - ], - [ - 28, - 79 - ], - [ - 10, - 95 - ], - [ - 35, - 181 - ], - [ - 15, - 49 - ], - [ - 59, - 89 - ], - [ - 55, - -35 - ], - [ - 88, - -17 - ], - [ - 80, - 5 - ], - [ - 69, - 87 - ], - [ - 12, - -26 - ] - ], - [ - [ - 21518, - 10933 - ], - [ - -4, - -105 - ], - [ - -35, - 12 - ], - [ - -11, - -73 - ], - [ - 29, - -63 - ], - [ - -20, - -15 - ], - [ - -28, - 76 - ], - [ - -21, - 154 - ], - [ - 14, - 95 - ], - [ - 23, - 44 - ], - [ - 5, - -65 - ], - [ - 42, - -11 - ], - [ - 6, - -49 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 12, - -80 - ], - [ - 47, - -68 - ], - [ - 45, - 24 - ], - [ - 45, - -8 - ], - [ - 40, - 60 - ], - [ - 34, - 11 - ], - [ - 66, - -34 - ], - [ - 57, - 26 - ], - [ - 35, - 167 - ], - [ - 27, - 41 - ], - [ - 24, - 137 - ], - [ - 80, - 0 - ], - [ - 61, - -20 - ] - ], - [ - [ - 20765, - 11294 - ], - [ - -40, - -109 - ], - [ - 51, - -113 - ], - [ - -12, - -56 - ], - [ - 79, - -111 - ], - [ - -83, - -14 - ], - [ - -23, - -82 - ], - [ - 3, - -108 - ], - [ - -67, - -82 - ], - [ - -2, - -120 - ], - [ - -27, - -183 - ], - [ - -10, - 42 - ], - [ - -79, - -54 - ], - [ - -28, - 74 - ], - [ - -50, - 7 - ], - [ - -35, - 38 - ], - [ - -82, - -43 - ], - [ - -26, - 58 - ], - [ - -46, - -7 - ], - [ - -57, - 14 - ], - [ - -11, - 161 - ], - [ - -34, - 33 - ], - [ - -34, - 103 - ], - [ - -10, - 105 - ], - [ - 9, - 111 - ], - [ - 41, - 80 - ] - ], - [ - [ - 19924, - 10095 - ], - [ - -77, - -2 - ], - [ - -59, - 100 - ], - [ - -90, - 98 - ], - [ - -29, - 73 - ], - [ - -53, - 97 - ], - [ - -35, - 90 - ], - [ - -53, - 168 - ], - [ - -61, - 100 - ], - [ - -20, - 103 - ], - [ - -26, - 94 - ], - [ - -63, - 75 - ], - [ - -36, - 103 - ], - [ - -53, - 67 - ], - [ - -73, - 133 - ], - [ - -6, - 61 - ], - [ - 45, - -5 - ], - [ - 108, - -23 - ], - [ - 62, - -118 - ], - [ - 54, - -81 - ], - [ - 38, - -50 - ], - [ - 66, - -129 - ], - [ - 71, - -2 - ], - [ - 58, - -82 - ], - [ - 41, - -100 - ], - [ - 53, - -55 - ], - [ - -28, - -98 - ], - [ - 40, - -42 - ], - [ - 25, - -3 - ], - [ - 12, - -84 - ], - [ - 24, - -67 - ], - [ - 51, - -10 - ], - [ - 34, - -76 - ], - [ - -17, - -149 - ], - [ - -3, - -186 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -10, - -44 - ], - [ - -48, - 2 - ], - [ - -86, - -25 - ], - [ - 4, - -90 - ], - [ - -37, - -71 - ], - [ - -100, - -81 - ], - [ - -78, - -141 - ], - [ - -53, - -76 - ], - [ - -69, - -78 - ], - [ - 0, - -56 - ], - [ - -35, - -29 - ], - [ - -63, - -43 - ], - [ - -32, - -6 - ], - [ - -21, - -92 - ], - [ - 14, - -156 - ], - [ - 4, - -99 - ], - [ - -29, - -114 - ], - [ - -1, - -204 - ], - [ - -36, - -6 - ], - [ - -32, - -92 - ], - [ - 22, - -39 - ], - [ - -64, - -34 - ], - [ - -23, - -82 - ], - [ - -28, - -34 - ], - [ - -66, - 112 - ], - [ - -33, - 168 - ], - [ - -26, - 121 - ], - [ - -25, - 57 - ], - [ - -37, - 115 - ], - [ - -17, - 150 - ], - [ - -12, - 75 - ], - [ - -64, - 165 - ], - [ - -28, - 232 - ], - [ - -21, - 154 - ], - [ - 0, - 145 - ], - [ - -14, - 112 - ], - [ - -101, - -72 - ], - [ - -49, - 15 - ], - [ - -91, - 145 - ], - [ - 33, - 44 - ], - [ - -20, - 47 - ], - [ - -82, - 101 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - 46, - 81 - ], - [ - 154, - -1 - ], - [ - -14, - 103 - ], - [ - -39, - 61 - ], - [ - -8, - 92 - ], - [ - -46, - 54 - ], - [ - 77, - 126 - ], - [ - 81, - -9 - ], - [ - 73, - 126 - ], - [ - 44, - 121 - ], - [ - 67, - 121 - ], - [ - -1, - 85 - ], - [ - 60, - 70 - ], - [ - -57, - 59 - ], - [ - -24, - 81 - ], - [ - -25, - 105 - ], - [ - 35, - 52 - ], - [ - 105, - -29 - ], - [ - 78, - 18 - ], - [ - 67, - 100 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -45, - -54 - ], - [ - -27, - -112 - ], - [ - 68, - -46 - ], - [ - 66, - -59 - ], - [ - 91, - -67 - ], - [ - 95, - -15 - ], - [ - 40, - -61 - ], - [ - 54, - -12 - ], - [ - 84, - -28 - ], - [ - 58, - 2 - ], - [ - 8, - 48 - ], - [ - -9, - 76 - ], - [ - 5, - 52 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - 5, - -46 - ], - [ - -24, - -22 - ], - [ - 6, - -74 - ], - [ - -50, - 22 - ], - [ - -91, - -83 - ], - [ - 3, - -68 - ], - [ - -39, - -101 - ], - [ - -3, - -59 - ], - [ - -31, - -98 - ], - [ - -55, - 27 - ], - [ - -3, - -124 - ], - [ - -15, - -41 - ], - [ - 7, - -51 - ], - [ - -34, - -29 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - 12, - -86 - ], - [ - -53, - -107 - ], - [ - -123, - -71 - ], - [ - -99, - 18 - ], - [ - 57, - 125 - ], - [ - -37, - 122 - ], - [ - 95, - 94 - ], - [ - 53, - 56 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 34, - -63 - ], - [ - 29, - -73 - ], - [ - 66, - -53 - ], - [ - 2, - -105 - ], - [ - 33, - -20 - ], - [ - 6, - -55 - ], - [ - -100, - -62 - ], - [ - -27, - -139 - ] - ], - [ - [ - 16834, - 13806 - ], - [ - -131, - 36 - ], - [ - -76, - 28 - ], - [ - -78, - 15 - ], - [ - -30, - 147 - ], - [ - -34, - 22 - ], - [ - -53, - -22 - ], - [ - -70, - -58 - ], - [ - -86, - 40 - ], - [ - -70, - 92 - ], - [ - -67, - 34 - ], - [ - -47, - 114 - ], - [ - -51, - 160 - ], - [ - -38, - -19 - ], - [ - -44, - 39 - ], - [ - -26, - -47 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -38, - 64 - ], - [ - -1, - 63 - ], - [ - -22, - 0 - ], - [ - 11, - 87 - ], - [ - -36, - 91 - ], - [ - -85, - 66 - ], - [ - -49, - 114 - ], - [ - 17, - 94 - ], - [ - 35, - 41 - ], - [ - -6, - 70 - ], - [ - -45, - 36 - ], - [ - -45, - 143 - ] - ], - [ - [ - 15669, - 15256 - ], - [ - -39, - 97 - ], - [ - 14, - 37 - ], - [ - -22, - 137 - ], - [ - 48, - 35 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - 22, - -88 - ], - [ - 66, - -25 - ], - [ - 49, - -60 - ], - [ - 99, - -21 - ], - [ - 109, - 32 - ], - [ - 6, - 28 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - 62, - 23 - ], - [ - 49, - 69 - ], - [ - 47, - -4 - ], - [ - 30, - 23 - ], - [ - 50, - -11 - ], - [ - 77, - -61 - ], - [ - 56, - -13 - ], - [ - 79, - -107 - ], - [ - 52, - -4 - ], - [ - 6, - -101 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -41, - 6 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - -47, - 10 - ], - [ - -51, - -115 - ] - ], - [ - [ - 15794, - 14288 - ], - [ - -130, - 10 - ], - [ - -196, - 241 - ], - [ - -104, - 84 - ], - [ - -84, - 33 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -28, - 146 - ] - ], - [ - [ - 15252, - 14802 - ], - [ - 154, - 124 - ], - [ - 26, - 145 - ], - [ - -6, - 88 - ], - [ - 38, - 30 - ], - [ - 36, - 75 - ] - ], - [ - [ - 15500, - 15264 - ], - [ - 30, - 18 - ], - [ - 81, - -15 - ], - [ - 24, - -31 - ], - [ - 34, - 20 - ] - ], - [ - [ - 11536, - 18770 - ], - [ - -16, - -78 - ], - [ - 79, - -82 - ], - [ - -91, - -91 - ], - [ - -201, - -82 - ], - [ - -60, - -22 - ], - [ - -92, - 17 - ], - [ - -194, - 38 - ], - [ - 68, - 53 - ], - [ - -151, - 59 - ], - [ - 123, - 23 - ], - [ - -3, - 36 - ], - [ - -146, - 27 - ], - [ - 47, - 79 - ], - [ - 106, - 17 - ], - [ - 108, - -81 - ], - [ - 106, - 65 - ], - [ - 88, - -34 - ], - [ - 113, - 64 - ], - [ - 116, - -8 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 20, - 39 - ], - [ - -4, - 7 - ], - [ - 18, - 56 - ], - [ - 14, - 90 - ], - [ - 10, - 31 - ], - [ - 2, - 1 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 23, - 0 - ], - [ - 7, - 21 - ], - [ - 19, - 1 - ] - ], - [ - [ - 15045, - 14789 - ], - [ - 1, - -49 - ], - [ - -10, - -18 - ], - [ - 1, - -1 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - -12, - -38 - ] - ], - [ - [ - 15025, - 14683 - ], - [ - -25, - 17 - ], - [ - -14, - -80 - ], - [ - 17, - -13 - ], - [ - -18, - -17 - ], - [ - -3, - -31 - ], - [ - 33, - 16 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 2, - -47 - ], - [ - -35, - -192 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -8, - -59 - ], - [ - 17, - -51 - ] - ], - [ - [ - 13519, - 16267 - ], - [ - -55, - 17 - ], - [ - -57, - -42 - ], - [ - 4, - -60 - ], - [ - -9, - -34 - ], - [ - 23, - -61 - ], - [ - 65, - -61 - ], - [ - 35, - -99 - ], - [ - 78, - -96 - ], - [ - 55, - 0 - ], - [ - 17, - -26 - ], - [ - -20, - -24 - ], - [ - 63, - -44 - ], - [ - 51, - -36 - ], - [ - 60, - -62 - ], - [ - 7, - -23 - ], - [ - -13, - -43 - ], - [ - -39, - 56 - ], - [ - -61, - 20 - ], - [ - -29, - -78 - ], - [ - 50, - -44 - ], - [ - -8, - -63 - ], - [ - -29, - -7 - ], - [ - -37, - -103 - ], - [ - -29, - -9 - ], - [ - 0, - 37 - ], - [ - 14, - 64 - ], - [ - 15, - 26 - ], - [ - -27, - 69 - ], - [ - -21, - 61 - ], - [ - -29, - 15 - ], - [ - -21, - 51 - ], - [ - -44, - 22 - ], - [ - -31, - 49 - ], - [ - -51, - 7 - ], - [ - -55, - 54 - ], - [ - -63, - 79 - ], - [ - -48, - 69 - ], - [ - -21, - 118 - ], - [ - -35, - 14 - ], - [ - -57, - 40 - ], - [ - -32, - -16 - ], - [ - -40, - -56 - ], - [ - -29, - -9 - ] - ], - [ - [ - 13629, - 15384 - ], - [ - -25, - -95 - ], - [ - 11, - -37 - ], - [ - -15, - -62 - ], - [ - -53, - 46 - ], - [ - -36, - 13 - ], - [ - -97, - 61 - ], - [ - 10, - 61 - ], - [ - 81, - -11 - ], - [ - 71, - 13 - ], - [ - 53, - 11 - ] - ], - [ - [ - 13190, - 15741 - ], - [ - 41, - -85 - ], - [ - -9, - -159 - ], - [ - -32, - 8 - ], - [ - -29, - -40 - ], - [ - -26, - 32 - ], - [ - -3, - 144 - ], - [ - -16, - 69 - ], - [ - 39, - -6 - ], - [ - 35, - 37 - ] - ], - [ - [ - 7140, - 13015 - ], - [ - 47, - -10 - ], - [ - 37, - -29 - ], - [ - 12, - -33 - ], - [ - -49, - -2 - ], - [ - -21, - -20 - ], - [ - -39, - 19 - ], - [ - -40, - 44 - ], - [ - 8, - 27 - ], - [ - 29, - 9 - ], - [ - 16, - -5 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -14, - -19 - ], - [ - -139, - -60 - ], - [ - 69, - -120 - ], - [ - -23, - -20 - ], - [ - -11, - -40 - ], - [ - -53, - -17 - ], - [ - -17, - -43 - ], - [ - -30, - -37 - ], - [ - -78, - 19 - ] - ], - [ - [ - 14984, - 14319 - ], - [ - -2, - 17 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 10, - 35 - ], - [ - 0, - 73 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - 78, - -47 - ], - [ - 137, - 128 - ] - ], - [ - [ - 21933, - 14894 - ], - [ - 9, - -41 - ], - [ - -39, - -73 - ], - [ - -29, - 39 - ], - [ - -36, - -28 - ], - [ - -18, - -70 - ], - [ - -46, - 34 - ], - [ - 1, - 57 - ], - [ - 38, - 71 - ], - [ - 40, - -14 - ], - [ - 29, - 51 - ], - [ - 51, - -26 - ] - ], - [ - [ - 22375, - 15253 - ], - [ - -27, - -96 - ], - [ - 13, - -60 - ], - [ - -37, - -84 - ], - [ - -89, - -57 - ], - [ - -122, - -7 - ], - [ - -100, - -137 - ], - [ - -46, - 46 - ], - [ - -3, - 90 - ], - [ - -122, - -27 - ], - [ - -82, - -56 - ], - [ - -82, - -3 - ], - [ - 71, - -88 - ], - [ - -47, - -204 - ], - [ - -45, - -50 - ], - [ - -33, - 46 - ], - [ - 17, - 109 - ], - [ - -44, - 34 - ], - [ - -29, - 83 - ], - [ - 66, - 37 - ], - [ - 37, - 75 - ], - [ - 70, - 62 - ], - [ - 51, - 82 - ], - [ - 139, - 36 - ], - [ - 74, - -25 - ], - [ - 73, - 214 - ], - [ - 47, - -58 - ], - [ - 102, - 120 - ], - [ - 40, - 47 - ], - [ - 43, - 147 - ], - [ - -11, - 135 - ], - [ - 29, - 75 - ], - [ - 74, - 22 - ], - [ - 38, - -166 - ], - [ - -2, - -97 - ], - [ - -64, - -121 - ], - [ - 1, - -124 - ] - ], - [ - [ - 22579, - 16097 - ], - [ - 49, - -26 - ], - [ - 50, - 51 - ], - [ - 15, - -135 - ], - [ - -103, - -33 - ], - [ - -61, - -119 - ], - [ - -110, - 82 - ], - [ - -38, - -131 - ], - [ - -77, - -2 - ], - [ - -10, - 120 - ], - [ - 34, - 92 - ], - [ - 75, - 6 - ], - [ - 20, - 166 - ], - [ - 21, - 94 - ], - [ - 82, - -125 - ], - [ - 53, - -40 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -43, - 17 - ], - [ - -35, - 44 - ], - [ - -103, - 12 - ], - [ - -116, - 3 - ], - [ - -25, - -13 - ], - [ - -99, - 51 - ], - [ - -40, - -25 - ], - [ - -11, - -71 - ], - [ - -114, - 41 - ], - [ - -46, - -17 - ], - [ - -16, - -52 - ] - ], - [ - [ - 17494, - 15868 - ], - [ - -40, - -22 - ], - [ - -92, - -84 - ], - [ - -30, - -86 - ], - [ - -26, - -1 - ], - [ - -19, - 57 - ], - [ - -89, - 4 - ], - [ - -14, - 98 - ], - [ - -34, - 1 - ], - [ - 5, - 121 - ], - [ - -83, - 87 - ], - [ - -120, - -9 - ], - [ - -82, - -18 - ], - [ - -66, - 109 - ], - [ - -57, - 45 - ], - [ - -108, - 86 - ], - [ - -13, - 10 - ], - [ - -180, - -71 - ], - [ - 3, - -442 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - -36, - -6 - ], - [ - -49, - 94 - ], - [ - -47, - 34 - ], - [ - -79, - -25 - ], - [ - -31, - -40 - ] - ], - [ - [ - 16207, - 15810 - ], - [ - -4, - 29 - ], - [ - 18, - 50 - ], - [ - -14, - 42 - ], - [ - -81, - 41 - ], - [ - -31, - 108 - ], - [ - -38, - 30 - ], - [ - -3, - 39 - ], - [ - 68, - -11 - ], - [ - 3, - 87 - ], - [ - 59, - 20 - ], - [ - 61, - -18 - ], - [ - 12, - 117 - ], - [ - -12, - 74 - ], - [ - -70, - -6 - ], - [ - -59, - 30 - ], - [ - -81, - -53 - ], - [ - -65, - -25 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -35, - 19 - ], - [ - 7, - 62 - ], - [ - -45, - 80 - ], - [ - -51, - -3 - ], - [ - -59, - 81 - ], - [ - 40, - 91 - ], - [ - -21, - 24 - ], - [ - 56, - 132 - ], - [ - 72, - -69 - ], - [ - 8, - 87 - ], - [ - 144, - 131 - ], - [ - 109, - 3 - ], - [ - 154, - -83 - ], - [ - 82, - -49 - ], - [ - 74, - 51 - ], - [ - 111, - 2 - ], - [ - 89, - -62 - ], - [ - 20, - 36 - ], - [ - 98, - -6 - ], - [ - 18, - 57 - ], - [ - -113, - 83 - ], - [ - 67, - 58 - ], - [ - -13, - 33 - ], - [ - 67, - 31 - ], - [ - -51, - 82 - ], - [ - 32, - 41 - ], - [ - 261, - 42 - ], - [ - 34, - 30 - ], - [ - 174, - 44 - ], - [ - 63, - 50 - ], - [ - 125, - -26 - ], - [ - 22, - -125 - ], - [ - 73, - 30 - ], - [ - 90, - -41 - ], - [ - -6, - -66 - ], - [ - 67, - 7 - ], - [ - 174, - 113 - ], - [ - -25, - -37 - ], - [ - 89, - -93 - ], - [ - 156, - -305 - ], - [ - 37, - 63 - ], - [ - 96, - -69 - ], - [ - 100, - 31 - ], - [ - 38, - -22 - ], - [ - 34, - -69 - ], - [ - 49, - -23 - ], - [ - 29, - -51 - ], - [ - 90, - 16 - ], - [ - 37, - -74 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -61, - -136 - ], - [ - 1, - -437 - ], - [ - 41, - -99 - ] - ], - [ - [ - 15446, - 10595 - ], - [ - -48, - -48 - ], - [ - -18, - -50 - ], - [ - -26, - -8 - ], - [ - -10, - -85 - ], - [ - -22, - -48 - ], - [ - -14, - -80 - ], - [ - -28, - -40 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -100, - 120 - ], - [ - -5, - 70 - ], - [ - -252, - 244 - ], - [ - -12, - 13 - ] - ], - [ - [ - 14911, - 10683 - ], - [ - -1, - 127 - ], - [ - 20, - 49 - ], - [ - 34, - 79 - ], - [ - 26, - 88 - ], - [ - -31, - 138 - ], - [ - -8, - 60 - ], - [ - -33, - 83 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - 43, - 72 - ], - [ - 47, - 79 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - -132, - -18 - ], - [ - -86, - 38 - ], - [ - -75, - -9 - ], - [ - 6, - 69 - ], - [ - 76, - -20 - ], - [ - 26, - 37 - ] - ], - [ - [ - 17498, - 15625 - ], - [ - 53, - -12 - ], - [ - 89, - 87 - ], - [ - -83, - 63 - ], - [ - -49, - -30 - ], - [ - -52, - 45 - ], - [ - 59, - 78 - ], - [ - -21, - 12 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -17, - 145 - ], - [ - 45, - 100 - ], - [ - 90, - 23 - ], - [ - 65, - -17 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 58, - -48 - ], - [ - 31, - 83 - ], - [ - 62, - -44 - ] - ], - [ - [ - 20033, - 12501 - ], - [ - 16, - -80 - ], - [ - -8, - -144 - ], - [ - -118, - -92 - ], - [ - 31, - -73 - ], - [ - -73, - -8 - ], - [ - -61, - -49 - ] - ], - [ - [ - 19820, - 12055 - ], - [ - -58, - 18 - ], - [ - -28, - 62 - ], - [ - -35, - 124 - ] - ], - [ - [ - 21495, - 15429 - ], - [ - 60, - -141 - ], - [ - 17, - -78 - ], - [ - 1, - -138 - ], - [ - -27, - -66 - ], - [ - -63, - -23 - ], - [ - -56, - -50 - ], - [ - -62, - -10 - ], - [ - -8, - 65 - ], - [ - 13, - 90 - ], - [ - -31, - 125 - ], - [ - 52, - 20 - ], - [ - -48, - 103 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - 4, - 11 - ], - [ - 31, - -4 - ], - [ - 28, - 54 - ], - [ - 49, - 6 - ], - [ - 30, - 7 - ], - [ - 10, - 29 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 13, - 26 - ] - ], - [ - [ - 13960, - 15933 - ], - [ - 16, - 9 - ], - [ - 10, - 40 - ], - [ - 12, - 6 - ], - [ - 10, - -16 - ], - [ - 13, - -8 - ], - [ - 9, - -19 - ], - [ - 12, - -6 - ], - [ - 14, - -22 - ], - [ - 9, - 1 - ], - [ - -7, - -29 - ], - [ - -9, - -15 - ], - [ - 3, - -9 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - -16, - -4 - ], - [ - -41, - -19 - ], - [ - -3, - -24 - ], - [ - -9, - 1 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - 14, - -53 - ], - [ - -6, - -27 - ], - [ - 23, - -90 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - -50, - -4 - ], - [ - -17, - 58 - ], - [ - -62, - 11 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - 40, - -94 - ], - [ - 32, - -109 - ], - [ - 85, - -1 - ], - [ - 28, - -105 - ], - [ - -45, - -31 - ], - [ - -20, - -44 - ], - [ - 83, - -71 - ], - [ - 58, - -142 - ], - [ - 44, - -106 - ], - [ - 53, - -83 - ], - [ - 18, - -85 - ], - [ - -13, - -120 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 23, - 54 - ], - [ - 3, - 101 - ], - [ - -57, - 105 - ], - [ - -4, - 118 - ], - [ - -53, - 98 - ], - [ - -53, - 8 - ], - [ - -14, - -42 - ], - [ - -40, - -3 - ], - [ - -21, - 21 - ], - [ - -74, - -72 - ], - [ - -1, - 108 - ], - [ - 17, - 126 - ], - [ - -47, - 6 - ], - [ - -4, - 72 - ], - [ - -31, - 37 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - 15, - 44 - ], - [ - 60, - 78 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 25, - 98 - ], - [ - 35, - 84 - ], - [ - 1, - 5 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - 31, - -7 - ], - [ - 12, - -47 - ], - [ - -38, - -45 - ], - [ - -17, - -66 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - -18, - -1 - ], - [ - -72, - 57 - ], - [ - -64, - 91 - ], - [ - -59, - 66 - ], - [ - -47, - 77 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - 17, - 39 - ], - [ - 3, - 35 - ], - [ - 32, - 65 - ], - [ - 32, - 56 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 34, - 28 - ], - [ - 7, - 51 - ], - [ - -8, - 49 - ], - [ - 48, - 47 - ], - [ - 21, - 38 - ], - [ - 34, - 34 - ], - [ - 4, - 93 - ] - ], - [ - [ - 13348, - 14773 - ], - [ - 82, - -42 - ], - [ - 30, - 11 - ], - [ - 58, - -20 - ], - [ - 92, - -54 - ], - [ - 33, - -107 - ], - [ - 62, - -23 - ], - [ - 99, - -50 - ], - [ - 74, - -60 - ], - [ - 34, - 31 - ], - [ - 33, - 56 - ], - [ - -16, - 91 - ], - [ - 22, - 59 - ], - [ - 50, - 56 - ], - [ - 48, - 16 - ], - [ - 95, - -24 - ], - [ - 23, - -54 - ], - [ - 26, - 0 - ], - [ - 22, - -21 - ], - [ - 70, - -14 - ], - [ - 17, - -39 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - -240 - ], - [ - -80, - 0 - ], - [ - -1, - -51 - ] - ], - [ - [ - 14209, - 13146 - ], - [ - -278, - 230 - ], - [ - -278, - 230 - ], - [ - -70, - -66 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - -50, - -45 - ], - [ - -39, - 66 - ], - [ - -110, - 52 - ] - ], - [ - [ - 18249, - 11700 - ], - [ - -11, - -125 - ], - [ - -29, - -34 - ], - [ - -61, - -28 - ], - [ - -33, - 96 - ], - [ - -12, - 172 - ], - [ - 31, - 195 - ], - [ - 49, - -67 - ], - [ - 32, - -84 - ], - [ - 34, - -125 - ] - ], - [ - [ - 14568, - 7323 - ], - [ - 24, - -36 - ], - [ - -22, - -58 - ], - [ - -12, - -39 - ], - [ - -38, - -19 - ], - [ - -13, - -38 - ], - [ - -25, - -12 - ], - [ - -52, - 92 - ], - [ - 37, - 76 - ], - [ - 38, - 47 - ], - [ - 32, - 24 - ], - [ - 31, - -37 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - -17, - 37 - ], - [ - -36, - 13 - ] - ], - [ - [ - 14132, - 17315 - ], - [ - -6, - 30 - ], - [ - 8, - 33 - ], - [ - -31, - 19 - ], - [ - -73, - 21 - ] - ], - [ - [ - 14030, - 17418 - ], - [ - -15, - 101 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 80, - 37 - ], - [ - 117, - -8 - ], - [ - 68, - 12 - ], - [ - 10, - -25 - ], - [ - 37, - -8 - ], - [ - 67, - -58 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 3, - 90 - ], - [ - 34, - 76 - ], - [ - 66, - 41 - ], - [ - 55, - -90 - ], - [ - 56, - 2 - ], - [ - 13, - 93 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - 33, - -27 - ], - [ - 6, - -58 - ], - [ - 23, - -71 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - -10, - 0 - ], - [ - 1, - -64 - ], - [ - -43, - -4 - ], - [ - -22, - -27 - ], - [ - -32, - 0 - ], - [ - -25, - 15 - ], - [ - -59, - -13 - ], - [ - -22, - -93 - ], - [ - -22, - -9 - ], - [ - -33, - -151 - ], - [ - -97, - -130 - ], - [ - -23, - -165 - ], - [ - -28, - -54 - ], - [ - -9, - -43 - ], - [ - -157, - -10 - ], - [ - -1, - 0 - ] - ], - [ - [ - 11361, - 13367 - ], - [ - 3, - 56 - ], - [ - 27, - 32 - ], - [ - 23, - 63 - ], - [ - -5, - 41 - ], - [ - 24, - 84 - ], - [ - 39, - 77 - ], - [ - 24, - 19 - ], - [ - 18, - 70 - ], - [ - 2, - 64 - ], - [ - 25, - 74 - ], - [ - 46, - 44 - ], - [ - 45, - 122 - ], - [ - 1, - 2 - ], - [ - 35, - 46 - ], - [ - 65, - 13 - ], - [ - 55, - 82 - ], - [ - 35, - 32 - ], - [ - 58, - 100 - ], - [ - -18, - 150 - ], - [ - 27, - 103 - ], - [ - 9, - 63 - ], - [ - 45, - 81 - ], - [ - 70, - 55 - ], - [ - 52, - 49 - ], - [ - 46, - 125 - ], - [ - 22, - 73 - ], - [ - 51, - 0 - ], - [ - 42, - -51 - ], - [ - 67, - 8 - ], - [ - 72, - -26 - ], - [ - 30, - -2 - ] - ], - [ - [ - 14403, - 16582 - ], - [ - 17, - 18 - ], - [ - 46, - 12 - ], - [ - 51, - -38 - ], - [ - 29, - -4 - ], - [ - 32, - -32 - ], - [ - -5, - -41 - ], - [ - 25, - -20 - ], - [ - 10, - -50 - ], - [ - 24, - -30 - ], - [ - -5, - -18 - ], - [ - 13, - -12 - ], - [ - -18, - -9 - ], - [ - -41, - 3 - ], - [ - -7, - 17 - ], - [ - -15, - -10 - ], - [ - 5, - -21 - ], - [ - -19, - -38 - ], - [ - -12, - -42 - ], - [ - -17, - -13 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - -13, - 55 - ], - [ - 7, - 51 - ], - [ - -2, - 53 - ], - [ - -40, - 71 - ], - [ - -22, - 51 - ], - [ - -22, - 35 - ], - [ - -21, - 12 - ] - ], - [ - [ - 16001, - 9301 - ], - [ - 19, - -51 - ], - [ - 17, - -79 - ], - [ - 11, - -144 - ], - [ - 18, - -57 - ], - [ - -7, - -57 - ], - [ - -12, - -35 - ], - [ - -24, - 70 - ], - [ - -13, - -36 - ], - [ - 13, - -88 - ], - [ - -6, - -51 - ], - [ - -19, - -28 - ], - [ - -4, - -102 - ], - [ - -28, - -139 - ], - [ - -34, - -166 - ], - [ - -43, - -227 - ], - [ - -27, - -167 - ], - [ - -32, - -139 - ], - [ - -56, - -28 - ], - [ - -61, - -51 - ], - [ - -40, - 30 - ], - [ - -56, - 43 - ], - [ - -19, - 64 - ], - [ - -4, - 106 - ], - [ - -25, - 96 - ], - [ - -6, - 86 - ], - [ - 12, - 86 - ], - [ - 32, - 21 - ], - [ - 0, - 40 - ], - [ - 34, - 91 - ], - [ - 6, - 77 - ], - [ - -16, - 56 - ], - [ - -13, - 76 - ], - [ - -6, - 111 - ], - [ - 24, - 67 - ], - [ - 10, - 76 - ], - [ - 35, - 4 - ], - [ - 38, - 25 - ], - [ - 26, - 21 - ], - [ - 31, - 2 - ], - [ - 40, - 68 - ], - [ - 57, - 74 - ], - [ - 21, - 61 - ], - [ - -10, - 51 - ], - [ - 30, - -14 - ], - [ - 38, - 83 - ], - [ - 2, - 72 - ], - [ - 23, - 54 - ], - [ - 24, - -52 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - -78, - 130 - ], - [ - -36, - 39 - ], - [ - -57, - 31 - ], - [ - -39, - -9 - ], - [ - -56, - -45 - ], - [ - -35, - -12 - ], - [ - -50, - 32 - ], - [ - -52, - 23 - ], - [ - -65, - 55 - ], - [ - -52, - 16 - ], - [ - -79, - 56 - ], - [ - -58, - 58 - ], - [ - -18, - 32 - ], - [ - -39, - 7 - ], - [ - -71, - 38 - ], - [ - -29, - 54 - ], - [ - -75, - 69 - ], - [ - -35, - 75 - ], - [ - -17, - 59 - ], - [ - 23, - 11 - ], - [ - -7, - 35 - ], - [ - 16, - 31 - ], - [ - 1, - 41 - ], - [ - -24, - 54 - ], - [ - -6, - 48 - ], - [ - -24, - 60 - ], - [ - -61, - 120 - ], - [ - -70, - 93 - ], - [ - -34, - 75 - ], - [ - -60, - 49 - ], - [ - -13, - 29 - ], - [ - 11, - 75 - ], - [ - -36, - 28 - ], - [ - -41, - 58 - ], - [ - -17, - 84 - ], - [ - -38, - 9 - ], - [ - -40, - 63 - ], - [ - -33, - 59 - ], - [ - -3, - 37 - ], - [ - -37, - 91 - ], - [ - -25, - 92 - ], - [ - 1, - 46 - ], - [ - -50, - 47 - ], - [ - -24, - -5 - ], - [ - -39, - 33 - ], - [ - -12, - -49 - ], - [ - 12, - -57 - ], - [ - 7, - -90 - ], - [ - 24, - -50 - ], - [ - 51, - -82 - ], - [ - 12, - -29 - ], - [ - 10, - -8 - ], - [ - 10, - -41 - ], - [ - 12, - 1 - ], - [ - 14, - -77 - ], - [ - 21, - -31 - ], - [ - 15, - -42 - ], - [ - 44, - -61 - ], - [ - 23, - -112 - ], - [ - 21, - -52 - ], - [ - 19, - -56 - ], - [ - 4, - -64 - ], - [ - 34, - -4 - ], - [ - 27, - -54 - ], - [ - 26, - -54 - ], - [ - -2, - -21 - ], - [ - -29, - -44 - ], - [ - -13, - 0 - ], - [ - -18, - 73 - ], - [ - -46, - 69 - ], - [ - -50, - 58 - ], - [ - -36, - 30 - ], - [ - 3, - 88 - ], - [ - -11, - 65 - ], - [ - -33, - 37 - ], - [ - -48, - 54 - ], - [ - -9, - -16 - ], - [ - -18, - 31 - ], - [ - -43, - 29 - ], - [ - -41, - 70 - ], - [ - 5, - 9 - ], - [ - 29, - -7 - ], - [ - 26, - 45 - ], - [ - 2, - 54 - ], - [ - -53, - 86 - ], - [ - -41, - 33 - ], - [ - -26, - 75 - ], - [ - -26, - 79 - ], - [ - -32, - 95 - ], - [ - -28, - 108 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - 79, - 10 - ], - [ - 88, - 13 - ], - [ - -6, - -24 - ], - [ - 105, - -58 - ], - [ - 159, - -85 - ], - [ - 139, - 1 - ], - [ - 55, - 0 - ], - [ - 0, - 50 - ], - [ - 121, - 0 - ], - [ - 25, - -43 - ], - [ - 36, - -38 - ], - [ - 42, - -52 - ], - [ - 23, - -63 - ], - [ - 17, - -66 - ], - [ - 36, - -36 - ], - [ - 58, - -36 - ], - [ - 44, - 94 - ], - [ - 57, - 3 - ], - [ - 49, - -48 - ], - [ - 35, - -82 - ], - [ - 24, - -70 - ], - [ - 41, - -69 - ], - [ - 15, - -84 - ], - [ - 20, - -56 - ], - [ - 54, - -37 - ], - [ - 50, - -27 - ], - [ - 27, - 4 - ] - ], - [ - [ - 5776, - 13901 - ], - [ - -27, - -106 - ], - [ - -12, - -86 - ], - [ - -5, - -161 - ], - [ - -7, - -58 - ], - [ - 12, - -66 - ], - [ - 22, - -58 - ], - [ - 14, - -93 - ], - [ - 46, - -90 - ], - [ - 16, - -68 - ], - [ - 27, - -59 - ], - [ - 74, - -32 - ], - [ - 29, - -50 - ], - [ - 61, - 33 - ], - [ - 54, - 13 - ], - [ - 52, - 21 - ], - [ - 44, - 21 - ], - [ - 44, - 49 - ], - [ - 17, - 70 - ], - [ - 5, - 100 - ], - [ - 12, - 36 - ], - [ - 48, - 31 - ], - [ - 73, - 28 - ], - [ - 62, - -4 - ], - [ - 42, - 10 - ], - [ - 17, - -26 - ], - [ - -2, - -57 - ], - [ - -38, - -72 - ], - [ - -16, - -73 - ], - [ - 12, - -21 - ], - [ - -10, - -52 - ], - [ - -17, - -93 - ], - [ - -18, - 31 - ], - [ - -15, - -2 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - 23, - 7 - ], - [ - 33, - 2 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 3, - 37 - ], - [ - -6, - 47 - ], - [ - -26, - 33 - ], - [ - -14, - 69 - ], - [ - -3, - 75 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - 24, - 22 - ], - [ - 11, - 70 - ], - [ - 22, - 3 - ], - [ - 49, - -33 - ], - [ - 39, - 23 - ], - [ - 27, - -8 - ], - [ - 11, - 27 - ], - [ - 279, - 2 - ], - [ - 16, - 84 - ], - [ - -12, - 15 - ], - [ - -34, - 517 - ], - [ - -33, - 518 - ], - [ - 106, - 2 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - 0, - -276 - ], - [ - -38, - -80 - ], - [ - -6, - -74 - ], - [ - -62, - -19 - ], - [ - -95, - -10 - ], - [ - -26, - -43 - ], - [ - -44, - -5 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - -40, - -28 - ], - [ - -40, - -52 - ], - [ - -49, - -5 - ], - [ - -32, - -130 - ], - [ - -30, - -22 - ], - [ - 34, - -105 - ], - [ - 44, - -88 - ], - [ - 29, - -79 - ], - [ - -26, - -104 - ], - [ - -24, - -22 - ], - [ - 17, - -61 - ], - [ - 46, - -95 - ], - [ - 8, - -67 - ], - [ - -1, - -56 - ], - [ - 28, - -109 - ], - [ - -39, - -112 - ], - [ - -33, - -123 - ] - ], - [ - [ - 19418, - 11989 - ], - [ - -7, - 89 - ], - [ - 21, - 92 - ], - [ - -23, - 71 - ], - [ - 5, - 130 - ], - [ - -28, - 63 - ], - [ - -23, - 143 - ], - [ - -12, - 152 - ], - [ - -30, - 99 - ], - [ - -46, - -60 - ], - [ - -79, - -86 - ], - [ - -40, - 11 - ], - [ - -43, - 28 - ], - [ - 24, - 149 - ], - [ - -14, - 112 - ], - [ - -55, - 139 - ], - [ - 9, - 43 - ], - [ - -41, - 15 - ], - [ - -50, - 98 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -15, - 9 - ], - [ - -19, - 40 - ], - [ - -30, - 23 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - 19, - -21 - ], - [ - 10, - -17 - ], - [ - 23, - -12 - ], - [ - 26, - -25 - ], - [ - -5, - -11 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 74, - 21 - ], - [ - 133, - 103 - ], - [ - 106, - 57 - ], - [ - 61, - -37 - ], - [ - 72, - -2 - ], - [ - 47, - -56 - ], - [ - 70, - -4 - ], - [ - 100, - -30 - ], - [ - 68, - 83 - ], - [ - -28, - 71 - ], - [ - 72, - 124 - ], - [ - 78, - -49 - ], - [ - 63, - -14 - ], - [ - 82, - -31 - ], - [ - 14, - -90 - ], - [ - 99, - -51 - ], - [ - 65, - 23 - ], - [ - 89, - 15 - ], - [ - 70, - -15 - ], - [ - 68, - -58 - ], - [ - 42, - -61 - ], - [ - 65, - 1 - ], - [ - 88, - -20 - ], - [ - 64, - 30 - ], - [ - 91, - 20 - ], - [ - 103, - 84 - ], - [ - 41, - -13 - ], - [ - 37, - -40 - ], - [ - 83, - 10 - ] - ], - [ - [ - 14957, - 9415 - ], - [ - 52, - 10 - ], - [ - 84, - -34 - ], - [ - 18, - 15 - ], - [ - 49, - 3 - ], - [ - 24, - 36 - ], - [ - 42, - -2 - ], - [ - 76, - 47 - ], - [ - 56, - 69 - ] - ], - [ - [ - 15358, - 9559 - ], - [ - 11, - -53 - ], - [ - -3, - -120 - ], - [ - 9, - -105 - ], - [ - 3, - -188 - ], - [ - 12, - -58 - ], - [ - -21, - -86 - ], - [ - -27, - -83 - ], - [ - -44, - -75 - ], - [ - -64, - -45 - ], - [ - -79, - -59 - ], - [ - -78, - -128 - ], - [ - -27, - -22 - ], - [ - -49, - -86 - ], - [ - -29, - -27 - ], - [ - -5, - -86 - ], - [ - 33, - -91 - ], - [ - 13, - -70 - ], - [ - 1, - -36 - ], - [ - 13, - 6 - ], - [ - -2, - -118 - ], - [ - -12, - -55 - ], - [ - 17, - -21 - ], - [ - -11, - -50 - ], - [ - -29, - -42 - ], - [ - -57, - -41 - ], - [ - -84, - -65 - ], - [ - -31, - -44 - ], - [ - 6, - -51 - ], - [ - 18, - -8 - ], - [ - -6, - -63 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -53, - 1 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -6, - 53 - ], - [ - -10, - 54 - ] - ], - [ - [ - 14767, - 7697 - ], - [ - -6, - 43 - ], - [ - 12, - 134 - ], - [ - -18, - 85 - ], - [ - -33, - 169 - ] - ], - [ - [ - 14722, - 8128 - ], - [ - 73, - 136 - ], - [ - 19, - 86 - ], - [ - 10, - 11 - ], - [ - 8, - 71 - ], - [ - -11, - 35 - ], - [ - 3, - 90 - ], - [ - 13, - 83 - ], - [ - 0, - 152 - ], - [ - -36, - 39 - ], - [ - -33, - 8 - ], - [ - -15, - 30 - ], - [ - -32, - 25 - ], - [ - -59, - -2 - ], - [ - -4, - 45 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -7, - 85 - ], - [ - 212, - 99 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - 40, - -58 - ], - [ - 19, - 11 - ], - [ - 28, - -30 - ], - [ - 4, - -48 - ], - [ - -15, - -56 - ], - [ - 5, - -84 - ], - [ - 46, - -74 - ], - [ - 21, - 83 - ], - [ - 30, - 25 - ], - [ - -6, - 154 - ], - [ - -29, - 87 - ], - [ - -25, - 39 - ], - [ - -24, - -2 - ], - [ - -20, - 156 - ], - [ - 20, - 91 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - -46, - 82 - ], - [ - -42, - 88 - ], - [ - -46, - 32 - ], - [ - -34, - 35 - ], - [ - -39, - -1 - ], - [ - -34, - -26 - ], - [ - -34, - 10 - ], - [ - -24, - -38 - ] - ], - [ - [ - 11400, - 12733 - ], - [ - -6, - 65 - ], - [ - 19, - 59 - ], - [ - 9, - 113 - ], - [ - -8, - 118 - ], - [ - -8, - 60 - ], - [ - 7, - 60 - ], - [ - -18, - 57 - ], - [ - -37, - 52 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 15, - 40 - ], - [ - 273, - -1 - ], - [ - -13, - 173 - ], - [ - 17, - 62 - ], - [ - 65, - 10 - ], - [ - -2, - 307 - ], - [ - 229, - -6 - ], - [ - 0, - 182 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - -37, - 31 - ], - [ - 21, - 112 - ], - [ - 22, - 41 - ], - [ - -13, - 100 - ], - [ - 14, - 97 - ], - [ - 12, - 32 - ], - [ - -18, - 102 - ], - [ - -33, - 54 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - 68, - -23 - ], - [ - 14, - -33 - ], - [ - 24, - -56 - ], - [ - 20, - -163 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - 54, - 83 - ], - [ - 35, - 94 - ], - [ - 28, - 0 - ], - [ - 36, - -60 - ], - [ - 3, - -52 - ], - [ - 46, - -34 - ], - [ - 58, - -36 - ], - [ - -4, - -47 - ], - [ - -47, - -6 - ], - [ - 12, - -59 - ], - [ - -51, - -40 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 51, - -41 - ], - [ - 54, - 22 - ], - [ - 14, - 102 - ], - [ - 30, - 22 - ], - [ - 83, - 26 - ], - [ - 50, - 95 - ], - [ - 34, - 76 - ] - ], - [ - [ - 19668, - 11544 - ], - [ - 16, - -12 - ], - [ - 41, - -72 - ], - [ - 29, - -80 - ], - [ - 4, - -81 - ], - [ - -7, - -55 - ], - [ - 6, - -41 - ], - [ - 5, - -71 - ], - [ - 25, - -33 - ], - [ - 27, - -106 - ], - [ - -1, - -41 - ], - [ - -49, - -8 - ], - [ - -66, - 89 - ], - [ - -83, - 95 - ], - [ - -8, - 62 - ], - [ - -40, - 80 - ], - [ - -10, - 99 - ], - [ - -25, - 66 - ], - [ - 8, - 87 - ], - [ - -16, - 51 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - 12, - 21 - ], - [ - 57, - -52 - ], - [ - 6, - -62 - ], - [ - 46, - 14 - ], - [ - 23, - 50 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - 57, - 27 - ], - [ - 45, - -7 - ], - [ - 28, - -27 - ], - [ - 0, - -10 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - -443 - ], - [ - -62, - -62 - ], - [ - -37, - -8 - ], - [ - -44, - 22 - ], - [ - -31, - 9 - ], - [ - -12, - 51 - ], - [ - -28, - 33 - ], - [ - -33, - -59 - ] - ], - [ - [ - 13687, - 7369 - ], - [ - -52, - 91 - ], - [ - -27, - 87 - ], - [ - -16, - 117 - ], - [ - -17, - 87 - ], - [ - -23, - 185 - ], - [ - -2, - 143 - ], - [ - -9, - 66 - ], - [ - -27, - 49 - ], - [ - -36, - 99 - ], - [ - -36, - 144 - ], - [ - -16, - 75 - ], - [ - -56, - 117 - ], - [ - -5, - 93 - ] - ], - [ - [ - 24104, - 8268 - ], - [ - 57, - -74 - ], - [ - 36, - -55 - ], - [ - -26, - -29 - ], - [ - -39, - 32 - ], - [ - -50, - 54 - ], - [ - -44, - 64 - ], - [ - -47, - 84 - ], - [ - -9, - 41 - ], - [ - 30, - -2 - ], - [ - 39, - -40 - ], - [ - 30, - -41 - ], - [ - 23, - -34 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - 17, - -186 - ], - [ - 26, - -32 - ], - [ - 1, - -38 - ], - [ - 29, - -41 - ], - [ - -15, - -52 - ], - [ - -27, - -243 - ], - [ - -4, - -156 - ], - [ - -89, - -113 - ], - [ - -30, - -158 - ], - [ - 29, - -45 - ], - [ - 0, - -77 - ], - [ - 45, - -3 - ], - [ - -7, - -56 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - -13, - -3 - ], - [ - -47, - 132 - ], - [ - -16, - 4 - ], - [ - -55, - -67 - ], - [ - -54, - 35 - ], - [ - -37, - 7 - ], - [ - -21, - -17 - ], - [ - -40, - 4 - ], - [ - -42, - -51 - ], - [ - -35, - -3 - ], - [ - -84, - 62 - ], - [ - -33, - -29 - ], - [ - -36, - 2 - ], - [ - -26, - 45 - ], - [ - -70, - 45 - ], - [ - -75, - -15 - ], - [ - -18, - -25 - ], - [ - -10, - -69 - ], - [ - -20, - -49 - ], - [ - -5, - -107 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - -72, - -43 - ], - [ - -27, - 6 - ], - [ - -27, - -27 - ], - [ - -55, - 3 - ], - [ - -38, - 75 - ], - [ - -23, - 86 - ], - [ - -49, - 79 - ], - [ - -52, - -1 - ], - [ - -62, - 0 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - -24, - 38 - ], - [ - -33, - 49 - ], - [ - -15, - 40 - ], - [ - -30, - 38 - ], - [ - -35, - 54 - ], - [ - 8, - 19 - ], - [ - 12, - -19 - ], - [ - 5, - 9 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -6, - -11 - ], - [ - -3, - -27 - ], - [ - 7, - -44 - ], - [ - -16, - -41 - ], - [ - -8, - -48 - ], - [ - -2, - -53 - ], - [ - 4, - -31 - ], - [ - 2, - -54 - ], - [ - -11, - -12 - ], - [ - -6, - -51 - ], - [ - 4, - -32 - ], - [ - -14, - -30 - ], - [ - 3, - -33 - ], - [ - 11, - -19 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 36, - 33 - ], - [ - 61, - 177 - ], - [ - 95, - 50 - ], - [ - 58, - -4 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - -44, - -5 - ], - [ - -10, - -79 - ], - [ - -131, - 19 - ], - [ - -19, - -67 - ], - [ - -67, - 1 - ], - [ - -46, - -86 - ], - [ - -69, - -133 - ], - [ - -109, - -168 - ], - [ - 26, - -41 - ], - [ - -24, - -48 - ], - [ - -70, - 2 - ], - [ - -45, - -112 - ], - [ - 4, - -160 - ], - [ - 45, - -60 - ], - [ - -23, - -142 - ], - [ - -58, - -82 - ], - [ - -31, - -69 - ] - ], - [ - [ - 13316, - 17858 - ], - [ - -47, - 74 - ], - [ - -137, - -139 - ], - [ - -93, - -28 - ], - [ - -97, - 61 - ], - [ - -24, - 129 - ], - [ - -23, - 277 - ], - [ - 65, - 77 - ], - [ - 184, - 101 - ], - [ - 137, - 124 - ], - [ - 128, - 167 - ], - [ - 167, - 231 - ], - [ - 117, - 91 - ], - [ - 192, - 150 - ], - [ - 153, - 53 - ], - [ - 114, - -7 - ], - [ - 107, - 100 - ], - [ - 127, - -6 - ], - [ - 125, - 24 - ], - [ - 218, - -88 - ], - [ - -90, - -32 - ], - [ - 77, - -75 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - -119, - -48 - ], - [ - -56, - -11 - ] - ], - [ - [ - 14271, - 20137 - ], - [ - -156, - -49 - ], - [ - -123, - 28 - ], - [ - 48, - 31 - ], - [ - -42, - 38 - ], - [ - 145, - 24 - ], - [ - 27, - -45 - ], - [ - 101, - -27 - ] - ], - [ - [ - 13820, - 20359 - ], - [ - 229, - -90 - ], - [ - -175, - -47 - ], - [ - -39, - -88 - ], - [ - -61, - -23 - ], - [ - -33, - -99 - ], - [ - -84, - -5 - ], - [ - -150, - 73 - ], - [ - 63, - 43 - ], - [ - -104, - 35 - ], - [ - -136, - 101 - ], - [ - -54, - 94 - ], - [ - 190, - 43 - ], - [ - 38, - -42 - ], - [ - 99, - 2 - ], - [ - 27, - 41 - ], - [ - 102, - 4 - ], - [ - 88, - -42 - ] - ], - [ - [ - 14321, - 20444 - ], - [ - 137, - -43 - ], - [ - -103, - -64 - ], - [ - -203, - -14 - ], - [ - -205, - 20 - ], - [ - -12, - 33 - ], - [ - -101, - 2 - ], - [ - -76, - 55 - ], - [ - 215, - 33 - ], - [ - 102, - -28 - ], - [ - 70, - 36 - ], - [ - 176, - -30 - ] - ], - [ - [ - 24608, - 5888 - ], - [ - 16, - -49 - ], - [ - 50, - 48 - ], - [ - 20, - -50 - ], - [ - 0, - -51 - ], - [ - -26, - -55 - ], - [ - -45, - -89 - ], - [ - -36, - -48 - ], - [ - 26, - -58 - ], - [ - -54, - -1 - ], - [ - -60, - -46 - ], - [ - -18, - -78 - ], - [ - -40, - -121 - ], - [ - -55, - -54 - ], - [ - -35, - -34 - ], - [ - -64, - 2 - ], - [ - -45, - 40 - ], - [ - -76, - 8 - ], - [ - -11, - 44 - ], - [ - 37, - 89 - ], - [ - 88, - 119 - ], - [ - 45, - 22 - ], - [ - 50, - 46 - ], - [ - 60, - 63 - ], - [ - 41, - 62 - ], - [ - 31, - 89 - ], - [ - 27, - 31 - ], - [ - 10, - 67 - ], - [ - 49, - 55 - ], - [ - 15, - -51 - ] - ], - [ - [ - 24719, - 6460 - ], - [ - 51, - -127 - ], - [ - 1, - 82 - ], - [ - 32, - -33 - ], - [ - 10, - -90 - ], - [ - 56, - -39 - ], - [ - 47, - -10 - ], - [ - 40, - 46 - ], - [ - 36, - -14 - ], - [ - -17, - -107 - ], - [ - -21, - -70 - ], - [ - -54, - 3 - ], - [ - -18, - -37 - ], - [ - 6, - -51 - ], - [ - -10, - -22 - ], - [ - -26, - -65 - ], - [ - -35, - -82 - ], - [ - -54, - -48 - ], - [ - -12, - 31 - ], - [ - -29, - 18 - ], - [ - 40, - 98 - ], - [ - -23, - 66 - ], - [ - -75, - 48 - ], - [ - 2, - 44 - ], - [ - 51, - 42 - ], - [ - 12, - 92 - ], - [ - -4, - 78 - ], - [ - -28, - 80 - ], - [ - 2, - 21 - ], - [ - -33, - 50 - ], - [ - -55, - 106 - ], - [ - -29, - 85 - ], - [ - 26, - 9 - ], - [ - 37, - -66 - ], - [ - 55, - -32 - ], - [ - 19, - -106 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 20, - 41 - ], - [ - 9, - -11 - ], - [ - -7, - -49 - ], - [ - -9, - -22 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - 31, - -82 - ], - [ - 39, - -43 - ], - [ - 51, - -16 - ], - [ - 41, - -22 - ], - [ - 32, - -68 - ], - [ - 19, - -40 - ], - [ - 25, - -15 - ], - [ - -1, - -27 - ], - [ - -25, - -72 - ], - [ - -11, - -33 - ], - [ - -29, - -39 - ], - [ - -26, - -82 - ], - [ - -32, - 6 - ], - [ - -15, - -28 - ], - [ - -11, - -61 - ], - [ - 9, - -80 - ], - [ - -7, - -15 - ], - [ - -32, - 0 - ], - [ - -43, - -44 - ], - [ - -7, - -59 - ], - [ - -16, - -25 - ], - [ - -43, - 1 - ], - [ - -28, - -30 - ], - [ - 1, - -49 - ], - [ - -34, - -33 - ], - [ - -39, - 11 - ], - [ - -46, - -40 - ], - [ - -32, - -7 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -23, - 84 - ], - [ - -55, - 198 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - 209, - 120 - ], - [ - 47, - 240 - ], - [ - -32, - 84 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - -51, - 31 - ], - [ - -21, - 86 - ], - [ - -54, - 91 - ], - [ - -128, - -22 - ], - [ - -113, - -2 - ], - [ - -99, - -17 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - -24, - 34 - ], - [ - -15, - 65 - ], - [ - 18, - 32 - ], - [ - -18, - 8 - ], - [ - -13, - 40 - ], - [ - -35, - 33 - ], - [ - -30, - -7 - ], - [ - -14, - -42 - ], - [ - -29, - -30 - ], - [ - -15, - -4 - ], - [ - -7, - -25 - ], - [ - 34, - -65 - ], - [ - -19, - -16 - ], - [ - -11, - -17 - ], - [ - -32, - -7 - ], - [ - -12, - 72 - ], - [ - -9, - -20 - ], - [ - -23, - 7 - ], - [ - -14, - 48 - ], - [ - -29, - 8 - ], - [ - -18, - 14 - ], - [ - -30, - 0 - ], - [ - -2, - -26 - ], - [ - -8, - 18 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - 25, - -43 - ], - [ - -1, - -26 - ], - [ - 28, - -5 - ], - [ - 6, - 10 - ], - [ - 20, - -30 - ], - [ - 34, - 9 - ], - [ - 29, - 30 - ], - [ - 43, - 24 - ], - [ - 24, - 36 - ], - [ - 38, - -7 - ], - [ - -3, - -12 - ], - [ - 39, - -4 - ], - [ - 31, - -20 - ], - [ - 23, - -36 - ], - [ - 26, - -34 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - -70, - 69 - ], - [ - -6, - 49 - ], - [ - -138, - 121 - ], - [ - -125, - 131 - ], - [ - -54, - 74 - ], - [ - -29, - 99 - ], - [ - 12, - 34 - ], - [ - -59, - 158 - ], - [ - -69, - 221 - ], - [ - -66, - 239 - ], - [ - -29, - 55 - ], - [ - -21, - 88 - ], - [ - -55, - 78 - ], - [ - -49, - 49 - ], - [ - 22, - 54 - ], - [ - -34, - 114 - ], - [ - 22, - 84 - ], - [ - 56, - 76 - ] - ], - [ - [ - 21357, - 11807 - ], - [ - 7, - -80 - ], - [ - 4, - -67 - ], - [ - -24, - -110 - ], - [ - -25, - 122 - ], - [ - -33, - -61 - ], - [ - 23, - -88 - ], - [ - -20, - -56 - ], - [ - -82, - 69 - ], - [ - -20, - 87 - ], - [ - 21, - 57 - ], - [ - -44, - 57 - ], - [ - -22, - -50 - ], - [ - -33, - 5 - ], - [ - -51, - -67 - ], - [ - -12, - 35 - ], - [ - 28, - 101 - ], - [ - 44, - 34 - ], - [ - 38, - 45 - ], - [ - 24, - -54 - ], - [ - 53, - 33 - ], - [ - 12, - 53 - ], - [ - 49, - 3 - ], - [ - -4, - 93 - ], - [ - 56, - -57 - ], - [ - 6, - -60 - ], - [ - 5, - -44 - ] - ], - [ - [ - 21190, - 12030 - ], - [ - -25, - -39 - ], - [ - -22, - -76 - ], - [ - -22, - -35 - ], - [ - -43, - 82 - ], - [ - 15, - 33 - ], - [ - 17, - 33 - ], - [ - 8, - 75 - ], - [ - 38, - 7 - ], - [ - -11, - -81 - ], - [ - 52, - 116 - ], - [ - -7, - -115 - ] - ], - [ - [ - 20808, - 11915 - ], - [ - -92, - -114 - ], - [ - 34, - 84 - ], - [ - 50, - 74 - ], - [ - 42, - 83 - ], - [ - 36, - 119 - ], - [ - 13, - -98 - ], - [ - -46, - -66 - ], - [ - -37, - -82 - ] - ], - [ - [ - 21044, - 12224 - ], - [ - 42, - -37 - ], - [ - 44, - 0 - ], - [ - -1, - -50 - ], - [ - -33, - -51 - ], - [ - -44, - -36 - ], - [ - -2, - 56 - ], - [ - 5, - 61 - ], - [ - -11, - 57 - ] - ], - [ - [ - 21296, - 12256 - ], - [ - 20, - -134 - ], - [ - -54, - 32 - ], - [ - 1, - -40 - ], - [ - 17, - -74 - ], - [ - -33, - -27 - ], - [ - -3, - 84 - ], - [ - -21, - 7 - ], - [ - -11, - 72 - ], - [ - 41, - -9 - ], - [ - 0, - 45 - ], - [ - -43, - 92 - ], - [ - 67, - -3 - ], - [ - 19, - -45 - ] - ], - [ - [ - 21019, - 12365 - ], - [ - -19, - -104 - ], - [ - -29, - 60 - ], - [ - -36, - 92 - ], - [ - 60, - -5 - ], - [ - 24, - -43 - ] - ], - [ - [ - 21005, - 13017 - ], - [ - 43, - -34 - ], - [ - 21, - 31 - ], - [ - 6, - -30 - ], - [ - -11, - -50 - ], - [ - 24, - -86 - ], - [ - -18, - -100 - ], - [ - -42, - -40 - ], - [ - -11, - -96 - ], - [ - 16, - -96 - ], - [ - 37, - -13 - ], - [ - 31, - 14 - ], - [ - 87, - -66 - ], - [ - -7, - -66 - ], - [ - 23, - -29 - ], - [ - -7, - -55 - ], - [ - -55, - 59 - ], - [ - -25, - 63 - ], - [ - -18, - -44 - ], - [ - -45, - 72 - ], - [ - -63, - -18 - ], - [ - -35, - 27 - ], - [ - 4, - 49 - ], - [ - 22, - 31 - ], - [ - -21, - 28 - ], - [ - -9, - -44 - ], - [ - -35, - 69 - ], - [ - -10, - 52 - ], - [ - -3, - 115 - ], - [ - 28, - -39 - ], - [ - 8, - 188 - ], - [ - 22, - 108 - ], - [ - 43, - 0 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 121, - -82 - ], - [ - 129, - -69 - ], - [ - 48, - -62 - ], - [ - 39, - -60 - ], - [ - 11, - -71 - ], - [ - 116, - -74 - ], - [ - 17, - -63 - ], - [ - -64, - -13 - ], - [ - 15, - -80 - ], - [ - 62, - -79 - ], - [ - 46, - -127 - ], - [ - 39, - 4 - ], - [ - -2, - -53 - ], - [ - 53, - -21 - ], - [ - -20, - -22 - ], - [ - 74, - -51 - ], - [ - -8, - -34 - ], - [ - -46, - -9 - ], - [ - -17, - 31 - ], - [ - -60, - 14 - ], - [ - -71, - 18 - ], - [ - -54, - 76 - ], - [ - -39, - 66 - ], - [ - -37, - 105 - ], - [ - -91, - 53 - ], - [ - -59, - -34 - ], - [ - -42, - -40 - ], - [ - 9, - -88 - ], - [ - -55, - -42 - ], - [ - -39, - 20 - ], - [ - -72, - 5 - ] - ], - [ - [ - 23414, - 9979 - ], - [ - -20, - -12 - ], - [ - -30, - 46 - ], - [ - -31, - 76 - ], - [ - -15, - 92 - ], - [ - 10, - 11 - ], - [ - 8, - -35 - ], - [ - 21, - -28 - ], - [ - 33, - -76 - ], - [ - 33, - -40 - ], - [ - -9, - -34 - ] - ], - [ - [ - 23142, - 10140 - ], - [ - -37, - -10 - ], - [ - -11, - -34 - ], - [ - -38, - -29 - ], - [ - -35, - -28 - ], - [ - -37, - 0 - ], - [ - -58, - 35 - ], - [ - -39, - 34 - ], - [ - 5, - 37 - ], - [ - 63, - -18 - ], - [ - 38, - 10 - ], - [ - 10, - 57 - ], - [ - 10, - 3 - ], - [ - 7, - -64 - ], - [ - 40, - 10 - ], - [ - 20, - 41 - ], - [ - 39, - 42 - ], - [ - -8, - 71 - ], - [ - 42, - 2 - ], - [ - 14, - -19 - ], - [ - -2, - -67 - ], - [ - -23, - -73 - ] - ], - [ - [ - 23223, - 10257 - ], - [ - -22, - -32 - ], - [ - -13, - 71 - ], - [ - -17, - 47 - ], - [ - -31, - 39 - ], - [ - -40, - 51 - ], - [ - -50, - 35 - ], - [ - 19, - 29 - ], - [ - 38, - -33 - ], - [ - 24, - -27 - ], - [ - 29, - -29 - ], - [ - 28, - -50 - ], - [ - 26, - -38 - ], - [ - 9, - -63 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - 35, - -105 - ], - [ - -8, - -33 - ], - [ - -34, - -14 - ], - [ - -64, - -100 - ], - [ - 18, - -54 - ], - [ - -15, - 7 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -66, - 46 - ], - [ - -50, - -17 - ], - [ - -33, - 12 - ], - [ - -42, - -25 - ], - [ - -35, - 42 - ], - [ - -28, - -16 - ], - [ - -4, - 7 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 47, - 36 - ], - [ - 109, - 55 - ], - [ - 88, - 41 - ], - [ - 70, - -21 - ], - [ - 5, - -29 - ], - [ - 67, - -1 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 86, - -14 - ], - [ - 128, - 2 - ] - ], - [ - [ - 7927, - 13018 - ], - [ - 36, - -10 - ], - [ - 12, - -24 - ], - [ - -18, - -30 - ], - [ - -52, - 0 - ], - [ - -41, - -4 - ], - [ - -4, - 52 - ], - [ - 10, - 17 - ], - [ - 57, - -1 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - 10, - -21 - ] - ], - [ - [ - 21664, - 15862 - ], - [ - -27, - 7 - ], - [ - -30, - -40 - ], - [ - -21, - -41 - ], - [ - 3, - -86 - ], - [ - -36, - -27 - ], - [ - -12, - -21 - ], - [ - -27, - -35 - ], - [ - -46, - -20 - ], - [ - -30, - -32 - ], - [ - -3, - -52 - ], - [ - -8, - -13 - ], - [ - 28, - -20 - ], - [ - 40, - -53 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - -34, - 23 - ], - [ - -8, - -23 - ], - [ - -21, - -10 - ], - [ - -2, - 23 - ], - [ - -18, - 11 - ], - [ - -19, - 19 - ], - [ - 19, - 53 - ], - [ - 17, - 14 - ], - [ - -7, - 22 - ], - [ - 18, - 65 - ], - [ - -5, - 19 - ], - [ - -40, - 13 - ], - [ - -33, - 32 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -28, - -31 - ], - [ - -37, - 17 - ], - [ - -36, - -14 - ], - [ - 11, - 94 - ], - [ - -7, - 74 - ], - [ - -31, - 11 - ], - [ - -17, - 45 - ], - [ - 6, - 79 - ], - [ - 28, - 44 - ], - [ - 5, - 48 - ], - [ - 14, - 72 - ], - [ - -1, - 51 - ], - [ - -14, - 43 - ], - [ - -3, - 41 - ] - ], - [ - [ - 16089, - 13767 - ], - [ - -4, - 87 - ], - [ - 19, - 63 - ], - [ - 19, - 13 - ], - [ - 21, - -37 - ], - [ - 1, - -71 - ], - [ - -15, - -70 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - -20, - -9 - ], - [ - -21, - 24 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - -13, - 21 - ], - [ - 16, - 20 - ], - [ - -17, - 15 - ], - [ - -22, - -27 - ], - [ - -40, - 35 - ], - [ - -6, - 50 - ], - [ - -42, - 28 - ], - [ - -8, - 38 - ], - [ - -38, - 47 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - 30, - 25 - ], - [ - 43, - -13 - ], - [ - 45, - 0 - ], - [ - 32, - -30 - ], - [ - 24, - 19 - ], - [ - 51, - 11 - ], - [ - 18, - 28 - ], - [ - 29, - 0 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - 31, - -22 - ], - [ - 32, - 20 - ], - [ - 32, - -21 - ] - ], - [ - [ - 14611, - 16231 - ], - [ - 2, - -31 - ], - [ - -34, - -26 - ], - [ - -21, - 11 - ], - [ - -20, - -144 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - -89, - 101 - ], - [ - -80, - 46 - ], - [ - -60, - 70 - ], - [ - 51, - 19 - ], - [ - 58, - 101 - ], - [ - -39, - 47 - ], - [ - 102, - 49 - ], - [ - -1, - 26 - ], - [ - -63, - -19 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - 2, - 53 - ], - [ - 36, - 34 - ], - [ - 68, - 9 - ], - [ - 11, - 40 - ], - [ - -16, - 66 - ], - [ - 28, - 63 - ], - [ - 0, - 35 - ], - [ - -103, - 39 - ], - [ - -41, - -1 - ], - [ - -43, - 56 - ], - [ - -53, - -19 - ], - [ - -89, - 42 - ], - [ - 2, - 23 - ], - [ - -25, - 53 - ], - [ - -56, - 5 - ], - [ - -6, - 38 - ], - [ - 18, - 24 - ], - [ - -45, - 68 - ], - [ - -72, - -12 - ], - [ - -21, - 6 - ], - [ - -18, - -27 - ], - [ - -26, - 5 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 79, - 67 - ], - [ - -73, - 57 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - 71, - 42 - ], - [ - 115, - -73 - ], - [ - 191, - -28 - ], - [ - 263, - -136 - ], - [ - 54, - -57 - ], - [ - 4, - -80 - ], - [ - -77, - -63 - ], - [ - -114, - -32 - ], - [ - -311, - 91 - ], - [ - -51, - -15 - ], - [ - 113, - -88 - ], - [ - 5, - -56 - ], - [ - 4, - -122 - ], - [ - 90, - -37 - ], - [ - 55, - -31 - ], - [ - 9, - 58 - ], - [ - -42, - 52 - ], - [ - 44, - 45 - ], - [ - 168, - -74 - ], - [ - 59, - 29 - ], - [ - -47, - 88 - ], - [ - 163, - 117 - ], - [ - 64, - -7 - ], - [ - 65, - -42 - ], - [ - 41, - 83 - ], - [ - -58, - 71 - ], - [ - 34, - 72 - ], - [ - -51, - 75 - ], - [ - 195, - -39 - ], - [ - 39, - -67 - ], - [ - -88, - -15 - ], - [ - 1, - -67 - ], - [ - 54, - -41 - ], - [ - 108, - 26 - ], - [ - 17, - 77 - ], - [ - 146, - 57 - ], - [ - 243, - 103 - ], - [ - 53, - -6 - ], - [ - -69, - -73 - ], - [ - 86, - -12 - ], - [ - 50, - 41 - ], - [ - 131, - 3 - ], - [ - 103, - 50 - ], - [ - 80, - -73 - ], - [ - 79, - 80 - ], - [ - -73, - 69 - ], - [ - 36, - 40 - ], - [ - 206, - -36 - ], - [ - 97, - -38 - ], - [ - 252, - -137 - ], - [ - 47, - 63 - ], - [ - -71, - 63 - ], - [ - -2, - 26 - ], - [ - -84, - 12 - ], - [ - 23, - 56 - ], - [ - -37, - 94 - ], - [ - -2, - 38 - ], - [ - 128, - 109 - ], - [ - 46, - 109 - ], - [ - 52, - 24 - ], - [ - 184, - -32 - ], - [ - 15, - -67 - ], - [ - -66, - -97 - ], - [ - 43, - -38 - ], - [ - 23, - -84 - ], - [ - -16, - -164 - ], - [ - 77, - -74 - ], - [ - -30, - -80 - ], - [ - -137, - -170 - ], - [ - 80, - -18 - ], - [ - 28, - 43 - ], - [ - 76, - 31 - ], - [ - 19, - 59 - ], - [ - 60, - 57 - ], - [ - -40, - 69 - ], - [ - 32, - 79 - ], - [ - -76, - 10 - ], - [ - -17, - 66 - ], - [ - 56, - 121 - ], - [ - -91, - 98 - ], - [ - 125, - 80 - ], - [ - -16, - 86 - ], - [ - 35, - 3 - ], - [ - 36, - -67 - ], - [ - -27, - -116 - ], - [ - 74, - -22 - ], - [ - -31, - 87 - ], - [ - 116, - 47 - ], - [ - 145, - 6 - ], - [ - 129, - -68 - ], - [ - -62, - 100 - ], - [ - -7, - 128 - ], - [ - 121, - 24 - ], - [ - 168, - -5 - ], - [ - 151, - 15 - ], - [ - -57, - 63 - ], - [ - 81, - 79 - ], - [ - 80, - 3 - ], - [ - 135, - 60 - ], - [ - 184, - 16 - ], - [ - 24, - 32 - ], - [ - 183, - 12 - ], - [ - 57, - -27 - ], - [ - 156, - 63 - ], - [ - 128, - -2 - ], - [ - 20, - 52 - ], - [ - 66, - 51 - ], - [ - 165, - 50 - ], - [ - 119, - -39 - ], - [ - -95, - -30 - ], - [ - 158, - -18 - ], - [ - 19, - -60 - ], - [ - 64, - 30 - ], - [ - 204, - -2 - ], - [ - 157, - -59 - ], - [ - 56, - -44 - ], - [ - -18, - -63 - ], - [ - -77, - -35 - ], - [ - -183, - -67 - ], - [ - -52, - -36 - ], - [ - 86, - -16 - ], - [ - 103, - -31 - ], - [ - 63, - 23 - ], - [ - 35, - -77 - ], - [ - 31, - 31 - ], - [ - 112, - 19 - ], - [ - 223, - -20 - ], - [ - 17, - -56 - ], - [ - 292, - -18 - ], - [ - 4, - 92 - ], - [ - 148, - -21 - ], - [ - 111, - 1 - ], - [ - 112, - -63 - ], - [ - 32, - -77 - ], - [ - -41, - -50 - ], - [ - 88, - -95 - ], - [ - 109, - -49 - ], - [ - 68, - 126 - ], - [ - 111, - -54 - ], - [ - 119, - 33 - ], - [ - 135, - -37 - ], - [ - 52, - 33 - ], - [ - 114, - -16 - ], - [ - -51, - 111 - ], - [ - 92, - 52 - ], - [ - 630, - -78 - ], - [ - 59, - -71 - ], - [ - 183, - -92 - ], - [ - 281, - 23 - ], - [ - 139, - -20 - ], - [ - 58, - -50 - ], - [ - -8, - -87 - ], - [ - 85, - -34 - ], - [ - 94, - 24 - ], - [ - 123, - 3 - ], - [ - 132, - -23 - ], - [ - 132, - 13 - ], - [ - 121, - -107 - ], - [ - 87, - 39 - ], - [ - -57, - 76 - ], - [ - 32, - 54 - ], - [ - 222, - -34 - ], - [ - 145, - 7 - ], - [ - 200, - -57 - ], - [ - 98, - -52 - ], - [ - 0, - -478 - ], - [ - -1, - -1 - ], - [ - -89, - -53 - ], - [ - -90, - 9 - ], - [ - 62, - -64 - ], - [ - 42, - -99 - ], - [ - 32, - -32 - ], - [ - 8, - -49 - ], - [ - -18, - -32 - ], - [ - -130, - 26 - ], - [ - -195, - -90 - ], - [ - -62, - -14 - ], - [ - -106, - -85 - ], - [ - -101, - -73 - ], - [ - -26, - -55 - ], - [ - -100, - 83 - ], - [ - -181, - -94 - ], - [ - -32, - 45 - ], - [ - -67, - -52 - ], - [ - -93, - 17 - ], - [ - -23, - -79 - ], - [ - -84, - -116 - ], - [ - 3, - -49 - ], - [ - 79, - -27 - ], - [ - -9, - -174 - ], - [ - -65, - -5 - ], - [ - -30, - -100 - ], - [ - 29, - -52 - ], - [ - -121, - -61 - ], - [ - -25, - -137 - ], - [ - -104, - -29 - ], - [ - -20, - -122 - ], - [ - -101, - -112 - ], - [ - -26, - 83 - ], - [ - -30, - 175 - ], - [ - -38, - 266 - ], - [ - 33, - 167 - ], - [ - 59, - 71 - ], - [ - 3, - 56 - ], - [ - 109, - 27 - ], - [ - 124, - 151 - ], - [ - 120, - 123 - ], - [ - 126, - 96 - ], - [ - 56, - 169 - ], - [ - -85, - -10 - ], - [ - -42, - -99 - ], - [ - -177, - -131 - ], - [ - -57, - 147 - ], - [ - -180, - -41 - ], - [ - -174, - -201 - ], - [ - 57, - -73 - ], - [ - -155, - -32 - ], - [ - -108, - -12 - ], - [ - 5, - 87 - ], - [ - -108, - 18 - ], - [ - -87, - -59 - ], - [ - -213, - 21 - ], - [ - -229, - -36 - ], - [ - -226, - -234 - ], - [ - -267, - -283 - ], - [ - 110, - -15 - ], - [ - 34, - -75 - ], - [ - 68, - -27 - ], - [ - 44, - 60 - ], - [ - 77, - -8 - ], - [ - 100, - -132 - ], - [ - 3, - -102 - ], - [ - -55, - -120 - ], - [ - -6, - -143 - ], - [ - -31, - -192 - ], - [ - -105, - -173 - ], - [ - -23, - -83 - ], - [ - -95, - -140 - ], - [ - -94, - -138 - ], - [ - -45, - -71 - ], - [ - -93, - -71 - ], - [ - -44, - -1 - ], - [ - -44, - 58 - ], - [ - -93, - -88 - ], - [ - -11, - -40 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -32, - -71 - ], - [ - -67, - -20 - ], - [ - -69, - -124 - ], - [ - 63, - -114 - ], - [ - -7, - -81 - ], - [ - 76, - -141 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 16, - 52 - ], - [ - 96, - 39 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - 19, - 30 - ], - [ - 49, - -26 - ], - [ - 23, - -4 - ], - [ - 9, - -24 - ], - [ - 10, - -4 - ] - ], - [ - [ - 14988, - 16284 - ], - [ - 1, - -10 - ], - [ - 34, - -29 - ], - [ - 71, - 7 - ], - [ - -14, - -43 - ], - [ - -76, - -20 - ], - [ - -95, - -70 - ], - [ - -38, - 25 - ], - [ - 15, - 56 - ], - [ - -76, - 35 - ], - [ - 12, - 23 - ], - [ - 67, - 40 - ], - [ - -11, - 14 - ] - ], - [ - [ - 22561, - 16885 - ], - [ - 70, - -212 - ], - [ - -103, - 39 - ], - [ - -43, - -173 - ], - [ - 68, - -123 - ], - [ - -2, - -84 - ], - [ - -53, - 73 - ], - [ - -46, - -93 - ], - [ - -12, - 100 - ], - [ - 7, - 117 - ], - [ - -8, - 130 - ], - [ - 17, - 90 - ], - [ - 3, - 161 - ], - [ - -41, - 118 - ], - [ - 6, - 164 - ], - [ - 64, - 55 - ], - [ - -27, - 56 - ], - [ - 31, - 16 - ], - [ - 18, - -79 - ], - [ - 24, - -116 - ], - [ - -2, - -118 - ], - [ - 29, - -121 - ] - ], - [ - [ - 348, - 18785 - ], - [ - 47, - -30 - ], - [ - -17, - 88 - ], - [ - 190, - -18 - ], - [ - 136, - -113 - ], - [ - -69, - -52 - ], - [ - -114, - -12 - ], - [ - -2, - -118 - ], - [ - -28, - -24 - ], - [ - -65, - 3 - ], - [ - -53, - 42 - ], - [ - -93, - 35 - ], - [ - -16, - 52 - ], - [ - -70, - 20 - ], - [ - -80, - -16 - ], - [ - -38, - 42 - ], - [ - 16, - 45 - ], - [ - -84, - -29 - ], - [ - 32, - -56 - ], - [ - -40, - -51 - ], - [ - 0, - 478 - ], - [ - 171, - -92 - ], - [ - 183, - -119 - ], - [ - -6, - -75 - ] - ], - [ - [ - 25095, - 19295 - ], - [ - -76, - -6 - ], - [ - -13, - 38 - ], - [ - 89, - 50 - ], - [ - 0, - -82 - ] - ], - [ - [ - 91, - 19302 - ], - [ - -91, - -7 - ], - [ - 0, - 82 - ], - [ - 9, - 5 - ], - [ - 59, - 0 - ], - [ - 101, - -35 - ], - [ - -6, - -16 - ], - [ - -72, - -29 - ] - ], - [ - [ - 22558, - 19580 - ], - [ - -106, - 0 - ], - [ - -143, - 13 - ], - [ - -12, - 6 - ], - [ - 66, - 48 - ], - [ - 87, - 11 - ], - [ - 99, - -46 - ], - [ - 9, - -32 - ] - ], - [ - [ - 23055, - 19805 - ], - [ - -81, - -47 - ], - [ - -111, - 10 - ], - [ - -130, - 48 - ], - [ - 17, - 38 - ], - [ - 130, - -18 - ], - [ - 175, - -31 - ] - ], - [ - [ - 22661, - 19862 - ], - [ - -55, - -89 - ], - [ - -257, - 4 - ], - [ - -115, - -29 - ], - [ - -138, - 78 - ], - [ - 37, - 83 - ], - [ - 92, - 22 - ], - [ - 184, - -5 - ], - [ - 252, - -64 - ] - ], - [ - [ - 16558, - 19281 - ], - [ - -41, - -10 - ], - [ - -228, - 16 - ], - [ - -18, - 53 - ], - [ - -126, - 32 - ], - [ - -11, - 65 - ], - [ - 72, - 25 - ], - [ - -3, - 66 - ], - [ - 139, - 102 - ], - [ - -65, - 15 - ], - [ - 167, - 105 - ], - [ - -18, - 55 - ], - [ - 155, - 63 - ], - [ - 231, - 77 - ], - [ - 232, - 22 - ], - [ - 119, - 45 - ], - [ - 136, - 16 - ], - [ - 48, - -48 - ], - [ - -47, - -37 - ], - [ - -247, - -60 - ], - [ - -213, - -57 - ], - [ - -216, - -114 - ], - [ - -104, - -117 - ], - [ - -109, - -116 - ], - [ - 14, - -99 - ], - [ - 133, - -99 - ] - ], - [ - [ - 19872, - 20192 - ], - [ - -393, - -47 - ], - [ - 128, - 158 - ], - [ - 57, - 13 - ], - [ - 52, - -8 - ], - [ - 177, - -68 - ], - [ - -21, - -48 - ] - ], - [ - [ - 16112, - 20460 - ], - [ - -93, - -15 - ], - [ - -63, - -10 - ], - [ - -10, - -19 - ], - [ - -81, - -20 - ], - [ - -76, - 28 - ], - [ - 40, - 38 - ], - [ - -155, - 3 - ], - [ - 136, - 22 - ], - [ - 106, - 2 - ], - [ - 14, - -33 - ], - [ - 40, - 29 - ], - [ - 66, - 20 - ], - [ - 103, - -26 - ], - [ - -27, - -19 - ] - ], - [ - [ - 19514, - 20260 - ], - [ - -152, - -15 - ], - [ - -194, - 35 - ], - [ - -116, - 46 - ], - [ - -53, - 86 - ], - [ - -95, - 24 - ], - [ - 181, - 82 - ], - [ - 150, - 27 - ], - [ - 136, - -61 - ], - [ - 160, - -116 - ], - [ - -17, - -108 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - 17, - -12 - ], - [ - 42, - 37 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 28, - -68 - ], - [ - -4, - -70 - ], - [ - -21, - -15 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 3, - 50 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - -201, - -46 - ], - [ - -65, - -54 - ], - [ - -50, - -126 - ], - [ - -32, - -20 - ], - [ - -18, - 40 - ], - [ - -26, - -6 - ], - [ - -68, - 12 - ], - [ - -13, - 12 - ], - [ - -80, - -3 - ], - [ - -19, - -11 - ], - [ - -28, - 31 - ], - [ - -19, - -59 - ], - [ - 7, - -50 - ], - [ - -30, - -39 - ] - ], - [ - [ - 15530, - 12758 - ], - [ - -9, - 52 - ], - [ - -21, - 36 - ], - [ - -6, - 48 - ], - [ - -36, - 43 - ], - [ - -37, - 100 - ], - [ - -20, - 98 - ], - [ - -48, - 83 - ], - [ - -31, - 19 - ], - [ - -46, - 115 - ], - [ - -8, - 83 - ], - [ - 3, - 71 - ], - [ - -40, - 133 - ], - [ - -33, - 47 - ], - [ - -38, - 25 - ], - [ - -22, - 68 - ], - [ - 3, - 28 - ], - [ - -19, - 62 - ], - [ - -20, - 27 - ], - [ - -28, - 89 - ], - [ - -42, - 97 - ], - [ - -36, - 82 - ], - [ - -34, - -1 - ], - [ - 10, - 66 - ], - [ - 4, - 42 - ], - [ - 8, - 48 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - 27, - -104 - ], - [ - 34, - -27 - ], - [ - 12, - -42 - ], - [ - 48, - -51 - ], - [ - 4, - -49 - ], - [ - -7, - -40 - ], - [ - 9, - -41 - ], - [ - 20, - -33 - ], - [ - 9, - -40 - ], - [ - 10, - -29 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - 13, - -46 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 1, - 29 - ], - [ - -25, - 35 - ], - [ - -1, - 70 - ], - [ - -15, - 46 - ], - [ - -24, - -7 - ], - [ - 7, - 44 - ], - [ - 18, - 50 - ], - [ - -8, - 50 - ], - [ - 23, - 37 - ], - [ - -15, - 28 - ], - [ - 19, - 74 - ], - [ - 32, - 88 - ], - [ - 60, - -8 - ], - [ - -4, - 476 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - 23, - -118 - ], - [ - -15, - -22 - ], - [ - 10, - -123 - ], - [ - 25, - -144 - ], - [ - 27, - -29 - ], - [ - 38, - -45 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 94 - ], - [ - -10, - 2 - ], - [ - 2, - 60 - ], - [ - -9, - 41 - ], - [ - -36, - 47 - ], - [ - -8, - 87 - ], - [ - 8, - 88 - ], - [ - -32, - 9 - ], - [ - -5, - -27 - ], - [ - -42, - -6 - ], - [ - 17, - -35 - ], - [ - 6, - -72 - ], - [ - -38, - -66 - ], - [ - -35, - -87 - ], - [ - -36, - -12 - ], - [ - -58, - 70 - ], - [ - -27, - -25 - ], - [ - -7, - -35 - ], - [ - -36, - -23 - ], - [ - -2, - -24 - ], - [ - -70, - 0 - ], - [ - -9, - 24 - ], - [ - -51, - 5 - ], - [ - -25, - -21 - ], - [ - -19, - 10 - ], - [ - -36, - 70 - ], - [ - -12, - 33 - ], - [ - -50, - -16 - ], - [ - -19, - -56 - ], - [ - -18, - -107 - ], - [ - -24, - -23 - ], - [ - -21, - -13 - ], - [ - 47, - -47 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - -43, - -55 - ], - [ - -49, - 0 - ], - [ - -56, - -28 - ], - [ - -44, - 27 - ], - [ - -29, - -33 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - -11, - 92 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - -28, - 94 - ], - [ - -35, - 42 - ], - [ - 31, - 23 - ], - [ - 33, - 84 - ], - [ - 17, - 62 - ] - ], - [ - [ - 23849, - 9540 - ], - [ - 19, - -42 - ], - [ - -49, - 1 - ], - [ - -26, - 74 - ], - [ - 41, - -29 - ], - [ - 15, - -4 - ] - ], - [ - [ - 23760, - 9613 - ], - [ - -27, - -3 - ], - [ - -43, - 12 - ], - [ - -14, - 19 - ], - [ - 4, - 47 - ], - [ - 46, - -19 - ], - [ - 23, - -25 - ], - [ - 11, - -31 - ] - ], - [ - [ - 23818, - 9645 - ], - [ - -11, - -22 - ], - [ - -51, - 104 - ], - [ - -15, - 72 - ], - [ - 24, - 0 - ], - [ - 25, - -96 - ], - [ - 28, - -58 - ] - ], - [ - [ - 23692, - 9797 - ], - [ - 3, - -24 - ], - [ - -55, - 51 - ], - [ - -38, - 43 - ], - [ - -26, - 40 - ], - [ - 11, - 12 - ], - [ - 32, - -29 - ], - [ - 57, - -55 - ], - [ - 16, - -38 - ] - ], - [ - [ - 23529, - 9916 - ], - [ - -14, - -7 - ], - [ - -30, - 27 - ], - [ - -29, - 49 - ], - [ - 4, - 20 - ], - [ - 41, - -50 - ], - [ - 28, - -39 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - -19, - 9 - ], - [ - -50, - 49 - ], - [ - -36, - 64 - ], - [ - -12, - 44 - ], - [ - -9, - 88 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - -8, - -28 - ], - [ - -41, - 1 - ], - [ - -25, - 12 - ], - [ - -28, - 24 - ], - [ - -39, - 7 - ], - [ - -20, - 26 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - 23, - -22 - ], - [ - 13, - -49 - ], - [ - 32, - -51 - ], - [ - 34, - 0 - ], - [ - 66, - 31 - ], - [ - 76, - 14 - ], - [ - 61, - 37 - ], - [ - 35, - 8 - ], - [ - 25, - 22 - ], - [ - 40, - 4 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - -1, - -2 - ], - [ - 0, - -49 - ], - [ - 0, - -121 - ], - [ - 0, - -63 - ], - [ - -32, - -74 - ], - [ - -48, - -100 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - 22, - 2 - ], - [ - 32, - 18 - ], - [ - 37, - 12 - ], - [ - 33, - 41 - ], - [ - 26, - 1 - ], - [ - 2, - -33 - ], - [ - -6, - -70 - ], - [ - 0, - -63 - ], - [ - -15, - -44 - ], - [ - -20, - -129 - ], - [ - -33, - -134 - ], - [ - -43, - -153 - ], - [ - -60, - -176 - ], - [ - -60, - -135 - ], - [ - -82, - -163 - ], - [ - -69, - -97 - ], - [ - -105, - -120 - ], - [ - -65, - -91 - ], - [ - -76, - -145 - ], - [ - -16, - -63 - ], - [ - -16, - -29 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - 83, - -24 - ], - [ - 8, - 21 - ], - [ - 56, - 9 - ], - [ - 75, - -32 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -19, - -31 - ], - [ - -14, - -49 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 15, - 11 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -120, - -34 - ], - [ - -68, - -84 - ], - [ - 11, - -73 - ], - [ - -111, - -97 - ], - [ - -134, - -103 - ], - [ - -51, - -169 - ], - [ - 49, - -84 - ], - [ - 67, - -67 - ], - [ - -64, - -135 - ], - [ - -72, - -28 - ], - [ - -27, - -202 - ], - [ - -40, - -112 - ], - [ - -84, - 12 - ], - [ - -40, - -96 - ], - [ - -80, - -5 - ], - [ - -22, - 113 - ], - [ - -59, - 136 - ], - [ - -53, - 170 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -14, - -53 - ], - [ - -41, - -13 - ], - [ - -41, - 65 - ], - [ - -1, - 41 - ], - [ - 19, - 45 - ], - [ - 7, - 35 - ], - [ - 20, - 9 - ], - [ - 35, - -22 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - -7, - 91 - ], - [ - 17, - 50 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - 19, - 26 - ], - [ - 19, - 26 - ], - [ - 4, - 67 - ], - [ - 22, - -23 - ], - [ - 77, - 33 - ], - [ - 37, - -22 - ], - [ - 58, - 0 - ], - [ - 80, - 45 - ], - [ - 37, - -2 - ], - [ - 80, - 19 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -57, - -26 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -63, - 55 - ], - [ - -60, - -2 - ], - [ - 11, - 94 - ], - [ - -62, - 0 - ], - [ - -5, - -132 - ], - [ - -38, - -176 - ], - [ - -23, - -106 - ], - [ - 5, - -86 - ], - [ - 46, - -4 - ], - [ - 28, - -110 - ], - [ - 12, - -103 - ], - [ - 39, - -69 - ], - [ - 42, - -14 - ], - [ - 37, - -62 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - -27, - 46 - ], - [ - -12, - 59 - ], - [ - -37, - 68 - ], - [ - -34, - 57 - ], - [ - -11, - -71 - ], - [ - -14, - 67 - ], - [ - 8, - 75 - ], - [ - 21, - 115 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 39, - 122 - ], - [ - -15, - 89 - ], - [ - -51, - 29 - ], - [ - 18, - 53 - ], - [ - 58, - -6 - ], - [ - 33, - 66 - ], - [ - 22, - 77 - ], - [ - 94, - 28 - ], - [ - -15, - -55 - ], - [ - 10, - -34 - ], - [ - 29, - 3 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - -13, - 85 - ], - [ - 10, - 125 - ], - [ - -54, - 41 - ], - [ - 18, - 82 - ], - [ - -46, - 7 - ], - [ - 15, - 101 - ], - [ - 66, - -29 - ], - [ - 61, - 38 - ], - [ - -51, - 72 - ], - [ - -20, - 69 - ], - [ - -56, - -31 - ], - [ - -7, - -88 - ], - [ - -22, - 78 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - 79, - 2 - ], - [ - -12, - 60 - ], - [ - 60, - 41 - ], - [ - 58, - 70 - ], - [ - 94, - -63 - ], - [ - 8, - -96 - ], - [ - 26, - -25 - ], - [ - 76, - 6 - ], - [ - 23, - -22 - ], - [ - 35, - -124 - ], - [ - 79, - -82 - ], - [ - 46, - -57 - ], - [ - 73, - -59 - ], - [ - 92, - -51 - ], - [ - -2, - -73 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 8, - 29 - ], - [ - 60, - 27 - ], - [ - 49, - 4 - ], - [ - 21, - 15 - ], - [ - 27, - -15 - ], - [ - -26, - -33 - ], - [ - -72, - -52 - ], - [ - -59, - -35 - ] - ], - [ - [ - 8248, - 12088 - ], - [ - 40, - 16 - ], - [ - 15, - -5 - ], - [ - -3, - -89 - ], - [ - -58, - -13 - ], - [ - -13, - 11 - ], - [ - 20, - 33 - ], - [ - -1, - 47 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - 75, - 48 - ], - [ - 49, - -14 - ], - [ - -2, - -61 - ], - [ - 59, - 44 - ], - [ - 5, - -23 - ], - [ - -35, - -59 - ], - [ - 0, - -55 - ], - [ - 24, - -30 - ], - [ - -9, - -104 - ], - [ - -46, - -60 - ], - [ - 13, - -66 - ], - [ - 36, - -2 - ], - [ - 18, - -57 - ], - [ - 26, - -18 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - -25, - 54 - ], - [ - 26, - 45 - ], - [ - -42, - -10 - ], - [ - -59, - 28 - ], - [ - -48, - -70 - ], - [ - -105, - -13 - ], - [ - -57, - 64 - ], - [ - -75, - 4 - ], - [ - -16, - -49 - ], - [ - -48, - -15 - ], - [ - -68, - 64 - ], - [ - -76, - -2 - ], - [ - -41, - 119 - ], - [ - -51, - 67 - ], - [ - 34, - 93 - ], - [ - -44, - 58 - ], - [ - 77, - 114 - ], - [ - 107, - 5 - ], - [ - 30, - 91 - ], - [ - 133, - -16 - ], - [ - 83, - 78 - ], - [ - 82, - 34 - ], - [ - 115, - 3 - ], - [ - 122, - -85 - ], - [ - 100, - -46 - ], - [ - 81, - 18 - ], - [ - 60, - -10 - ], - [ - 82, - 62 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - 8, - -46 - ], - [ - 61, - -39 - ], - [ - -12, - -29 - ], - [ - -83, - -7 - ], - [ - -30, - -37 - ], - [ - -58, - -65 - ], - [ - -22, - 56 - ], - [ - 1, - 25 - ] - ], - [ - [ - 21036, - 13724 - ], - [ - -42, - -193 - ], - [ - -29, - -98 - ], - [ - -37, - 101 - ], - [ - -8, - 89 - ], - [ - 41, - 118 - ], - [ - 56, - 91 - ], - [ - 32, - -36 - ], - [ - -13, - -72 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 24, - 14 - ], - [ - 77, - -1 - ], - [ - 142, - 9 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -32, - -148 - ], - [ - 4, - -68 - ], - [ - 45, - -43 - ], - [ - 2, - -32 - ], - [ - -19, - -72 - ], - [ - 4, - -36 - ], - [ - -5, - -58 - ], - [ - 24, - -75 - ], - [ - 29, - -118 - ], - [ - 26, - -27 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - -39, - 36 - ], - [ - -45, - 20 - ], - [ - -28, - 20 - ], - [ - -29, - 31 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - -56, - -10 - ], - [ - -46, - -38 - ], - [ - -65, - -7 - ], - [ - -60, - -44 - ], - [ - 3, - -65 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - -9, - 13 - ], - [ - -109, - 31 - ], - [ - -4, - 44 - ], - [ - -65, - -14 - ], - [ - -26, - -66 - ], - [ - -54, - -89 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -30, - -75 - ], - [ - -79, - -67 - ], - [ - -51, - 24 - ], - [ - -38, - -13 - ], - [ - -65, - 52 - ], - [ - -47, - -4 - ], - [ - -42, - 66 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - 13, - -39 - ], - [ - -75, - -58 - ], - [ - -72, - -42 - ], - [ - -73, - -35 - ], - [ - -37, - -71 - ], - [ - -12, - -27 - ], - [ - -1, - -64 - ], - [ - 23, - -64 - ], - [ - 29, - -3 - ], - [ - -7, - 44 - ], - [ - 21, - -26 - ], - [ - -6, - -35 - ], - [ - -47, - -19 - ], - [ - -33, - 2 - ], - [ - -52, - -21 - ], - [ - -30, - -6 - ], - [ - -41, - -6 - ], - [ - -58, - -34 - ], - [ - 103, - 22 - ], - [ - 20, - -22 - ], - [ - -97, - -36 - ], - [ - -45, - -1 - ], - [ - 2, - 15 - ], - [ - -21, - -33 - ], - [ - 21, - -6 - ], - [ - -15, - -86 - ], - [ - -51, - -92 - ], - [ - -5, - 31 - ], - [ - -16, - 6 - ], - [ - -22, - 30 - ], - [ - 14, - -65 - ], - [ - 17, - -21 - ], - [ - 1, - -46 - ], - [ - -22, - -46 - ], - [ - -39, - -96 - ], - [ - -7, - 5 - ], - [ - 22, - 81 - ], - [ - -36, - 46 - ], - [ - -8, - 100 - ], - [ - -13, - -52 - ], - [ - 15, - -76 - ], - [ - -46, - 19 - ], - [ - 48, - -39 - ], - [ - 3, - -114 - ], - [ - 20, - -8 - ], - [ - 7, - -42 - ], - [ - 10, - -120 - ], - [ - -45, - -89 - ], - [ - -72, - -35 - ], - [ - -46, - -71 - ], - [ - -34, - -8 - ], - [ - -36, - -44 - ], - [ - -10, - -40 - ], - [ - -76, - -78 - ], - [ - -39, - -57 - ], - [ - -33, - -71 - ], - [ - -11, - -85 - ], - [ - 12, - -83 - ], - [ - 24, - -103 - ], - [ - 30, - -85 - ], - [ - 1, - -52 - ], - [ - 33, - -139 - ], - [ - -2, - -81 - ], - [ - -3, - -47 - ], - [ - -18, - -73 - ], - [ - -21, - -15 - ], - [ - -34, - 15 - ], - [ - -11, - 52 - ], - [ - -26, - 28 - ], - [ - -37, - 103 - ], - [ - -33, - 92 - ], - [ - -10, - 47 - ], - [ - 14, - 79 - ], - [ - -19, - 66 - ], - [ - -55, - 101 - ], - [ - -27, - 18 - ], - [ - -70, - -54 - ], - [ - -13, - 6 - ], - [ - -34, - 56 - ], - [ - -43, - 29 - ], - [ - -79, - -15 - ], - [ - -62, - 13 - ], - [ - -53, - -8 - ], - [ - -29, - -19 - ], - [ - 13, - -31 - ], - [ - -2, - -49 - ], - [ - 15, - -24 - ], - [ - -13, - -16 - ], - [ - -26, - 18 - ], - [ - -26, - -23 - ], - [ - -51, - 4 - ], - [ - -52, - 64 - ], - [ - -60, - -15 - ], - [ - -51, - 27 - ], - [ - -44, - -8 - ], - [ - -58, - -28 - ], - [ - -64, - -89 - ], - [ - -69, - -52 - ], - [ - -38, - -57 - ], - [ - -16, - -54 - ], - [ - -1, - -83 - ], - [ - 4, - -57 - ], - [ - 13, - -41 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - -12, - 62 - ], - [ - -45, - 69 - ], - [ - -33, - 14 - ], - [ - -7, - 34 - ], - [ - -39, - 6 - ], - [ - -25, - 33 - ], - [ - -65, - 12 - ], - [ - -18, - 19 - ], - [ - -8, - 66 - ], - [ - -68, - 120 - ], - [ - -58, - 167 - ], - [ - 2, - 28 - ], - [ - -30, - 40 - ], - [ - -54, - 100 - ], - [ - -10, - 98 - ], - [ - -37, - 66 - ], - [ - 15, - 99 - ], - [ - -2, - 103 - ], - [ - -22, - 92 - ], - [ - 27, - 113 - ], - [ - 8, - 109 - ], - [ - 9, - 109 - ], - [ - -13, - 161 - ], - [ - -22, - 102 - ], - [ - -20, - 56 - ], - [ - 8, - 23 - ], - [ - 101, - -41 - ], - [ - 37, - -113 - ], - [ - 17, - 32 - ], - [ - -11, - 98 - ], - [ - -23, - 99 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - -38, - 45 - ], - [ - -62, - 38 - ], - [ - -19, - 105 - ], - [ - -90, - 97 - ], - [ - -38, - 113 - ], - [ - -67, - 8 - ], - [ - -111, - 3 - ], - [ - -81, - 34 - ], - [ - -144, - 125 - ], - [ - -67, - 23 - ], - [ - -122, - 42 - ], - [ - -97, - -10 - ], - [ - -137, - 55 - ], - [ - -83, - 51 - ], - [ - -77, - -25 - ], - [ - 14, - -83 - ], - [ - -38, - -8 - ], - [ - -81, - -25 - ], - [ - -61, - -40 - ], - [ - -77, - -26 - ], - [ - -10, - 71 - ], - [ - 31, - 117 - ], - [ - 74, - 37 - ], - [ - -19, - 30 - ], - [ - -89, - -66 - ], - [ - -47, - -80 - ], - [ - -101, - -86 - ], - [ - 51, - -58 - ], - [ - -66, - -86 - ], - [ - -75, - -50 - ], - [ - -69, - -37 - ], - [ - -18, - -53 - ], - [ - -109, - -62 - ], - [ - -22, - -56 - ], - [ - -81, - -52 - ], - [ - -48, - 10 - ], - [ - -65, - -34 - ], - [ - -71, - -41 - ], - [ - -58, - -40 - ], - [ - -119, - -34 - ], - [ - -11, - 20 - ], - [ - 76, - 56 - ], - [ - 68, - 37 - ], - [ - 74, - 66 - ], - [ - 87, - 13 - ], - [ - 34, - 50 - ], - [ - 97, - 71 - ], - [ - 15, - 24 - ], - [ - 52, - 43 - ], - [ - 12, - 91 - ], - [ - 35, - 71 - ], - [ - -80, - -37 - ], - [ - -22, - 21 - ], - [ - -38, - -44 - ], - [ - -46, - 61 - ], - [ - -19, - -43 - ], - [ - -26, - 60 - ], - [ - -69, - -48 - ], - [ - -43, - 0 - ], - [ - -6, - 71 - ], - [ - 13, - 44 - ], - [ - -45, - 43 - ], - [ - -91, - -23 - ], - [ - -59, - 56 - ], - [ - -48, - 29 - ], - [ - 0, - 68 - ], - [ - -54, - 51 - ], - [ - 27, - 69 - ], - [ - 57, - 67 - ], - [ - 25, - 62 - ], - [ - 57, - 9 - ], - [ - 47, - -20 - ], - [ - 57, - 58 - ], - [ - 50, - -10 - ], - [ - 53, - 37 - ], - [ - -13, - 55 - ], - [ - -39, - 22 - ], - [ - 52, - 46 - ], - [ - -43, - -2 - ], - [ - -74, - -26 - ], - [ - -21, - -26 - ], - [ - -55, - 26 - ], - [ - -99, - -13 - ], - [ - -102, - 29 - ], - [ - -29, - 48 - ], - [ - -88, - 70 - ], - [ - 98, - 50 - ], - [ - 155, - 58 - ], - [ - 58, - 0 - ], - [ - -10, - -60 - ], - [ - 147, - 5 - ], - [ - -56, - 74 - ], - [ - -86, - 46 - ], - [ - -50, - 60 - ], - [ - -67, - 51 - ], - [ - -95, - 38 - ], - [ - 39, - 63 - ], - [ - 123, - 4 - ], - [ - 88, - 55 - ], - [ - 17, - 58 - ], - [ - 71, - 57 - ], - [ - 68, - 14 - ], - [ - 132, - 53 - ], - [ - 64, - -8 - ], - [ - 108, - 64 - ], - [ - 105, - -25 - ], - [ - 50, - -54 - ], - [ - 31, - 23 - ], - [ - 118, - -7 - ], - [ - -4, - -28 - ], - [ - 107, - -20 - ], - [ - 71, - 12 - ], - [ - 147, - -38 - ], - [ - 134, - -12 - ], - [ - 53, - -15 - ], - [ - 93, - 19 - ], - [ - 106, - -36 - ], - [ - 76, - -17 - ] - ], - [ - [ - 1705, - 13087 - ], - [ - -10, - -20 - ], - [ - -18, - 17 - ], - [ - 2, - 33 - ], - [ - -11, - 44 - ], - [ - 3, - 13 - ], - [ - 12, - 20 - ], - [ - -4, - 23 - ], - [ - 4, - 12 - ], - [ - 5, - -3 - ], - [ - 27, - -20 - ], - [ - 12, - -10 - ], - [ - 11, - -16 - ], - [ - 18, - -42 - ], - [ - -2, - -7 - ], - [ - -27, - -26 - ], - [ - -22, - -18 - ] - ], - [ - [ - 1667, - 13274 - ], - [ - -23, - -9 - ], - [ - -12, - 26 - ], - [ - -8, - 9 - ], - [ - -1, - 8 - ], - [ - 7, - 10 - ], - [ - 25, - -11 - ], - [ - 18, - -19 - ], - [ - -6, - -14 - ] - ], - [ - [ - 1620, - 13338 - ], - [ - -2, - -13 - ], - [ - -37, - 3 - ], - [ - 5, - 15 - ], - [ - 34, - -5 - ] - ], - [ - [ - 1558, - 13355 - ], - [ - -4, - -7 - ], - [ - -5, - 2 - ], - [ - -24, - 4 - ], - [ - -9, - 27 - ], - [ - -3, - 5 - ], - [ - 19, - 17 - ], - [ - 6, - -8 - ], - [ - 20, - -40 - ] - ], - [ - [ - 1440, - 13434 - ], - [ - -8, - -12 - ], - [ - -24, - 22 - ], - [ - 4, - 9 - ], - [ - 10, - 12 - ], - [ - 16, - -3 - ], - [ - 2, - -28 - ] - ], - [ - [ - 1882, - 17649 - ], - [ - -70, - -45 - ], - [ - -36, - 31 - ], - [ - -10, - 56 - ], - [ - 63, - 42 - ], - [ - 37, - 19 - ], - [ - 46, - -8 - ], - [ - 30, - -38 - ], - [ - -60, - -57 - ] - ], - [ - [ - 1005, - 17985 - ], - [ - -43, - -19 - ], - [ - -45, - 22 - ], - [ - -43, - 33 - ], - [ - 69, - 20 - ], - [ - 56, - -10 - ], - [ - 6, - -46 - ] - ], - [ - [ - 576, - 18449 - ], - [ - 43, - -23 - ], - [ - 44, - 13 - ], - [ - 56, - -32 - ], - [ - 69, - -16 - ], - [ - -5, - -13 - ], - [ - -53, - -26 - ], - [ - -53, - 27 - ], - [ - -27, - 21 - ], - [ - -61, - -7 - ], - [ - -17, - 11 - ], - [ - 4, - 45 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -2, - -28 - ], - [ - -41, - -14 - ], - [ - 23, - -55 - ], - [ - -1, - -63 - ], - [ - -31, - -69 - ], - [ - 27, - -95 - ], - [ - 30, - 7 - ], - [ - 15, - 87 - ], - [ - -21, - 42 - ], - [ - -4, - 91 - ], - [ - 87, - 49 - ], - [ - -10, - 56 - ], - [ - 25, - 38 - ], - [ - 25, - -84 - ], - [ - 49, - -2 - ], - [ - 45, - -67 - ], - [ - 3, - -40 - ], - [ - 62, - -1 - ], - [ - 75, - 13 - ], - [ - 40, - -54 - ], - [ - 53, - -15 - ], - [ - 39, - 38 - ], - [ - 1, - 30 - ], - [ - 86, - 7 - ], - [ - 84, - 2 - ], - [ - -59, - -36 - ], - [ - 24, - -56 - ], - [ - 55, - -9 - ], - [ - 53, - -59 - ], - [ - 11, - -96 - ], - [ - 37, - 2 - ], - [ - 27, - -28 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -93, - -103 - ], - [ - -58, - -113 - ], - [ - -15, - -83 - ], - [ - 53, - -127 - ], - [ - 66, - -157 - ], - [ - 63, - -74 - ], - [ - 42, - -96 - ], - [ - 32, - -222 - ], - [ - -9, - -211 - ], - [ - -58, - -79 - ], - [ - -80, - -77 - ], - [ - -57, - -100 - ], - [ - -87, - -112 - ], - [ - -25, - 77 - ], - [ - 19, - 81 - ], - [ - -52, - 68 - ] - ], - [ - [ - 24248, - 8822 - ], - [ - -23, - -16 - ], - [ - -24, - 52 - ], - [ - 3, - 33 - ], - [ - 44, - -69 - ] - ], - [ - [ - 24196, - 9006 - ], - [ - 12, - -97 - ], - [ - -19, - 15 - ], - [ - -15, - -7 - ], - [ - -10, - 34 - ], - [ - -1, - 91 - ], - [ - 33, - -36 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -51, - -32 - ], - [ - -13, - -54 - ], - [ - -2, - -41 - ], - [ - -69, - -50 - ], - [ - -112, - -56 - ], - [ - -62, - -85 - ], - [ - -31, - -6 - ], - [ - -21, - 7 - ], - [ - -40, - -50 - ], - [ - -45, - -23 - ], - [ - -58, - -6 - ], - [ - -18, - -7 - ], - [ - -15, - -32 - ], - [ - -19, - -9 - ], - [ - -10, - -30 - ], - [ - -35, - 2 - ], - [ - -22, - -16 - ], - [ - -48, - 6 - ], - [ - -19, - 70 - ], - [ - 2, - 66 - ], - [ - -11, - 35 - ], - [ - -14, - 89 - ], - [ - -20, - 49 - ], - [ - 14, - 6 - ], - [ - -7, - 55 - ], - [ - 9, - 23 - ], - [ - -3, - 52 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - 29, - -1 - ], - [ - 33, - -21 - ], - [ - 24, - 15 - ], - [ - 37, - -12 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -17, - -87 - ], - [ - -9, - -100 - ], - [ - -18, - -54 - ], - [ - -47, - -61 - ], - [ - -14, - -17 - ], - [ - -29, - -61 - ], - [ - -20, - -62 - ], - [ - -39, - -86 - ], - [ - -79, - -123 - ], - [ - -49, - -72 - ], - [ - -53, - -55 - ], - [ - -73, - -47 - ], - [ - -35, - -6 - ], - [ - -9, - -33 - ], - [ - -43, - 18 - ], - [ - -34, - -23 - ], - [ - -76, - 23 - ], - [ - -42, - -15 - ], - [ - -29, - 7 - ], - [ - -72, - -48 - ], - [ - -59, - -19 - ], - [ - -43, - -45 - ], - [ - -32, - -3 - ], - [ - -30, - 43 - ], - [ - -23, - 2 - ], - [ - -30, - 54 - ], - [ - -3, - -17 - ], - [ - -10, - 32 - ], - [ - 1, - 70 - ], - [ - -23, - 81 - ], - [ - 23, - 22 - ], - [ - -2, - 92 - ], - [ - -46, - 112 - ], - [ - -35, - 102 - ], - [ - 0, - 0 - ], - [ - -50, - 156 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -53, - -17 - ], - [ - -40, - -47 - ], - [ - -8, - -42 - ], - [ - -25, - -10 - ], - [ - -61, - -98 - ], - [ - -38, - -78 - ], - [ - -24, - -3 - ], - [ - -22, - 14 - ], - [ - -78, - 13 - ] - ] - ], - "transform": { - "scale": [ - 0.01434548714883443, - 0.008335499711981569 - ], - "translate": [ - -180, - -90 - ] - }, - "objects": { - "ne_110m_admin_0_countries": { - "type": "GeometryCollection", - "geometries": [ - { - "arcs": [ - [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Afghanistan", - "NAME_LONG": "Afghanistan", - "ABBREV": "Afg.", - "FORMAL_EN": "Islamic State of Afghanistan", - "POP_EST": 34124811, - "POP_RANK": 15, - "GDP_MD_EST": 64080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AF", - "ISO_A3": "AFG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 6, - 7, - 8, - 9 - ] - ], - [ - [ - 10, - 11, - 12 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Angola", - "NAME_LONG": "Angola", - "ABBREV": "Ang.", - "FORMAL_EN": "People's Republic of Angola", - "POP_EST": 29310273, - "POP_RANK": 15, - "GDP_MD_EST": 189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AO", - "ISO_A3": "AGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - 13, - 14, - 15, - 16, - 17 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Albania", - "NAME_LONG": "Albania", - "ABBREV": "Alb.", - "FORMAL_EN": "Republic of Albania", - "POP_EST": 3047987, - "POP_RANK": 12, - "GDP_MD_EST": 33900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AL", - "ISO_A3": "ALB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 18, - 19, - 20, - 21, - 22 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "United Arab Emirates", - "NAME_LONG": "United Arab Emirates", - "ABBREV": "U.A.E.", - "FORMAL_EN": "United Arab Emirates", - "POP_EST": 6072475, - "POP_RANK": 13, - "GDP_MD_EST": 667200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AE", - "ISO_A3": "ARE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 23, - 24 - ] - ], - [ - [ - 25, - 26, - 27, - 28, - 29, - 30 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Argentina", - "NAME_LONG": "Argentina", - "ABBREV": "Arg.", - "FORMAL_EN": "Argentine Republic", - "POP_EST": 44293293, - "POP_RANK": 15, - "GDP_MD_EST": 879400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AR", - "ISO_A3": "ARG", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 31, - 32, - 33, - 34, - 35 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Armenia", - "NAME_LONG": "Armenia", - "ABBREV": "Arm.", - "FORMAL_EN": "Republic of Armenia", - "POP_EST": 3045191, - "POP_RANK": 12, - "GDP_MD_EST": 26300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AM", - "ISO_A3": "ARM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 36 - ] - ], - [ - [ - 37 - ] - ], - [ - [ - 38 - ] - ], - [ - [ - 39 - ] - ], - [ - [ - 40 - ] - ], - [ - [ - 41 - ] - ], - [ - [ - 42 - ] - ], - [ - [ - 43 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Antarctica", - "NAME_LONG": "Antarctica", - "ABBREV": "Ant.", - "FORMAL_EN": "", - "POP_EST": 4050, - "POP_RANK": 4, - "GDP_MD_EST": 810, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "AQ", - "ISO_A3": "ATA", - "CONTINENT": "Antarctica", - "REGION_UN": "Antarctica", - "SUBREGION": "Antarctica" - } - }, - { - "arcs": [ - [ - 44 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Fr. S. Antarctic Lands", - "NAME_LONG": "French Southern and Antarctic Lands", - "ABBREV": "Fr. S.A.L.", - "FORMAL_EN": "Territory of the French Southern and Antarctic Lands", - "POP_EST": 140, - "POP_RANK": 1, - "GDP_MD_EST": 16, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TF", - "ISO_A3": "ATF", - "CONTINENT": "Seven seas (open ocean)", - "REGION_UN": "Seven seas (open ocean)", - "SUBREGION": "Seven seas (open ocean)" - } - }, - { - "arcs": [ - [ - [ - 45 - ] - ], - [ - [ - 46 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Australia", - "NAME_LONG": "Australia", - "ABBREV": "Auz.", - "FORMAL_EN": "Commonwealth of Australia", - "POP_EST": 23232413, - "POP_RANK": 15, - "GDP_MD_EST": 1189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AU", - "ISO_A3": "AUS", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - 47, - 48, - 49, - 50, - 51, - 52, - 53 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Austria", - "NAME_LONG": "Austria", - "ABBREV": "Aust.", - "FORMAL_EN": "Republic of Austria", - "POP_EST": 8754413, - "POP_RANK": 13, - "GDP_MD_EST": 416600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AT", - "ISO_A3": "AUT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -33, - 54, - 55, - 56, - 57 - ] - ], - [ - [ - -35, - 58 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Azerbaijan", - "NAME_LONG": "Azerbaijan", - "ABBREV": "Aze.", - "FORMAL_EN": "Republic of Azerbaijan", - "POP_EST": 9961396, - "POP_RANK": 13, - "GDP_MD_EST": 167900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AZ", - "ISO_A3": "AZE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 59, - 60, - 61 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burundi", - "NAME_LONG": "Burundi", - "ABBREV": "Bur.", - "FORMAL_EN": "Republic of Burundi", - "POP_EST": 11466756, - "POP_RANK": 14, - "GDP_MD_EST": 7892, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BI", - "ISO_A3": "BDI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 62, - 63, - 64, - 65, - 66 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belgium", - "NAME_LONG": "Belgium", - "ABBREV": "Belg.", - "FORMAL_EN": "Kingdom of Belgium", - "POP_EST": 11491346, - "POP_RANK": 14, - "GDP_MD_EST": 508600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BE", - "ISO_A3": "BEL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 67, - 68, - 69, - 70, - 71 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Benin", - "NAME_LONG": "Benin", - "ABBREV": "Benin", - "FORMAL_EN": "Republic of Benin", - "POP_EST": 11038805, - "POP_RANK": 14, - "GDP_MD_EST": 24310, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BJ", - "ISO_A3": "BEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -70, - 72, - 73, - 74, - 75, - 76 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burkina Faso", - "NAME_LONG": "Burkina Faso", - "ABBREV": "B.F.", - "FORMAL_EN": "Burkina Faso", - "POP_EST": 20107509, - "POP_RANK": 15, - "GDP_MD_EST": 32990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BF", - "ISO_A3": "BFA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 77, - 78, - 79 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bangladesh", - "NAME_LONG": "Bangladesh", - "ABBREV": "Bang.", - "FORMAL_EN": "People's Republic of Bangladesh", - "POP_EST": 157826578, - "POP_RANK": 17, - "GDP_MD_EST": 628400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BD", - "ISO_A3": "BGD", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 80, - 81, - 82, - 83, - 84, - 85 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bulgaria", - "NAME_LONG": "Bulgaria", - "ABBREV": "Bulg.", - "FORMAL_EN": "Republic of Bulgaria", - "POP_EST": 7101510, - "POP_RANK": 13, - "GDP_MD_EST": 143100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BG", - "ISO_A3": "BGR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 86 - ] - ], - [ - [ - 87 - ] - ], - [ - [ - 88 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Bahamas", - "NAME_LONG": "Bahamas", - "ABBREV": "Bhs.", - "FORMAL_EN": "Commonwealth of the Bahamas", - "POP_EST": 329988, - "POP_RANK": 10, - "GDP_MD_EST": 9066, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BS", - "ISO_A3": "BHS", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 89, - 90, - 91 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bosnia and Herz.", - "NAME_LONG": "Bosnia and Herzegovina", - "ABBREV": "B.H.", - "FORMAL_EN": "Bosnia and Herzegovina", - "POP_EST": 3856181, - "POP_RANK": 12, - "GDP_MD_EST": 42530, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BA", - "ISO_A3": "BIH", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 92, - 93, - 94, - 95, - 96 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belarus", - "NAME_LONG": "Belarus", - "ABBREV": "Bela.", - "FORMAL_EN": "Republic of Belarus", - "POP_EST": 9549747, - "POP_RANK": 13, - "GDP_MD_EST": 165400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BY", - "ISO_A3": "BLR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 97, - 98, - 99 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belize", - "NAME_LONG": "Belize", - "ABBREV": "Belize", - "FORMAL_EN": "Belize", - "POP_EST": 360346, - "POP_RANK": 10, - "GDP_MD_EST": 3088, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BZ", - "ISO_A3": "BLZ", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -27, - 100, - 101, - 102, - 103 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bolivia", - "NAME_LONG": "Bolivia", - "ABBREV": "Bolivia", - "FORMAL_EN": "Plurinational State of Bolivia", - "POP_EST": 11138234, - "POP_RANK": 14, - "GDP_MD_EST": 78350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BO", - "ISO_A3": "BOL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -29, - 104, - -103, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brazil", - "NAME_LONG": "Brazil", - "ABBREV": "Brazil", - "FORMAL_EN": "Federative Republic of Brazil", - "POP_EST": 207353391, - "POP_RANK": 17, - "GDP_MD_EST": 3081000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BR", - "ISO_A3": "BRA", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 113, - 114 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brunei", - "NAME_LONG": "Brunei Darussalam", - "ABBREV": "Brunei", - "FORMAL_EN": "Negara Brunei Darussalam", - "POP_EST": 443593, - "POP_RANK": 10, - "GDP_MD_EST": 33730, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BN", - "ISO_A3": "BRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 115, - 116 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bhutan", - "NAME_LONG": "Bhutan", - "ABBREV": "Bhutan", - "FORMAL_EN": "Kingdom of Bhutan", - "POP_EST": 758288, - "POP_RANK": 11, - "GDP_MD_EST": 6432, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BT", - "ISO_A3": "BTN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 117, - 118, - 119, - 120 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Botswana", - "NAME_LONG": "Botswana", - "ABBREV": "Bwa.", - "FORMAL_EN": "Republic of Botswana", - "POP_EST": 2214858, - "POP_RANK": 12, - "GDP_MD_EST": 35900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BW", - "ISO_A3": "BWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 121, - 122, - 123, - 124, - 125, - 126 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Central African Rep.", - "NAME_LONG": "Central African Republic", - "ABBREV": "C.A.R.", - "FORMAL_EN": "Central African Republic", - "POP_EST": 5625118, - "POP_RANK": 13, - "GDP_MD_EST": 3206, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CF", - "ISO_A3": "CAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 127 - ] - ], - [ - [ - 128 - ] - ], - [ - [ - 129 - ] - ], - [ - [ - 130 - ] - ], - [ - [ - 131 - ] - ], - [ - [ - 132 - ] - ], - [ - [ - 133 - ] - ], - [ - [ - 134 - ] - ], - [ - [ - 135 - ] - ], - [ - [ - 136 - ] - ], - [ - [ - 137, - 138, - 139, - 140 - ] - ], - [ - [ - 141 - ] - ], - [ - [ - 142 - ] - ], - [ - [ - 143 - ] - ], - [ - [ - 144 - ] - ], - [ - [ - 145 - ] - ], - [ - [ - 146 - ] - ], - [ - [ - 147 - ] - ], - [ - [ - 148 - ] - ], - [ - [ - 149 - ] - ], - [ - [ - 150 - ] - ], - [ - [ - 151 - ] - ], - [ - [ - 152 - ] - ], - [ - [ - 153 - ] - ], - [ - [ - 154 - ] - ], - [ - [ - 155 - ] - ], - [ - [ - 156 - ] - ], - [ - [ - 157 - ] - ], - [ - [ - 158 - ] - ], - [ - [ - 159 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Canada", - "NAME_LONG": "Canada", - "ABBREV": "Can.", - "FORMAL_EN": "Canada", - "POP_EST": 35623680, - "POP_RANK": 15, - "GDP_MD_EST": 1674000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CA", - "ISO_A3": "CAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -51, - 160, - 161, - 162 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Switzerland", - "NAME_LONG": "Switzerland", - "ABBREV": "Switz.", - "FORMAL_EN": "Swiss Confederation", - "POP_EST": 8236303, - "POP_RANK": 13, - "GDP_MD_EST": 496300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CH", - "ISO_A3": "CHE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -24, - 163 - ] - ], - [ - [ - -26, - 164, - 165, - -101 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Chile", - "NAME_LONG": "Chile", - "ABBREV": "Chile", - "FORMAL_EN": "Republic of Chile", - "POP_EST": 17789267, - "POP_RANK": 14, - "GDP_MD_EST": 436100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CL", - "ISO_A3": "CHL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -4, - 166, - 167, - 168, - 169, - 170, - 171, - 172, - 173, - 174, - 175, - 176, - 177, - -117, - 178, - 179, - 180, - 181 - ] - ], - [ - [ - 182 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "China", - "NAME_LONG": "China", - "ABBREV": "China", - "FORMAL_EN": "People's Republic of China", - "POP_EST": 1379302771, - "POP_RANK": 18, - "GDP_MD_EST": 21140000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CN", - "ISO_A3": "CHN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -75, - 183, - 184, - 185, - 186, - 187 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Côte d'Ivoire", - "NAME_LONG": "Côte d'Ivoire", - "ABBREV": "I.C.", - "FORMAL_EN": "Republic of Ivory Coast", - "POP_EST": 24184810, - "POP_RANK": 15, - "GDP_MD_EST": 87120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CI", - "ISO_A3": "CIV", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -127, - 188, - 189, - 190, - 191, - 192, - 193, - 194 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cameroon", - "NAME_LONG": "Cameroon", - "ABBREV": "Cam.", - "FORMAL_EN": "Republic of Cameroon", - "POP_EST": 24994885, - "POP_RANK": 15, - "GDP_MD_EST": 77240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CM", - "ISO_A3": "CMR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -9, - 195, - -13, - 196, - -125, - 197, - 198, - 199, - -60, - 200, - 201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dem. Rep. Congo", - "NAME_LONG": "Democratic Republic of the Congo", - "ABBREV": "D.R.C.", - "FORMAL_EN": "Democratic Republic of the Congo", - "POP_EST": 83301151, - "POP_RANK": 16, - "GDP_MD_EST": 66010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CD", - "ISO_A3": "COD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -12, - 202, - 203, - -189, - -126, - -197 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Congo", - "NAME_LONG": "Republic of the Congo", - "ABBREV": "Rep. Congo", - "FORMAL_EN": "Republic of the Congo", - "POP_EST": 4954674, - "POP_RANK": 12, - "GDP_MD_EST": 30270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CG", - "ISO_A3": "COG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -107, - 204, - 205, - 206, - 207, - 208, - 209 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Colombia", - "NAME_LONG": "Colombia", - "ABBREV": "Col.", - "FORMAL_EN": "Republic of Colombia", - "POP_EST": 47698524, - "POP_RANK": 15, - "GDP_MD_EST": 688000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CO", - "ISO_A3": "COL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 210, - 211, - 212, - 213 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Costa Rica", - "NAME_LONG": "Costa Rica", - "ABBREV": "C.R.", - "FORMAL_EN": "Republic of Costa Rica", - "POP_EST": 4930258, - "POP_RANK": 12, - "GDP_MD_EST": 79260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CR", - "ISO_A3": "CRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - 214 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cuba", - "NAME_LONG": "Cuba", - "ABBREV": "Cuba", - "FORMAL_EN": "Republic of Cuba", - "POP_EST": 11147407, - "POP_RANK": 14, - "GDP_MD_EST": 132900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CU", - "ISO_A3": "CUB", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 215, - 216 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "N. Cyprus", - "NAME_LONG": "Northern Cyprus", - "ABBREV": "N. Cy.", - "FORMAL_EN": "Turkish Republic of Northern Cyprus", - "POP_EST": 265100, - "POP_RANK": 10, - "GDP_MD_EST": 3600, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -217, - 217 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cyprus", - "NAME_LONG": "Cyprus", - "ABBREV": "Cyp.", - "FORMAL_EN": "Republic of Cyprus", - "POP_EST": 1221549, - "POP_RANK": 12, - "GDP_MD_EST": 29260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CY", - "ISO_A3": "CYP", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -53, - 218, - 219, - 220 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Czechia", - "NAME_LONG": "Czech Republic", - "ABBREV": "Cz.", - "FORMAL_EN": "Czech Republic", - "POP_EST": 10674723, - "POP_RANK": 14, - "GDP_MD_EST": 350900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CZ", - "ISO_A3": "CZE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -52, - -163, - 221, - 222, - -63, - 223, - 224, - 225, - 226, - 227, - -219 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Germany", - "NAME_LONG": "Germany", - "ABBREV": "Ger.", - "FORMAL_EN": "Federal Republic of Germany", - "POP_EST": 80594017, - "POP_RANK": 16, - "GDP_MD_EST": 3979000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DE", - "ISO_A3": "DEU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 228, - 229, - 230, - 231 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Djibouti", - "NAME_LONG": "Djibouti", - "ABBREV": "Dji.", - "FORMAL_EN": "Republic of Djibouti", - "POP_EST": 865267, - "POP_RANK": 11, - "GDP_MD_EST": 3345, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DJ", - "ISO_A3": "DJI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -226, - 232 - ] - ], - [ - [ - 233 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Denmark", - "NAME_LONG": "Denmark", - "ABBREV": "Den.", - "FORMAL_EN": "Kingdom of Denmark", - "POP_EST": 5605948, - "POP_RANK": 13, - "GDP_MD_EST": 264800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DK", - "ISO_A3": "DNK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 234, - 235 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dominican Rep.", - "NAME_LONG": "Dominican Republic", - "ABBREV": "Dom. Rep.", - "FORMAL_EN": "Dominican Republic", - "POP_EST": 10734247, - "POP_RANK": 14, - "GDP_MD_EST": 161900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DO", - "ISO_A3": "DOM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 236, - 237, - 238, - 239, - 240, - 241, - 242, - 243 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Algeria", - "NAME_LONG": "Algeria", - "ABBREV": "Alg.", - "FORMAL_EN": "People's Democratic Republic of Algeria", - "POP_EST": 40969443, - "POP_RANK": 15, - "GDP_MD_EST": 609400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DZ", - "ISO_A3": "DZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -206, - 244, - 245 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ecuador", - "NAME_LONG": "Ecuador", - "ABBREV": "Ecu.", - "FORMAL_EN": "Republic of Ecuador", - "POP_EST": 16290913, - "POP_RANK": 14, - "GDP_MD_EST": 182400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EC", - "ISO_A3": "ECU", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 246, - 247, - 248, - 249, - 250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Egypt", - "NAME_LONG": "Egypt", - "ABBREV": "Egypt", - "FORMAL_EN": "Arab Republic of Egypt", - "POP_EST": 97041072, - "POP_RANK": 16, - "GDP_MD_EST": 1105000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EG", - "ISO_A3": "EGY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -232, - 251, - 252, - 253 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eritrea", - "NAME_LONG": "Eritrea", - "ABBREV": "Erit.", - "FORMAL_EN": "State of Eritrea", - "POP_EST": 5918919, - "POP_RANK": 13, - "GDP_MD_EST": 9169, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ER", - "ISO_A3": "ERI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 254, - 255, - 256, - 257 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Spain", - "NAME_LONG": "Spain", - "ABBREV": "Sp.", - "FORMAL_EN": "Kingdom of Spain", - "POP_EST": 48958159, - "POP_RANK": 15, - "GDP_MD_EST": 1690000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ES", - "ISO_A3": "ESP", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 258, - 259, - 260 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Estonia", - "NAME_LONG": "Estonia", - "ABBREV": "Est.", - "FORMAL_EN": "Republic of Estonia", - "POP_EST": 1251581, - "POP_RANK": 12, - "GDP_MD_EST": 38700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EE", - "ISO_A3": "EST", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -231, - 261, - 262, - 263, - 264, - 265, - -252 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ethiopia", - "NAME_LONG": "Ethiopia", - "ABBREV": "Eth.", - "FORMAL_EN": "Federal Democratic Republic of Ethiopia", - "POP_EST": 105350020, - "POP_RANK": 17, - "GDP_MD_EST": 174700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ET", - "ISO_A3": "ETH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 266, - 267, - 268, - 269 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Finland", - "NAME_LONG": "Finland", - "ABBREV": "Fin.", - "FORMAL_EN": "Republic of Finland", - "POP_EST": 5491218, - "POP_RANK": 13, - "GDP_MD_EST": 224137, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FI", - "ISO_A3": "FIN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - [ - 270 - ] - ], - [ - [ - 271 - ] - ], - [ - [ - 272 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Fiji", - "NAME_LONG": "Fiji", - "ABBREV": "Fiji", - "FORMAL_EN": "Republic of Fiji", - "POP_EST": 920938, - "POP_RANK": 11, - "GDP_MD_EST": 8374, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FJ", - "ISO_A3": "FJI", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - 273 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Falkland Is.", - "NAME_LONG": "Falkland Islands", - "ABBREV": "Flk. Is.", - "FORMAL_EN": "Falkland Islands", - "POP_EST": 2931, - "POP_RANK": 4, - "GDP_MD_EST": 281.8, - "POP_YEAR": 2014, - "GDP_YEAR": 2012, - "ISO_A2": "FK", - "ISO_A3": "FLK", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -65, - 274, - -222, - -162, - 275, - 276, - -256, - 277 - ] - ], - [ - [ - -111, - 278, - 279 - ] - ], - [ - [ - 280 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "France", - "NAME_LONG": "France", - "ABBREV": "Fr.", - "FORMAL_EN": "French Republic", - "POP_EST": 67106161, - "POP_RANK": 16, - "GDP_MD_EST": 2699000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FR", - "ISO_A3": "FRA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -190, - -204, - 281, - 282 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gabon", - "NAME_LONG": "Gabon", - "ABBREV": "Gabon", - "FORMAL_EN": "Gabonese Republic", - "POP_EST": 1772255, - "POP_RANK": 12, - "GDP_MD_EST": 35980, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GA", - "ISO_A3": "GAB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 283, - 284 - ] - ], - [ - [ - 285 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United Kingdom", - "NAME_LONG": "United Kingdom", - "ABBREV": "U.K.", - "FORMAL_EN": "United Kingdom of Great Britain and Northern Ireland", - "POP_EST": 64769452, - "POP_RANK": 16, - "GDP_MD_EST": 2788000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GB", - "ISO_A3": "GBR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -32, - 286, - 287, - 288, - -55 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Georgia", - "NAME_LONG": "Georgia", - "ABBREV": "Geo.", - "FORMAL_EN": "Georgia", - "POP_EST": 4926330, - "POP_RANK": 12, - "GDP_MD_EST": 37270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GE", - "ISO_A3": "GEO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -74, - 289, - 290, - -184 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ghana", - "NAME_LONG": "Ghana", - "ABBREV": "Ghana", - "FORMAL_EN": "Republic of Ghana", - "POP_EST": 27499924, - "POP_RANK": 15, - "GDP_MD_EST": 120800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GH", - "ISO_A3": "GHA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -187, - 291, - 292, - 293, - 294, - 295, - 296 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea", - "NAME_LONG": "Guinea", - "ABBREV": "Gin.", - "FORMAL_EN": "Republic of Guinea", - "POP_EST": 12413867, - "POP_RANK": 14, - "GDP_MD_EST": 16080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GN", - "ISO_A3": "GIN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 297, - 298 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gambia", - "NAME_LONG": "The Gambia", - "ABBREV": "Gambia", - "FORMAL_EN": "Republic of the Gambia", - "POP_EST": 2051363, - "POP_RANK": 12, - "GDP_MD_EST": 3387, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GM", - "ISO_A3": "GMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -295, - 299, - 300 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea-Bissau", - "NAME_LONG": "Guinea-Bissau", - "ABBREV": "GnB.", - "FORMAL_EN": "Republic of Guinea-Bissau", - "POP_EST": 1792338, - "POP_RANK": 12, - "GDP_MD_EST": 2851, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GW", - "ISO_A3": "GNB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -191, - -283, - 301 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eq. Guinea", - "NAME_LONG": "Equatorial Guinea", - "ABBREV": "Eq. G.", - "FORMAL_EN": "Republic of Equatorial Guinea", - "POP_EST": 778358, - "POP_RANK": 11, - "GDP_MD_EST": 31770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GQ", - "ISO_A3": "GNQ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - -14, - 302, - -84, - 303, - 304 - ] - ], - [ - [ - 305 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Greece", - "NAME_LONG": "Greece", - "ABBREV": "Greece", - "FORMAL_EN": "Hellenic Republic", - "POP_EST": 10768477, - "POP_RANK": 14, - "GDP_MD_EST": 290500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GR", - "ISO_A3": "GRC", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 306 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Greenland", - "NAME_LONG": "Greenland", - "ABBREV": "Grlnd.", - "FORMAL_EN": "Greenland", - "POP_EST": 57713, - "POP_RANK": 8, - "GDP_MD_EST": 2173, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "GL", - "ISO_A3": "GRL", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -100, - 307, - 308, - 309, - 310, - 311 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guatemala", - "NAME_LONG": "Guatemala", - "ABBREV": "Guat.", - "FORMAL_EN": "Republic of Guatemala", - "POP_EST": 15460732, - "POP_RANK": 14, - "GDP_MD_EST": 131800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GT", - "ISO_A3": "GTM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -109, - 312, - 313, - 314 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guyana", - "NAME_LONG": "Guyana", - "ABBREV": "Guy.", - "FORMAL_EN": "Co-operative Republic of Guyana", - "POP_EST": 737718, - "POP_RANK": 11, - "GDP_MD_EST": 6093, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GY", - "ISO_A3": "GUY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -309, - 315, - 316, - 317, - 318 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Honduras", - "NAME_LONG": "Honduras", - "ABBREV": "Hond.", - "FORMAL_EN": "Republic of Honduras", - "POP_EST": 9038741, - "POP_RANK": 13, - "GDP_MD_EST": 43190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HN", - "ISO_A3": "HND", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -91, - 319, - 320, - 321, - 322, - 323 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Croatia", - "NAME_LONG": "Croatia", - "ABBREV": "Cro.", - "FORMAL_EN": "Republic of Croatia", - "POP_EST": 4292095, - "POP_RANK": 12, - "GDP_MD_EST": 94240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HR", - "ISO_A3": "HRV", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -236, - 324 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Haiti", - "NAME_LONG": "Haiti", - "ABBREV": "Haiti", - "FORMAL_EN": "Republic of Haiti", - "POP_EST": 10646714, - "POP_RANK": 14, - "GDP_MD_EST": 19340, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HT", - "ISO_A3": "HTI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -48, - 325, - 326, - 327, - 328, - -323, - 329 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Hungary", - "NAME_LONG": "Hungary", - "ABBREV": "Hun.", - "FORMAL_EN": "Republic of Hungary", - "POP_EST": 9850845, - "POP_RANK": 13, - "GDP_MD_EST": 267600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HU", - "ISO_A3": "HUN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 330 - ] - ], - [ - [ - 331, - 332 - ] - ], - [ - [ - 333 - ] - ], - [ - [ - 334 - ] - ], - [ - [ - 335 - ] - ], - [ - [ - 336 - ] - ], - [ - [ - 337 - ] - ], - [ - [ - 338 - ] - ], - [ - [ - 339, - 340 - ] - ], - [ - [ - 341 - ] - ], - [ - [ - 342 - ] - ], - [ - [ - 343, - 344 - ] - ], - [ - [ - 345 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Indonesia", - "NAME_LONG": "Indonesia", - "ABBREV": "Indo.", - "FORMAL_EN": "Republic of Indonesia", - "POP_EST": 260580739, - "POP_RANK": 17, - "GDP_MD_EST": 3028000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ID", - "ISO_A3": "IDN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -80, - 346, - 347, - -181, - 348, - -179, - -116, - -178, - 349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "India", - "NAME_LONG": "India", - "ABBREV": "India", - "FORMAL_EN": "Republic of India", - "POP_EST": 1281935911, - "POP_RANK": 18, - "GDP_MD_EST": 8721000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IN", - "ISO_A3": "IND", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -284, - 350 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ireland", - "NAME_LONG": "Ireland", - "ABBREV": "Ire.", - "FORMAL_EN": "Ireland", - "POP_EST": 5011102, - "POP_RANK": 13, - "GDP_MD_EST": 322000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IE", - "ISO_A3": "IRL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -6, - 351, - 352, - 353, - 354, - -59, - -34, - -58, - 355, - 356 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iran", - "NAME_LONG": "Iran", - "ABBREV": "Iran", - "FORMAL_EN": "Islamic Republic of Iran", - "POP_EST": 82021564, - "POP_RANK": 16, - "GDP_MD_EST": 1459000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IR", - "ISO_A3": "IRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -354, - 357, - 358, - 359, - 360, - 361, - 362 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iraq", - "NAME_LONG": "Iraq", - "ABBREV": "Iraq", - "FORMAL_EN": "Republic of Iraq", - "POP_EST": 39192111, - "POP_RANK": 15, - "GDP_MD_EST": 596700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IQ", - "ISO_A3": "IRQ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 363 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iceland", - "NAME_LONG": "Iceland", - "ABBREV": "Iceland", - "FORMAL_EN": "Republic of Iceland", - "POP_EST": 339747, - "POP_RANK": 10, - "GDP_MD_EST": 16150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IS", - "ISO_A3": "ISL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 364, - 365, - 366, - 367, - 368, - 369, - -250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Israel", - "NAME_LONG": "Israel", - "ABBREV": "Isr.", - "FORMAL_EN": "State of Israel", - "POP_EST": 8299706, - "POP_RANK": 13, - "GDP_MD_EST": 297000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IL", - "ISO_A3": "ISR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - -50, - 370, - 371, - -276, - -161 - ] - ], - [ - [ - 372 - ] - ], - [ - [ - 373 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Italy", - "NAME_LONG": "Italy", - "ABBREV": "Italy", - "FORMAL_EN": "Italian Republic", - "POP_EST": 62137802, - "POP_RANK": 16, - "GDP_MD_EST": 2221000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IT", - "ISO_A3": "ITA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 374 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jamaica", - "NAME_LONG": "Jamaica", - "ABBREV": "Jam.", - "FORMAL_EN": "Jamaica", - "POP_EST": 2990561, - "POP_RANK": 12, - "GDP_MD_EST": 25390, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JM", - "ISO_A3": "JAM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -361, - 375, - 376, - -370, - 377, - -368, - 378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jordan", - "NAME_LONG": "Jordan", - "ABBREV": "Jord.", - "FORMAL_EN": "Hashemite Kingdom of Jordan", - "POP_EST": 10248069, - "POP_RANK": 14, - "GDP_MD_EST": 86190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JO", - "ISO_A3": "JOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 379 - ] - ], - [ - [ - 380 - ] - ], - [ - [ - 381 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Japan", - "NAME_LONG": "Japan", - "ABBREV": "Japan", - "FORMAL_EN": "Japan", - "POP_EST": 126451398, - "POP_RANK": 17, - "GDP_MD_EST": 4932000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JP", - "ISO_A3": "JPN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -169, - 382, - 383, - 384, - 385, - 386 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kazakhstan", - "NAME_LONG": "Kazakhstan", - "ABBREV": "Kaz.", - "FORMAL_EN": "Republic of Kazakhstan", - "POP_EST": 18556698, - "POP_RANK": 14, - "GDP_MD_EST": 460700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KZ", - "ISO_A3": "KAZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -264, - 387, - 388, - 389, - 390, - 391 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kenya", - "NAME_LONG": "Kenya", - "ABBREV": "Ken.", - "FORMAL_EN": "Republic of Kenya", - "POP_EST": 47615739, - "POP_RANK": 15, - "GDP_MD_EST": 152700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KE", - "ISO_A3": "KEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -168, - 392, - 393, - -383 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kyrgyzstan", - "NAME_LONG": "Kyrgyzstan", - "ABBREV": "Kgz.", - "FORMAL_EN": "Kyrgyz Republic", - "POP_EST": 5789122, - "POP_RANK": 13, - "GDP_MD_EST": 21010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KG", - "ISO_A3": "KGZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - 394, - 395, - 396, - 397 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cambodia", - "NAME_LONG": "Cambodia", - "ABBREV": "Camb.", - "FORMAL_EN": "Kingdom of Cambodia", - "POP_EST": 16204486, - "POP_RANK": 14, - "GDP_MD_EST": 58940, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KH", - "ISO_A3": "KHM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 398, - 399 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Korea", - "NAME_LONG": "Republic of Korea", - "ABBREV": "S.K.", - "FORMAL_EN": "Republic of Korea", - "POP_EST": 51181299, - "POP_RANK": 16, - "GDP_MD_EST": 1929000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KR", - "ISO_A3": "KOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -17, - 400, - 401, - 402 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kosovo", - "NAME_LONG": "Kosovo", - "ABBREV": "Kos.", - "FORMAL_EN": "Republic of Kosovo", - "POP_EST": 1895250, - "POP_RANK": 12, - "GDP_MD_EST": 18490, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "XK", - "ISO_A3": "-99", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -359, - 403, - 404 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kuwait", - "NAME_LONG": "Kuwait", - "ABBREV": "Kwt.", - "FORMAL_EN": "State of Kuwait", - "POP_EST": 2875422, - "POP_RANK": 12, - "GDP_MD_EST": 301100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KW", - "ISO_A3": "KWT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -176, - 405, - -396, - 406, - 407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Laos", - "NAME_LONG": "Lao PDR", - "ABBREV": "Laos", - "FORMAL_EN": "Lao People's Democratic Republic", - "POP_EST": 7126706, - "POP_RANK": 13, - "GDP_MD_EST": 40960, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LA", - "ISO_A3": "LAO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -366, - 408, - 409 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lebanon", - "NAME_LONG": "Lebanon", - "ABBREV": "Leb.", - "FORMAL_EN": "Lebanese Republic", - "POP_EST": 6229794, - "POP_RANK": 13, - "GDP_MD_EST": 85160, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LB", - "ISO_A3": "LBN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -186, - 410, - 411, - -292 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Liberia", - "NAME_LONG": "Liberia", - "ABBREV": "Liberia", - "FORMAL_EN": "Republic of Liberia", - "POP_EST": 4689021, - "POP_RANK": 12, - "GDP_MD_EST": 3881, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LR", - "ISO_A3": "LBR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -243, - 412, - 413, - -248, - 414, - 415, - 416 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Libya", - "NAME_LONG": "Libya", - "ABBREV": "Libya", - "FORMAL_EN": "Libya", - "POP_EST": 6653210, - "POP_RANK": 13, - "GDP_MD_EST": 90890, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LY", - "ISO_A3": "LBY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 417 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sri Lanka", - "NAME_LONG": "Sri Lanka", - "ABBREV": "Sri L.", - "FORMAL_EN": "Democratic Socialist Republic of Sri Lanka", - "POP_EST": 22409381, - "POP_RANK": 15, - "GDP_MD_EST": 236700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LK", - "ISO_A3": "LKA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 418 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lesotho", - "NAME_LONG": "Lesotho", - "ABBREV": "Les.", - "FORMAL_EN": "Kingdom of Lesotho", - "POP_EST": 1958042, - "POP_RANK": 12, - "GDP_MD_EST": 6019, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LS", - "ISO_A3": "LSO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -93, - 419, - 420, - 421, - 422 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lithuania", - "NAME_LONG": "Lithuania", - "ABBREV": "Lith.", - "FORMAL_EN": "Republic of Lithuania", - "POP_EST": 2823859, - "POP_RANK": 12, - "GDP_MD_EST": 85620, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LT", - "ISO_A3": "LTU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -64, - -223, - -275 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Luxembourg", - "NAME_LONG": "Luxembourg", - "ABBREV": "Lux.", - "FORMAL_EN": "Grand Duchy of Luxembourg", - "POP_EST": 594130, - "POP_RANK": 11, - "GDP_MD_EST": 58740, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LU", - "ISO_A3": "LUX", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -94, - -423, - 423, - -261, - 424 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Latvia", - "NAME_LONG": "Latvia", - "ABBREV": "Lat.", - "FORMAL_EN": "Republic of Latvia", - "POP_EST": 1944643, - "POP_RANK": 12, - "GDP_MD_EST": 50650, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LV", - "ISO_A3": "LVA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -240, - 425, - 426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Morocco", - "NAME_LONG": "Morocco", - "ABBREV": "Mor.", - "FORMAL_EN": "Kingdom of Morocco", - "POP_EST": 33986655, - "POP_RANK": 15, - "GDP_MD_EST": 282800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MA", - "ISO_A3": "MAR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 427, - 428 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Moldova", - "NAME_LONG": "Moldova", - "ABBREV": "Mda.", - "FORMAL_EN": "Republic of Moldova", - "POP_EST": 3474121, - "POP_RANK": 12, - "GDP_MD_EST": 18540, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MD", - "ISO_A3": "MDA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 429 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Madagascar", - "NAME_LONG": "Madagascar", - "ABBREV": "Mad.", - "FORMAL_EN": "Republic of Madagascar", - "POP_EST": 25054161, - "POP_RANK": 15, - "GDP_MD_EST": 36860, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MG", - "ISO_A3": "MDG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -98, - -312, - 430, - 431, - 432 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mexico", - "NAME_LONG": "Mexico", - "ABBREV": "Mex.", - "FORMAL_EN": "United Mexican States", - "POP_EST": 124574795, - "POP_RANK": 17, - "GDP_MD_EST": 2307000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MX", - "ISO_A3": "MEX", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -18, - -403, - 433, - -85, - -303 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Macedonia", - "NAME_LONG": "Macedonia", - "ABBREV": "Mkd.", - "FORMAL_EN": "Former Yugoslav Republic of Macedonia", - "POP_EST": 2103721, - "POP_RANK": 12, - "GDP_MD_EST": 29520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MK", - "ISO_A3": "MKD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -76, - -188, - -297, - 434, - 435, - -237, - 436 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mali", - "NAME_LONG": "Mali", - "ABBREV": "Mali", - "FORMAL_EN": "Republic of Mali", - "POP_EST": 17885245, - "POP_RANK": 14, - "GDP_MD_EST": 38090, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ML", - "ISO_A3": "MLI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -78, - -350, - -177, - -408, - 437, - 438 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Myanmar", - "NAME_LONG": "Myanmar", - "ABBREV": "Myan.", - "FORMAL_EN": "Republic of the Union of Myanmar", - "POP_EST": 55123814, - "POP_RANK": 16, - "GDP_MD_EST": 311100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MM", - "ISO_A3": "MMR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -16, - 439, - -320, - -90, - 440, - -401 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Montenegro", - "NAME_LONG": "Montenegro", - "ABBREV": "Mont.", - "FORMAL_EN": "Montenegro", - "POP_EST": 642550, - "POP_RANK": 11, - "GDP_MD_EST": 10610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ME", - "ISO_A3": "MNE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -171, - 441 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mongolia", - "NAME_LONG": "Mongolia", - "ABBREV": "Mong.", - "FORMAL_EN": "Mongolia", - "POP_EST": 3068243, - "POP_RANK": 12, - "GDP_MD_EST": 37000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MN", - "ISO_A3": "MNG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - 442, - 443, - 444, - 445, - 446, - 447, - 448, - 449 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mozambique", - "NAME_LONG": "Mozambique", - "ABBREV": "Moz.", - "FORMAL_EN": "Republic of Mozambique", - "POP_EST": 26573706, - "POP_RANK": 15, - "GDP_MD_EST": 35010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MZ", - "ISO_A3": "MOZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -238, - -436, - 450, - 451, - 452 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mauritania", - "NAME_LONG": "Mauritania", - "ABBREV": "Mrt.", - "FORMAL_EN": "Islamic Republic of Mauritania", - "POP_EST": 3758571, - "POP_RANK": 12, - "GDP_MD_EST": 16710, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MR", - "ISO_A3": "MRT", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -450, - 453, - 454 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Malawi", - "NAME_LONG": "Malawi", - "ABBREV": "Mal.", - "FORMAL_EN": "Republic of Malawi", - "POP_EST": 19196246, - "POP_RANK": 14, - "GDP_MD_EST": 21200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MW", - "ISO_A3": "MWI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -115, - 455, - -344, - 456 - ] - ], - [ - [ - 457, - 458 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Malaysia", - "NAME_LONG": "Malaysia", - "ABBREV": "Malay.", - "FORMAL_EN": "Malaysia", - "POP_EST": 31381992, - "POP_RANK": 15, - "GDP_MD_EST": 863000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MY", - "ISO_A3": "MYS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -7, - 459, - -119, - 460, - 461 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Namibia", - "NAME_LONG": "Namibia", - "ABBREV": "Nam.", - "FORMAL_EN": "Republic of Namibia", - "POP_EST": 2484780, - "POP_RANK": 12, - "GDP_MD_EST": 25990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NA", - "ISO_A3": "NAM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 462 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "New Caledonia", - "NAME_LONG": "New Caledonia", - "ABBREV": "New C.", - "FORMAL_EN": "New Caledonia", - "POP_EST": 279070, - "POP_RANK": 10, - "GDP_MD_EST": 10770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NC", - "ISO_A3": "NCL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -71, - -77, - -437, - -244, - -417, - 463, - -194, - 464 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Niger", - "NAME_LONG": "Niger", - "ABBREV": "Niger", - "FORMAL_EN": "Republic of Niger", - "POP_EST": 19245344, - "POP_RANK": 14, - "GDP_MD_EST": 20150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NE", - "ISO_A3": "NER", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -72, - -465, - -193, - 465 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nigeria", - "NAME_LONG": "Nigeria", - "ABBREV": "Nigeria", - "FORMAL_EN": "Federal Republic of Nigeria", - "POP_EST": 190632261, - "POP_RANK": 17, - "GDP_MD_EST": 1089000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NG", - "ISO_A3": "NGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -212, - 466, - -317, - 467 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nicaragua", - "NAME_LONG": "Nicaragua", - "ABBREV": "Nic.", - "FORMAL_EN": "Republic of Nicaragua", - "POP_EST": 6025951, - "POP_RANK": 13, - "GDP_MD_EST": 33550, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NI", - "ISO_A3": "NIC", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -67, - 468, - -224 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Netherlands", - "NAME_LONG": "Netherlands", - "ABBREV": "Neth.", - "FORMAL_EN": "Kingdom of the Netherlands", - "POP_EST": 17084719, - "POP_RANK": 14, - "GDP_MD_EST": 870800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NL", - "ISO_A3": "NLD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -268, - 469, - 470, - 471 - ] - ], - [ - [ - 472 - ] - ], - [ - [ - 473 - ] - ], - [ - [ - 474 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Norway", - "NAME_LONG": "Norway", - "ABBREV": "Nor.", - "FORMAL_EN": "Kingdom of Norway", - "POP_EST": 5320045, - "POP_RANK": 13, - "GDP_MD_EST": 364700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NO", - "ISO_A3": "NOR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -180, - -349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nepal", - "NAME_LONG": "Nepal", - "ABBREV": "Nepal", - "FORMAL_EN": "Nepal", - "POP_EST": 29384297, - "POP_RANK": 15, - "GDP_MD_EST": 71520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NP", - "ISO_A3": "NPL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 475 - ] - ], - [ - [ - 476 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "New Zealand", - "NAME_LONG": "New Zealand", - "ABBREV": "N.Z.", - "FORMAL_EN": "New Zealand", - "POP_EST": 4510327, - "POP_RANK": 12, - "GDP_MD_EST": 174800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NZ", - "ISO_A3": "NZL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - [ - -20, - 477 - ] - ], - [ - [ - -22, - 478, - 479, - 480 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Oman", - "NAME_LONG": "Oman", - "ABBREV": "Oman", - "FORMAL_EN": "Sultanate of Oman", - "POP_EST": 3424386, - "POP_RANK": 12, - "GDP_MD_EST": 173100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "OM", - "ISO_A3": "OMN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -5, - -182, - -348, - 481, - -352 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Pakistan", - "NAME_LONG": "Pakistan", - "ABBREV": "Pak.", - "FORMAL_EN": "Islamic Republic of Pakistan", - "POP_EST": 204924861, - "POP_RANK": 17, - "GDP_MD_EST": 988200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PK", - "ISO_A3": "PAK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -208, - 482, - -214, - 483 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Panama", - "NAME_LONG": "Panama", - "ABBREV": "Pan.", - "FORMAL_EN": "Republic of Panama", - "POP_EST": 3753142, - "POP_RANK": 12, - "GDP_MD_EST": 93120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PA", - "ISO_A3": "PAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -102, - -166, - 484, - -245, - -205, - -106 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Peru", - "NAME_LONG": "Peru", - "ABBREV": "Peru", - "FORMAL_EN": "Republic of Peru", - "POP_EST": 31036656, - "POP_RANK": 15, - "GDP_MD_EST": 410400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PE", - "ISO_A3": "PER", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - 485 - ] - ], - [ - [ - 486 - ] - ], - [ - [ - 487 - ] - ], - [ - [ - 488 - ] - ], - [ - [ - 489 - ] - ], - [ - [ - 490 - ] - ], - [ - [ - 491 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Philippines", - "NAME_LONG": "Philippines", - "ABBREV": "Phil.", - "FORMAL_EN": "Republic of the Philippines", - "POP_EST": 104256076, - "POP_RANK": 17, - "GDP_MD_EST": 801900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PH", - "ISO_A3": "PHL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - -340, - 492 - ] - ], - [ - [ - 493 - ] - ], - [ - [ - 494 - ] - ], - [ - [ - 495 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Papua New Guinea", - "NAME_LONG": "Papua New Guinea", - "ABBREV": "P.N.G.", - "FORMAL_EN": "Independent State of Papua New Guinea", - "POP_EST": 6909701, - "POP_RANK": 13, - "GDP_MD_EST": 28020, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PG", - "ISO_A3": "PNG", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -97, - 496, - 497, - -220, - -228, - 498, - 499, - -420 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Poland", - "NAME_LONG": "Poland", - "ABBREV": "Pol.", - "FORMAL_EN": "Republic of Poland", - "POP_EST": 38476269, - "POP_RANK": 15, - "GDP_MD_EST": 1052000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PL", - "ISO_A3": "POL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 500 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Puerto Rico", - "NAME_LONG": "Puerto Rico", - "ABBREV": "P.R.", - "FORMAL_EN": "Commonwealth of Puerto Rico", - "POP_EST": 3351827, - "POP_RANK": 12, - "GDP_MD_EST": 131000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PR", - "ISO_A3": "PRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -173, - 501, - 502, - -400, - 503 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "North Korea", - "NAME_LONG": "Dem. Rep. Korea", - "ABBREV": "N.K.", - "FORMAL_EN": "Democratic People's Republic of Korea", - "POP_EST": 25248140, - "POP_RANK": 15, - "GDP_MD_EST": 40000, - "POP_YEAR": 2013, - "GDP_YEAR": 2016, - "ISO_A2": "KP", - "ISO_A3": "PRK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -258, - 504 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Portugal", - "NAME_LONG": "Portugal", - "ABBREV": "Port.", - "FORMAL_EN": "Portuguese Republic", - "POP_EST": 10839514, - "POP_RANK": 14, - "GDP_MD_EST": 297100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PT", - "ISO_A3": "PRT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -28, - -104, - -105 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Paraguay", - "NAME_LONG": "Paraguay", - "ABBREV": "Para.", - "FORMAL_EN": "Republic of Paraguay", - "POP_EST": 6943739, - "POP_RANK": 13, - "GDP_MD_EST": 64670, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PY", - "ISO_A3": "PRY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -369, - -378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Palestine", - "NAME_LONG": "Palestine", - "ABBREV": "Pal.", - "FORMAL_EN": "West Bank and Gaza", - "POP_EST": 4543126, - "POP_RANK": 12, - "GDP_MD_EST": 21220.77, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PS", - "ISO_A3": "PSE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 505, - 506 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Qatar", - "NAME_LONG": "Qatar", - "ABBREV": "Qatar", - "FORMAL_EN": "State of Qatar", - "POP_EST": 2314307, - "POP_RANK": 12, - "GDP_MD_EST": 334500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "QA", - "ISO_A3": "QAT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -81, - 507, - -328, - 508, - -429, - 509, - 510 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Romania", - "NAME_LONG": "Romania", - "ABBREV": "Rom.", - "FORMAL_EN": "Romania", - "POP_EST": 21529967, - "POP_RANK": 15, - "GDP_MD_EST": 441000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RO", - "ISO_A3": "ROU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - -56, - -289, - 511, - 512, - -95, - -425, - -260, - 513, - -269, - -472, - 514, - -502, - -172, - -442, - -170, - -387, - 515 - ] - ], - [ - [ - -421, - -500, - 516 - ] - ], - [ - [ - 519 - ] - ], - [ - [ - 520 - ] - ], - [ - [ - 521 - ] - ], - [ - [ - 522 - ] - ], - [ - [ - 523 - ] - ], - [ - [ - 524 - ] - ], - [ - [ - 525 - ] - ], - [ - [ - 526 - ] - ], - [ - [ - 527 - ] - ], - [ - [ - 528 - ] - ], - [ - [ - 529 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Russia", - "NAME_LONG": "Russian Federation", - "ABBREV": "Rus.", - "FORMAL_EN": "Russian Federation", - "POP_EST": 142257519, - "POP_RANK": 17, - "GDP_MD_EST": 3745000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RU", - "ISO_A3": "RUS", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -61, - -200, - 530, - 531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Rwanda", - "NAME_LONG": "Rwanda", - "ABBREV": "Rwa.", - "FORMAL_EN": "Republic of Rwanda", - "POP_EST": 11901484, - "POP_RANK": 14, - "GDP_MD_EST": 21970, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RW", - "ISO_A3": "RWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -239, - -453, - 532, - -426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "W. Sahara", - "NAME_LONG": "Western Sahara", - "ABBREV": "W. Sah.", - "FORMAL_EN": "Sahrawi Arab Democratic Republic", - "POP_EST": 603253, - "POP_RANK": 11, - "GDP_MD_EST": 906.5, - "POP_YEAR": 2017, - "GDP_YEAR": 2007, - "ISO_A2": "EH", - "ISO_A3": "ESH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -23, - -481, - 533, - 534, - -376, - -360, - -405, - 535, - -507, - 536 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Saudi Arabia", - "NAME_LONG": "Saudi Arabia", - "ABBREV": "Saud.", - "FORMAL_EN": "Kingdom of Saudi Arabia", - "POP_EST": 28571770, - "POP_RANK": 15, - "GDP_MD_EST": 1731000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SA", - "ISO_A3": "SAU", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -123, - 537, - -415, - -247, - 538, - -253, - -266, - 539 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sudan", - "NAME_LONG": "Sudan", - "ABBREV": "Sudan", - "FORMAL_EN": "Republic of the Sudan", - "POP_EST": 37345935, - "POP_RANK": 15, - "GDP_MD_EST": 176300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SD", - "ISO_A3": "SDN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -124, - -540, - -265, - -392, - 540, - -198 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "S. Sudan", - "NAME_LONG": "South Sudan", - "ABBREV": "S. Sud.", - "FORMAL_EN": "Republic of South Sudan", - "POP_EST": 13026129, - "POP_RANK": 14, - "GDP_MD_EST": 20880, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SS", - "ISO_A3": "SSD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -296, - -301, - 541, - -299, - 542, - -451, - -435 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Senegal", - "NAME_LONG": "Senegal", - "ABBREV": "Sen.", - "FORMAL_EN": "Republic of Senegal", - "POP_EST": 14668522, - "POP_RANK": 14, - "GDP_MD_EST": 39720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SN", - "ISO_A3": "SEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - [ - 543 - ] - ], - [ - [ - 544 - ] - ], - [ - [ - 545 - ] - ], - [ - [ - 546 - ] - ], - [ - [ - 547 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Solomon Is.", - "NAME_LONG": "Solomon Islands", - "ABBREV": "S. Is.", - "FORMAL_EN": "", - "POP_EST": 647581, - "POP_RANK": 11, - "GDP_MD_EST": 1198, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SB", - "ISO_A3": "SLB", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -293, - -412, - 548 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sierra Leone", - "NAME_LONG": "Sierra Leone", - "ABBREV": "S.L.", - "FORMAL_EN": "Republic of Sierra Leone", - "POP_EST": 6163195, - "POP_RANK": 13, - "GDP_MD_EST": 10640, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SL", - "ISO_A3": "SLE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -310, - -319, - 549 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "El Salvador", - "NAME_LONG": "El Salvador", - "ABBREV": "El. S.", - "FORMAL_EN": "Republic of El Salvador", - "POP_EST": 6172011, - "POP_RANK": 13, - "GDP_MD_EST": 54790, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SV", - "ISO_A3": "SLV", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -230, - 550, - 551, - -262 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somaliland", - "NAME_LONG": "Somaliland", - "ABBREV": "Solnd.", - "FORMAL_EN": "Republic of Somaliland", - "POP_EST": 3500000, - "POP_RANK": 12, - "GDP_MD_EST": 12250, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -263, - -552, - 552, - -388 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somalia", - "NAME_LONG": "Somalia", - "ABBREV": "Som.", - "FORMAL_EN": "Federal Republic of Somalia", - "POP_EST": 7531386, - "POP_RANK": 13, - "GDP_MD_EST": 4719, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SO", - "ISO_A3": "SOM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -86, - -434, - -402, - -441, - -92, - -324, - -329, - -508 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Serbia", - "NAME_LONG": "Serbia", - "ABBREV": "Serb.", - "FORMAL_EN": "Republic of Serbia", - "POP_EST": 7111024, - "POP_RANK": 13, - "GDP_MD_EST": 101800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RS", - "ISO_A3": "SRB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -110, - -315, - 553, - -279 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Suriname", - "NAME_LONG": "Suriname", - "ABBREV": "Sur.", - "FORMAL_EN": "Republic of Suriname", - "POP_EST": 591919, - "POP_RANK": 11, - "GDP_MD_EST": 8547, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SR", - "ISO_A3": "SUR", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -54, - -221, - -498, - 554, - -326 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovakia", - "NAME_LONG": "Slovakia", - "ABBREV": "Svk.", - "FORMAL_EN": "Slovak Republic", - "POP_EST": 5445829, - "POP_RANK": 13, - "GDP_MD_EST": 168800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SK", - "ISO_A3": "SVK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -49, - -330, - -322, - 555, - -371 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovenia", - "NAME_LONG": "Slovenia", - "ABBREV": "Slo.", - "FORMAL_EN": "Republic of Slovenia", - "POP_EST": 1972126, - "POP_RANK": 12, - "GDP_MD_EST": 68350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SI", - "ISO_A3": "SVN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -267, - 556, - -470 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sweden", - "NAME_LONG": "Sweden", - "ABBREV": "Swe.", - "FORMAL_EN": "Kingdom of Sweden", - "POP_EST": 9960487, - "POP_RANK": 13, - "GDP_MD_EST": 498100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SE", - "ISO_A3": "SWE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -446, - 557 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Swaziland", - "NAME_LONG": "Swaziland", - "ABBREV": "Swz.", - "FORMAL_EN": "Kingdom of Swaziland", - "POP_EST": 1467152, - "POP_RANK": 12, - "GDP_MD_EST": 11060, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SZ", - "ISO_A3": "SWZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -362, - -379, - -367, - -410, - 558, - 559 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Syria", - "NAME_LONG": "Syria", - "ABBREV": "Syria", - "FORMAL_EN": "Syrian Arab Republic", - "POP_EST": 18028549, - "POP_RANK": 14, - "GDP_MD_EST": 50280, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "SY", - "ISO_A3": "SYR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -122, - -195, - -464, - -416, - -538 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Chad", - "NAME_LONG": "Chad", - "ABBREV": "Chad", - "FORMAL_EN": "Republic of Chad", - "POP_EST": 12075985, - "POP_RANK": 14, - "GDP_MD_EST": 30590, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TD", - "ISO_A3": "TCD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -69, - 560, - -290, - -73 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Togo", - "NAME_LONG": "Togo", - "ABBREV": "Togo", - "FORMAL_EN": "Togolese Republic", - "POP_EST": 7965055, - "POP_RANK": 13, - "GDP_MD_EST": 11610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TG", - "ISO_A3": "TGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -395, - 561, - -459, - 562, - -438, - -407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Thailand", - "NAME_LONG": "Thailand", - "ABBREV": "Thai.", - "FORMAL_EN": "Kingdom of Thailand", - "POP_EST": 68414135, - "POP_RANK": 16, - "GDP_MD_EST": 1161000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TH", - "ISO_A3": "THA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -3, - 563, - -393, - -167 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tajikistan", - "NAME_LONG": "Tajikistan", - "ABBREV": "Tjk.", - "FORMAL_EN": "Republic of Tajikistan", - "POP_EST": 8468555, - "POP_RANK": 13, - "GDP_MD_EST": 25810, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TJ", - "ISO_A3": "TJK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -1, - -357, - 564, - -385, - 565 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Turkmenistan", - "NAME_LONG": "Turkmenistan", - "ABBREV": "Turkm.", - "FORMAL_EN": "Turkmenistan", - "POP_EST": 5351277, - "POP_RANK": 13, - "GDP_MD_EST": 94720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TM", - "ISO_A3": "TKM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -332, - 566 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Timor-Leste", - "NAME_LONG": "Timor-Leste", - "ABBREV": "T.L.", - "FORMAL_EN": "Democratic Republic of Timor-Leste", - "POP_EST": 1291358, - "POP_RANK": 12, - "GDP_MD_EST": 4975, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TL", - "ISO_A3": "TLS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 567 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Trinidad and Tobago", - "NAME_LONG": "Trinidad and Tobago", - "ABBREV": "Tr.T.", - "FORMAL_EN": "Republic of Trinidad and Tobago", - "POP_EST": 1218208, - "POP_RANK": 12, - "GDP_MD_EST": 43570, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TT", - "ISO_A3": "TTO", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -242, - 568, - -413 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tunisia", - "NAME_LONG": "Tunisia", - "ABBREV": "Tun.", - "FORMAL_EN": "Republic of Tunisia", - "POP_EST": 11403800, - "POP_RANK": 14, - "GDP_MD_EST": 130800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TN", - "ISO_A3": "TUN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - [ - -36, - -355, - -363, - -560, - 569, - -287 - ] - ], - [ - [ - -83, - 570, - -304 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Turkey", - "NAME_LONG": "Turkey", - "ABBREV": "Tur.", - "FORMAL_EN": "Republic of Turkey", - "POP_EST": 80845215, - "POP_RANK": 16, - "GDP_MD_EST": 1670000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TR", - "ISO_A3": "TUR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 571 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Taiwan", - "NAME_LONG": "Taiwan", - "ABBREV": "Taiwan", - "FORMAL_EN": "", - "POP_EST": 23508428, - "POP_RANK": 15, - "GDP_MD_EST": 1127000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TW", - "ISO_A3": "TWN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -62, - -532, - 572, - -390, - 573, - -443, - -455, - 574, - -201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tanzania", - "NAME_LONG": "Tanzania", - "ABBREV": "Tanz.", - "FORMAL_EN": "United Republic of Tanzania", - "POP_EST": 53950935, - "POP_RANK": 16, - "GDP_MD_EST": 150600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TZ", - "ISO_A3": "TZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -199, - -541, - -391, - -573, - -531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uganda", - "NAME_LONG": "Uganda", - "ABBREV": "Uga.", - "FORMAL_EN": "Republic of Uganda", - "POP_EST": 39570125, - "POP_RANK": 15, - "GDP_MD_EST": 84930, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UG", - "ISO_A3": "UGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -96, - -513, - 575, - -518, - 576, - -510, - -428, - -509, - -327, - -555, - -497 - ], - [ - 517, - 518 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ukraine", - "NAME_LONG": "Ukraine", - "ABBREV": "Ukr.", - "FORMAL_EN": "Ukraine", - "POP_EST": 44033874, - "POP_RANK": 15, - "GDP_MD_EST": 352600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UA", - "ISO_A3": "UKR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -30, - -113, - 577 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uruguay", - "NAME_LONG": "Uruguay", - "ABBREV": "Ury.", - "FORMAL_EN": "Oriental Republic of Uruguay", - "POP_EST": 3360148, - "POP_RANK": 12, - "GDP_MD_EST": 73250, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UY", - "ISO_A3": "URY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -141, - 578, - -432, - 579 - ] - ], - [ - [ - -139, - 580 - ] - ], - [ - [ - 581 - ] - ], - [ - [ - 582 - ] - ], - [ - [ - 583 - ] - ], - [ - [ - 584 - ] - ], - [ - [ - 585 - ] - ], - [ - [ - 586 - ] - ], - [ - [ - 587 - ] - ], - [ - [ - 588 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United States of America", - "NAME_LONG": "United States", - "ABBREV": "U.S.A.", - "FORMAL_EN": "United States of America", - "POP_EST": 326625791, - "POP_RANK": 17, - "GDP_MD_EST": 18560000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "US", - "ISO_A3": "USA", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -2, - -566, - -384, - -394, - -564 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uzbekistan", - "NAME_LONG": "Uzbekistan", - "ABBREV": "Uzb.", - "FORMAL_EN": "Republic of Uzbekistan", - "POP_EST": 29748859, - "POP_RANK": 15, - "GDP_MD_EST": 202300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UZ", - "ISO_A3": "UZB", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -108, - -210, - 589, - -313 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Venezuela", - "NAME_LONG": "Venezuela", - "ABBREV": "Ven.", - "FORMAL_EN": "Bolivarian Republic of Venezuela", - "POP_EST": 31304016, - "POP_RANK": 15, - "GDP_MD_EST": 468600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VE", - "ISO_A3": "VEN", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -175, - 590, - -397, - -406 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Vietnam", - "NAME_LONG": "Vietnam", - "ABBREV": "Viet.", - "FORMAL_EN": "Socialist Republic of Vietnam", - "POP_EST": 96160163, - "POP_RANK": 16, - "GDP_MD_EST": 594900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VN", - "ISO_A3": "VNM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - 591 - ] - ], - [ - [ - 592 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Vanuatu", - "NAME_LONG": "Vanuatu", - "ABBREV": "Van.", - "FORMAL_EN": "Republic of Vanuatu", - "POP_EST": 282814, - "POP_RANK": 10, - "GDP_MD_EST": 723, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VU", - "ISO_A3": "VUT", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -480, - 593, - -534 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Yemen", - "NAME_LONG": "Yemen", - "ABBREV": "Yem.", - "FORMAL_EN": "Republic of Yemen", - "POP_EST": 28036829, - "POP_RANK": 15, - "GDP_MD_EST": 73450, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "YE", - "ISO_A3": "YEM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -118, - 594, - -447, - -558, - -445, - 595, - -461 - ], - [ - -419 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Africa", - "NAME_LONG": "South Africa", - "ABBREV": "S.Af.", - "FORMAL_EN": "Republic of South Africa", - "POP_EST": 54841552, - "POP_RANK": 16, - "GDP_MD_EST": 739100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZA", - "ISO_A3": "ZAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -10, - -202, - -575, - -454, - -449, - 596, - -120, - -460 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zambia", - "NAME_LONG": "Zambia", - "ABBREV": "Zambia", - "FORMAL_EN": "Republic of Zambia", - "POP_EST": 15972000, - "POP_RANK": 14, - "GDP_MD_EST": 65170, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZM", - "ISO_A3": "ZMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -121, - -597, - -448, - -595 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zimbabwe", - "NAME_LONG": "Zimbabwe", - "ABBREV": "Zimb.", - "FORMAL_EN": "Republic of Zimbabwe", - "POP_EST": 13805084, - "POP_RANK": 14, - "GDP_MD_EST": 28330, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZW", - "ISO_A3": "ZWE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - } - ] - } - } -} diff --git a/explorer/src/components/ComponentError.tsx b/explorer/src/components/ComponentError.tsx deleted file mode 100644 index 00b448fb9e..0000000000 --- a/explorer/src/components/ComponentError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; - -export const ComponentError: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }} - variant="body1" - data-testid="delegation-total-amount" - > - {text} - </Typography> -); diff --git a/explorer/src/components/ContentCard.tsx b/explorer/src/components/ContentCard.tsx deleted file mode 100644 index c78c719ca7..0000000000 --- a/explorer/src/components/ContentCard.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Card, CardHeader, CardContent, Typography } from '@mui/material'; -import React, { ReactEventHandler } from 'react'; - -type ContentCardProps = { - title?: React.ReactNode; - subtitle?: string; - Icon?: React.ReactNode; - Action?: React.ReactNode; - errorMsg?: string; - onClick?: ReactEventHandler; -}; - -export const ContentCard: FCWithChildren<ContentCardProps> = ({ - title, - Icon, - Action, - subtitle, - errorMsg, - children, - onClick, -}) => ( - <Card onClick={onClick} sx={{ height: '100%' }}> - {title && <CardHeader title={title || ''} avatar={Icon} action={Action} subheader={subtitle} />} - {children && <CardContent>{children}</CardContent>} - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {errorMsg} - </Typography> - )} - </Card> -); - -ContentCard.defaultProps = { - title: undefined, - subtitle: undefined, - Icon: null, - Action: null, - errorMsg: undefined, - onClick: () => null, -}; diff --git a/explorer/src/components/CustomColumnHeading.tsx b/explorer/src/components/CustomColumnHeading.tsx deleted file mode 100644 index 1f85806fa2..0000000000 --- a/explorer/src/components/CustomColumnHeading.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import { Box, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; - -export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltipInfo?: string }> = ({ - headingTitle, - tooltipInfo, -}) => { - const theme = useTheme(); - - return ( - <Box alignItems="center" display="flex"> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={headingTitle} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - )} - <Typography variant="body2" fontWeight={600} data-testid={headingTitle}> - {headingTitle} - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Delegations/ConfirmationModal.tsx b/explorer/src/components/Delegations/ConfirmationModal.tsx deleted file mode 100644 index 8e859f5eba..0000000000 --- a/explorer/src/components/Delegations/ConfirmationModal.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { - Breakpoint, - Button, - Paper, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - SxProps, - Typography, -} from '@mui/material'; - -export interface ConfirmationModalProps { - open: boolean; - onConfirm: () => void; - onClose?: () => void; - children?: React.ReactNode; - title: React.ReactNode | string; - subTitle?: React.ReactNode | string; - confirmButton: React.ReactNode | string; - disabled?: boolean; - sx?: SxProps; - fullWidth?: boolean; - maxWidth?: Breakpoint; - backdropProps?: object; -} - -export const ConfirmationModal = ({ - open, - onConfirm, - onClose, - children, - title, - subTitle, - confirmButton, - disabled, - sx, - fullWidth, - maxWidth, - backdropProps, -}: ConfirmationModalProps) => { - const Title = ( - <DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}> - {title} - {subTitle && - (typeof subTitle === 'string' ? ( - <Typography fontWeight={400} variant="subtitle1" fontSize={12} color="grey"> - {subTitle} - </Typography> - ) : ( - subTitle - ))} - </DialogTitle> - ); - const ConfirmButton = - typeof confirmButton === 'string' ? ( - <Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}> - <Typography variant="button" fontSize="large"> - {confirmButton} - </Typography> - </Button> - ) : ( - confirmButton - ); - return ( - <Dialog - open={open} - onClose={onClose} - aria-labelledby="responsive-dialog-title" - maxWidth={maxWidth || 'sm'} - sx={{ textAlign: 'center', ...sx }} - fullWidth={fullWidth} - BackdropProps={backdropProps} - PaperComponent={Paper} - PaperProps={{ elevation: 0 }} - > - {Title} - <DialogContent>{children}</DialogContent> - <DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions> - </Dialog> - ); -}; diff --git a/explorer/src/components/Delegations/DelegateIconButton.tsx b/explorer/src/components/Delegations/DelegateIconButton.tsx deleted file mode 100644 index 36b581a010..0000000000 --- a/explorer/src/components/Delegations/DelegateIconButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { SxProps } from '@mui/system'; -import { useIsMobile } from '@src/hooks'; -import { DelegateIcon } from '@src/icons/DelevateSVG'; - -export const DelegateIconButton: FCWithChildren<{ - size?: 'small' | 'medium'; - disabled?: boolean; - tooltip?: React.ReactNode; - sx?: SxProps; - onDelegate: () => void; -}> = ({ onDelegate, sx, disabled, size = 'medium' }) => { - const isMobile = useIsMobile(); - - const handleOnDelegate = () => { - onDelegate(); - }; - - if (isMobile) { - return ( - <IconButton size="small" disabled={disabled} onClick={handleOnDelegate}> - <DelegateIcon fontSize="small" /> - </IconButton> - ); - } - - return ( - <Button variant="outlined" size={size} disabled={disabled} onClick={handleOnDelegate} sx={sx}> - Delegate - </Button> - ); -}; diff --git a/explorer/src/components/Delegations/DelegateModal.tsx b/explorer/src/components/Delegations/DelegateModal.tsx deleted file mode 100644 index 3bafd1888f..0000000000 --- a/explorer/src/components/Delegations/DelegateModal.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState } from 'react'; -import { Box, SxProps } from '@mui/material'; -import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; -import { CurrencyDenom, DecCoin } from '@nymproject/types'; -import { useWalletContext } from '@src/context/wallet'; -import { useDelegationsContext } from '@src/context/delegations'; -import { SimpleModal } from './SimpleModal'; -import { ModalListItem } from './ModalListItem'; -import { DelegationModalProps } from './DelegationModal'; -import { validateAmount } from '../../utils/currency'; -import { urls } from '../../utils'; - -const MIN_AMOUNT_TO_DELEGATE = 10; - -export const DelegateModal: FCWithChildren<{ - mixId: number; - identityKey: string; - header?: string; - buttonText?: string; - rewardInterval?: string; - estimatedReward?: number; - profitMarginPercentage?: string | null; - nodeUptimePercentage?: number | null; - denom: CurrencyDenom; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - onOk?: (delegationModalProps: DelegationModalProps) => void; -}> = ({ mixId, identityKey, onClose, onOk, denom, sx }) => { - const [amount, setAmount] = useState<DecCoin | undefined>({ amount: '10', denom: 'nym' }); - const [isValidated, setValidated] = useState<boolean>(false); - const [errorAmount, setErrorAmount] = useState<string | undefined>(); - - const { address, balance } = useWalletContext(); - const { handleDelegate } = useDelegationsContext(); - - const validate = async () => { - let newValidatedValue = true; - let errorAmountMessage; - - if (amount && !(await validateAmount(amount.amount, '0'))) { - newValidatedValue = false; - errorAmountMessage = 'Please enter a valid amount'; - } - - if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) { - errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}`; - newValidatedValue = false; - } - - if (!amount?.amount.length) { - newValidatedValue = false; - } - - if (amount && balance.data && +balance.data - +amount.amount <= 0) { - errorAmountMessage = 'Not enough funds'; - newValidatedValue = false; - } - - setErrorAmount(errorAmountMessage); - setValidated(newValidatedValue); - }; - - const delegateToMixnode = async ({ - delegationMixId, - delegationAmount, - }: { - delegationMixId: number; - delegationAmount: string; - }) => { - try { - const tx = await handleDelegate(delegationMixId, delegationAmount); - return tx; - } catch (e) { - console.error('Failed to delegate to mixnode', e); - throw e; - } - }; - - const handleConfirm = async () => { - if (mixId && amount && onOk) { - onOk({ - status: 'loading', - }); - try { - if (!address) { - throw new Error('Please connect your wallet'); - } - - const tx = await delegateToMixnode({ - delegationMixId: mixId, - delegationAmount: amount.amount, - }); - - if (!tx) { - throw new Error('Failed to delegate'); - } - - onOk({ - status: 'success', - message: 'Delegation can take up to one hour to process', - transactions: [ - { url: `${urls('MAINNET').blockExplorer}/tx/${tx.transactionHash}`, hash: tx.transactionHash }, - ], - }); - } catch (e) { - console.error('Failed to delegate', e); - onOk({ - status: 'error', - message: (e as Error).message, - }); - } - } - }; - - const handleAmountChanged = (newAmount: DecCoin) => { - setAmount(newAmount); - }; - - React.useEffect(() => { - validate(); - }, [amount, identityKey, mixId]); - - return ( - <SimpleModal - open - onClose={onClose} - onOk={handleConfirm} - header="Delegate" - okLabel="Delegate" - okDisabled={!isValidated} - sx={sx} - > - <Box sx={{ mt: 3 }} gap={2}> - <IdentityKeyFormField - required - fullWidth - label="Node identity key" - onChanged={() => undefined} - initialValue={identityKey} - readOnly - showTickOnValid={false} - /> - </Box> - - <Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}> - <CurrencyFormField - required - fullWidth - autoFocus - label="Amount" - initialValue={amount?.amount || '10'} - onChanged={handleAmountChanged} - denom={denom} - validationError={errorAmount} - /> - </Box> - <Box sx={{ mt: 3 }}> - <ModalListItem label="Account balance" value={`${balance.data} NYM`} divider fontWeight={600} /> - </Box> - - <ModalListItem label="Est. fee for this transaction will be calculated in your connected wallet" /> - </SimpleModal> - ); -}; diff --git a/explorer/src/components/Delegations/DelegationModal.tsx b/explorer/src/components/Delegations/DelegationModal.tsx deleted file mode 100644 index db5b51aefe..0000000000 --- a/explorer/src/components/Delegations/DelegationModal.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { Typography, SxProps, Stack } from '@mui/material'; -import { Link } from '@nymproject/react/link/Link'; -import { LoadingModal } from './LoadingModal'; -import { ConfirmationModal } from './ConfirmationModal'; -import { ErrorModal } from './ErrorModal'; - -export type DelegationModalProps = { - status: 'loading' | 'success' | 'error' | 'info'; - message?: string; - transactions?: { - url: string; - hash: string; - }[]; -}; - -export const DelegationModal: FCWithChildren< - DelegationModalProps & { - open: boolean; - onClose: () => void; - sx?: SxProps; - backdropProps?: object; - children?: React.ReactNode; - } -> = ({ status, message, transactions, open, onClose, children, sx, backdropProps }) => { - if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />; - - if (status === 'error') { - return ( - <ErrorModal message={message} sx={sx} open={open} onClose={onClose}> - {children} - </ErrorModal> - ); - } - - if (status === 'info') { - return ( - <ConfirmationModal open={open} title="Connect wallet" confirmButton="OK" onConfirm={onClose}> - <Typography>{message}</Typography> - </ConfirmationModal> - ); - } - - return ( - <ConfirmationModal - open={open} - onConfirm={onClose || (() => {})} - title="Transaction successful" - confirmButton="Done" - > - <Stack alignItems="center" spacing={2} mb={0}> - {message && <Typography>{message}</Typography>} - {transactions?.length === 1 && ( - <Link href={transactions[0].url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" noIcon /> - )} - {transactions && transactions.length > 1 && ( - <Stack alignItems="center" spacing={1}> - <Typography>View the transactions on blockchain:</Typography> - {transactions.map(({ url, hash }) => ( - <Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon /> - ))} - </Stack> - )} - </Stack> - </ConfirmationModal> - ); -}; diff --git a/explorer/src/components/Delegations/ErrorModal.tsx b/explorer/src/components/Delegations/ErrorModal.tsx deleted file mode 100644 index b9218d1e1e..0000000000 --- a/explorer/src/components/Delegations/ErrorModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, SxProps, Typography } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const ErrorModal: FCWithChildren<{ - open: boolean; - title?: string; - message?: string; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - children?: React.ReactNode; -}> = ({ children, open, title, message, sx, backdropProps, onClose }) => ( - <Modal open={open} onClose={onClose} BackdropProps={backdropProps}> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Typography color={(theme) => theme.palette.error.main} mb={1}> - {title || 'Oh no! Something went wrong...'} - </Typography> - <Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}> - {message} - </Typography> - {children} - <Button variant="contained" onClick={onClose}> - Close - </Button> - </Box> - </Modal> -); diff --git a/explorer/src/components/Delegations/LoadingModal.tsx b/explorer/src/components/Delegations/LoadingModal.tsx deleted file mode 100644 index eda13938d4..0000000000 --- a/explorer/src/components/Delegations/LoadingModal.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const LoadingModal: FCWithChildren<{ - text?: string; - sx?: SxProps; - backdropProps?: object; -}> = ({ sx, text = 'Please wait...' }) => ( - <Modal open> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Stack spacing={4} direction="row" alignItems="center"> - <CircularProgress /> - <Typography sx={{ color: 'text.primary' }}>{text}</Typography> - </Stack> - </Box> - </Modal> -); diff --git a/explorer/src/components/Delegations/ModalDivider.tsx b/explorer/src/components/Delegations/ModalDivider.tsx deleted file mode 100644 index 6258e0bfac..0000000000 --- a/explorer/src/components/Delegations/ModalDivider.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Box, SxProps } from '@mui/material'; - -export const ModalDivider: FCWithChildren<{ - sx?: SxProps; -}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />; diff --git a/explorer/src/components/Delegations/ModalListItem.tsx b/explorer/src/components/Delegations/ModalListItem.tsx deleted file mode 100644 index 830c25705e..0000000000 --- a/explorer/src/components/Delegations/ModalListItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material'; -import { ModalDivider } from './ModalDivider'; - -export const ModalListItem: FCWithChildren<{ - label: string; - divider?: boolean; - hidden?: boolean; - fontWeight?: TypographyProps['fontWeight']; - fontSize?: TypographyProps['fontSize']; - light?: boolean; - value?: React.ReactNode; - sxValue?: SxProps; -}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => ( - <Box sx={{ display: hidden ? 'none' : 'block' }}> - <Stack direction="row" justifyContent="space-between" alignItems="center"> - <Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}> - {label} - </Typography> - {value && ( - <Typography - fontSize="smaller" - fontWeight={fontWeight} - sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }} - > - {value} - </Typography> - )} - </Stack> - {divider && <ModalDivider />} - </Box> -); diff --git a/explorer/src/components/Delegations/SimpleModal.tsx b/explorer/src/components/Delegations/SimpleModal.tsx deleted file mode 100644 index 88dff4d9d7..0000000000 --- a/explorer/src/components/Delegations/SimpleModal.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import ErrorOutline from '@mui/icons-material/ErrorOutline'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export const modalStyle = (width: number | string = 600) => ({ - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - width, - transform: 'translate(-50%, -50%)', - bgcolor: 'background.paper', - boxShadow: 24, - borderRadius: '16px', - p: 4, -}); - -export const StyledBackButton = ({ - onBack, - label, - fullWidth, - sx, -}: { - onBack: () => void; - label?: string; - fullWidth?: boolean; - sx?: SxProps; -}) => ( - <Button disableFocusRipple size="large" fullWidth={fullWidth} variant="outlined" onClick={onBack} sx={sx}> - {label || <ArrowBackIosNewIcon fontSize="small" />} - </Button> -); - -export const SimpleModal: FCWithChildren<{ - open: boolean; - hideCloseIcon?: boolean; - displayErrorIcon?: boolean; - displayInfoIcon?: boolean; - headerStyles?: SxProps; - subHeaderStyles?: SxProps; - buttonFullWidth?: boolean; - onClose?: () => void; - onOk?: () => Promise<void>; - onBack?: () => void; - header: string | React.ReactNode; - subHeader?: string; - okLabel: string; - backLabel?: string; - backButtonFullWidth?: boolean; - okDisabled?: boolean; - sx?: SxProps; - children?: React.ReactNode; -}> = ({ - open, - hideCloseIcon, - displayErrorIcon, - displayInfoIcon, - headerStyles, - buttonFullWidth, - onClose, - okDisabled, - onOk, - onBack, - header, - subHeader, - okLabel, - backLabel, - backButtonFullWidth, - sx, - children, -}) => { - const isMobile = useIsMobile(); - - return ( - <Modal open={open} onClose={onClose}> - <Box sx={{ ...modalStyle(isMobile ? '90%' : 600), ...sx }}> - {displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />} - {displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />} - <Stack direction="row" justifyContent="space-between" alignItems="center"> - {typeof header === 'string' ? ( - <Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}> - {header} - </Typography> - ) : ( - header - )} - {!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />} - </Stack> - - <Typography mt={subHeader ? 0.5 : 0} mb={3} fontSize={12} color={(theme) => theme.palette.text.secondary}> - {subHeader} - </Typography> - - {children} - - {(onOk || onBack) && ( - <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2, width: buttonFullWidth ? '100%' : null }}> - {onBack && <StyledBackButton onBack={onBack} label={backLabel} fullWidth={backButtonFullWidth} />} - {onOk && ( - <Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}> - {okLabel} - </Button> - )} - </Box> - )} - </Box> - </Modal> - ); -}; diff --git a/explorer/src/components/Delegations/index.ts b/explorer/src/components/Delegations/index.ts deleted file mode 100644 index da51de9bd9..0000000000 --- a/explorer/src/components/Delegations/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './ConfirmationModal'; -export * from './DelegateIconButton'; -export * from './DelegationModal'; -export * from './DelegateModal'; -export * from './ErrorModal'; -export * from './LoadingModal'; -export * from './ModalDivider'; -export * from './ModalListItem'; -export * from './SimpleModal'; -export * from './styles'; diff --git a/explorer/src/components/Delegations/styles.ts b/explorer/src/components/Delegations/styles.ts deleted file mode 100644 index 9b26551767..0000000000 --- a/explorer/src/components/Delegations/styles.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Theme } from '@mui/material/styles'; - -export const backDropStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { - style: { - left: mode === 'light' ? '0' : '50%', - width: '50%', - }, - }; -}; - -export const modalStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '25%' : '75%' }; -}; - -export const dialogStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '-50%' : '50%' }; -}; diff --git a/explorer/src/components/DetailTable.tsx b/explorer/src/components/DetailTable.tsx deleted file mode 100644 index 39355d0607..0000000000 --- a/explorer/src/components/DetailTable.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from 'react'; -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'; -import { Box } from '@mui/system'; -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?: TableCellProps['align']; - width?: string | number; - tooltipInfo?: string; -}; - -export interface UniversalTableProps<T = any> { - tableName: string; - columnsData: ColumnsType[]; - rows: T[]; -} - -function formatCellValues(val: string | number, field: string) { - if (field === 'identity_key' && typeof val === 'string') { - return ( - <Box display="flex" justifyContent="flex-end"> - <CopyToClipboard - sx={{ mr: 1, mt: 0.5, fontSize: '18px' }} - value={val} - tooltip={`Copy identity key ${val} to clipboard`} - /> - <span>{val}</span> - </Box> - ); - } - - if (field === 'bond') { - return unymToNym(val, 6); - } - - if (field === 'owner') { - return ( - <Link underline="none" color="inherit" target="_blank" href={`https://mixnet.explorers.guru/account/${val}`}> - {val} - </Link> - ); - } - - if (field === 'stake_saturation') { - return <StakeSaturationProgressBar value={Number(val)} threshold={100} />; - } - - return val; -} - -export const DetailTable: FCWithChildren<{ - tableName: string; - columnsData: ColumnsType[]; - rows: MixnodeRowType[] | GatewayEnrichedRowType[]; -}> = ({ tableName, columnsData, rows }: UniversalTableProps) => { - const theme = useTheme(); - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 1080 }} aria-label={tableName}> - <TableHead> - <TableRow> - {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' }}> - <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> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows.map((eachRow) => ( - <TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> - {columnsData?.map((data, index) => ( - <TableCell - key={data.title} - component="th" - scope="row" - variant="body" - sx={{ - padding: 2, - width: 200, - fontSize: 14, - }} - data-testid={`${data.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)} - </TableCell> - ))} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ); -}; diff --git a/explorer/src/components/Filters/Filters.tsx b/explorer/src/components/Filters/Filters.tsx deleted file mode 100644 index 0823ef6579..0000000000 --- a/explorer/src/components/Filters/Filters.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { - Button, - Dialog, - DialogContent, - DialogActions, - DialogTitle, - Slider, - Typography, - Box, - Snackbar, - Slide, - Alert, -} from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { useMainContext } from '../../context/main'; -import { MixnodeStatusWithAll, toMixnodeStatus } from '../../typeDefs/explorer-api'; -import { EnumFilterKey, TFilterItem, TFilters } from '../../typeDefs/filters'; -import { formatOnSave, generateFilterSchema } from './filterSchema'; -import { Api } from '../../api'; -import { useIsMobile } from '../../hooks/useIsMobile'; -import FiltersButton from './FiltersButton'; - -const FilterItem = ({ - label, - id, - tooltipInfo, - value, - isSmooth, - marks, - scale, - min, - max, - onChange, -}: TFilterItem & { - onChange: (id: EnumFilterKey, newValue: number[]) => void; -}) => ( - <Box sx={{ p: 2 }}> - <Typography gutterBottom>{label}</Typography> - <Typography fontSize={12}>{tooltipInfo}</Typography> - <Slider - value={value} - onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])} - valueLabelDisplay={isSmooth ? 'auto' : 'off'} - marks={marks} - step={isSmooth ? 1 : null} - scale={scale} - min={min} - max={max} - valueLabelFormat={(val: number) => (val === 100 && id === 'stakeSaturation' ? '>100' : val)} - /> - </Box> -); - -export const Filters = () => { - const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext(); - const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); - const isMobile = useIsMobile(); - - const [showFilters, setShowFilters] = useState(false); - const [isFiltered, setIsFiltered] = useState(false); - const [filters, setFilters] = React.useState<TFilters>(); - const [upperSaturationValue, setUpperSaturationValue] = React.useState<number>(100); - - const baseFilters = useRef<TFilters>(); - const prevFilters = useRef<TFilters>(); - - const handleToggleShowFilters = () => setShowFilters(!showFilters); - - const initialiseFilters = useCallback(async () => { - const allMixnodes = await Api.fetchMixnodes(); - if (allMixnodes) { - setUpperSaturationValue(Math.round(Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1)); - const initFilters = generateFilterSchema(); - baseFilters.current = initFilters; - prevFilters.current = initFilters; - setFilters(initFilters); - } - }, []); - - const handleOnChange = (id: EnumFilterKey, newValue: number[]) => { - if (id === 'stakeSaturation' && newValue[1] === 100) { - newValue.splice(1, 1, upperSaturationValue); - } - setFilters((ftrs) => { - if (ftrs) - return { - ...ftrs, - [id]: { - ...ftrs[id], - value: newValue, - }, - }; - return undefined; - }); - }; - - const handleOnSave = async () => { - setShowFilters(false); - await filterMixnodes(formatOnSave(filters!), status); - setIsFiltered(true); - prevFilters.current = filters; - }; - - const handleOnCancel = () => { - setShowFilters(false); - setFilters(prevFilters.current); - }; - - const resetFilters = () => { - setFilters(baseFilters.current); - setIsFiltered(false); - prevFilters.current = baseFilters.current; - }; - - const onClearFilters = async () => { - await fetchMixnodes(toMixnodeStatus(status)); - resetFilters(); - }; - - useEffect(() => { - initialiseFilters(); - }, [initialiseFilters]); - - useEffect(() => { - resetFilters(); - }, [status]); - - if (!filters) return null; - - return ( - <> - <Snackbar - open={isFiltered} - anchorOrigin={{ vertical: 'top', horizontal: 'center' }} - message="Filters applied" - TransitionComponent={Slide} - transitionDuration={250} - > - <Alert - severity="info" - variant={isMobile ? 'standard' : 'outlined'} - sx={{ color: (t) => t.palette.info.light }} - action={ - <Button size="small" onClick={onClearFilters}> - CLEAR FILTERS - </Button> - } - > - {mixnodes?.data?.length} mixnodes matched your criteria - </Alert> - </Snackbar> - <FiltersButton onClick={handleToggleShowFilters} fullWidth /> - <Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth> - <DialogTitle>Mixnode filters</DialogTitle> - <DialogContent dividers> - {Object.values(filters).map((v) => ( - <FilterItem {...v} key={v.id} onChange={handleOnChange} /> - ))} - </DialogContent> - <DialogActions> - <Button size="large" onClick={handleOnCancel}> - Cancel - </Button> - <Button variant="contained" size="large" onClick={handleOnSave}> - Save - </Button> - </DialogActions> - </Dialog> - </> - ); -}; diff --git a/explorer/src/components/Filters/FiltersButton.tsx b/explorer/src/components/Filters/FiltersButton.tsx deleted file mode 100644 index 1e6d3e2940..0000000000 --- a/explorer/src/components/Filters/FiltersButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { Tune } from '@mui/icons-material'; - -type FiltersButtonProps = { - iconOnly?: boolean; - fullWidth?: boolean; - onClick: () => void; -}; - -const FiltersButton = ({ iconOnly, fullWidth, onClick }: FiltersButtonProps) => { - if (iconOnly) { - return ( - <IconButton onClick={onClick} color="primary"> - <Tune /> - </IconButton> - ); - } - - return ( - <Button - fullWidth={fullWidth} - size="large" - variant="contained" - endIcon={<Tune />} - onClick={onClick} - sx={{ textTransform: 'none' }} - > - Filters - </Button> - ); -}; - -export default FiltersButton; diff --git a/explorer/src/components/Filters/filterSchema.ts b/explorer/src/components/Filters/filterSchema.ts deleted file mode 100644 index c5011114b1..0000000000 --- a/explorer/src/components/Filters/filterSchema.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { EnumFilterKey, TFilters } from '../../typeDefs/filters'; - -export const generateFilterSchema = () => ({ - profitMargin: { - label: 'Profit margin (%)', - id: EnumFilterKey.profitMargin, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: - 'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators', - }, - stakeSaturation: { - label: 'Stake saturation (%)', - id: EnumFilterKey.stakeSaturation, - value: [0, 100], - isSmooth: true, - marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({ - value: value < 100 ? value : 100, - label: value < 100 ? value : '>100', - })), - tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", - }, - routingScore: { - label: 'Routing score (%)', - id: EnumFilterKey.routingScore, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards', - }, -}); - -const formatStakeSaturationValues = ([value_1, value_2]: number[]) => { - const lowerValue = value_1 / 100; - const upperValue = value_2 / 100; - - return [lowerValue, upperValue]; -}; - -export const formatOnSave = (filters: TFilters) => ({ - routingScore: filters.routingScore.value, - profitMargin: filters.profitMargin.value, - stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value), -}); diff --git a/explorer/src/components/Footer.tsx b/explorer/src/components/Footer.tsx deleted file mode 100644 index 375b5c9b1a..0000000000 --- a/explorer/src/components/Footer.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import MuiLink from '@mui/material/Link'; -import { Link } from 'react-router-dom'; -import Typography from '@mui/material/Typography'; -import { Socials } from './Socials'; -import { useIsMobile } from '../hooks/useIsMobile'; -import { NymVpnIcon } from '../icons/NymVpn'; - -export const Footer: FCWithChildren = () => { - const isMobile = useIsMobile(); - - return ( - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - width: '100%', - height: 'auto', - mt: 3, - pt: 3, - pb: 3, - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - justifyContent: 'center', - alignItems: 'center', - mb: 2, - }} - > - <MuiLink component={Link} to="http://nymvpn.com" target="_blank" underline="none" marginRight={1}> - <NymVpnIcon /> - </MuiLink> - <Socials isFooter /> - </Box> - - <Typography - sx={{ - fontSize: 12, - textAlign: isMobile ? 'center' : 'end', - color: 'nym.muted.onDarkBg', - }} - > - © {new Date().getFullYear()} Nym Technologies SA, all rights reserved - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Gateways.ts b/explorer/src/components/Gateways.ts deleted file mode 100644 index 966a3c987e..0000000000 --- a/explorer/src/components/Gateways.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GatewayResponse, GatewayBond, GatewayReportResponse } from '../typeDefs/explorer-api'; -import { toPercentInteger } from '../utils'; - -export type GatewayRowType = { - id: string; - owner: string; - identity_key: string; - bond: number; - host: string; - location: string; - version: string; - node_performance: number; -}; - -export type GatewayEnrichedRowType = GatewayRowType & { - routingScore: string; - avgUptime: string; - clientsPort: number; - mixPort: number; -}; - -export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] { - return !arrayOfGateways - ? [] - : arrayOfGateways.map((gw) => ({ - id: gw.owner, - owner: gw.owner, - identity_key: gw.gateway.identity_key || '', - location: gw.location?.country_name.toUpperCase() || '', - bond: gw.pledge_amount.amount || 0, - host: gw.gateway.host || '', - version: gw.gateway.version || '', - node_performance: toPercentInteger(gw.node_performance.last_24h), - })); -} - -export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayReportResponse): GatewayEnrichedRowType { - return { - id: gateway.owner, - owner: gateway.owner, - identity_key: gateway.gateway.identity_key || '', - location: gateway.location?.country_name.toUpperCase() || '', - bond: gateway.pledge_amount.amount || 0, - host: gateway.gateway.host || '', - version: gateway.gateway.version || '', - clientsPort: gateway.gateway.clients_port || 0, - mixPort: gateway.gateway.mix_port || 0, - routingScore: `${report.most_recent}%`, - avgUptime: `${report.last_day || report.last_hour}%`, - node_performance: toPercentInteger(gateway.node_performance.most_recent), - }; -} diff --git a/explorer/src/components/Gateways/VersionDisplaySelector.tsx b/explorer/src/components/Gateways/VersionDisplaySelector.tsx deleted file mode 100644 index e9be02b419..0000000000 --- a/explorer/src/components/Gateways/VersionDisplaySelector.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { FormControl, MenuItem, Select } from '@mui/material'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export enum VersionSelectOptions { - latestVersion = 'Latest versions', - olderVersions = 'Older versions', - all = 'All', -} -export const VersionDisplaySelector = ({ - selected, - handleChange, -}: { - selected: VersionSelectOptions; - handleChange: (option: VersionSelectOptions) => void; -}) => { - const isMobile = useIsMobile(); - - return ( - <FormControl size="small"> - <Select - value={selected} - onChange={(e) => handleChange(e.target.value as VersionSelectOptions)} - labelId="simple-select-label" - id="simple-select" - sx={{ - marginRight: isMobile ? 0 : 2, - }} - > - <MenuItem value={VersionSelectOptions.latestVersion} data-testid="show-gateway-latest-version"> - {VersionSelectOptions.latestVersion} - </MenuItem> - <MenuItem value={VersionSelectOptions.olderVersions} data-testid="show-gateway-old-versions"> - {VersionSelectOptions.olderVersions} - </MenuItem> - <MenuItem value={VersionSelectOptions.all} data-testid="show-gateway-all-versions"> - {VersionSelectOptions.all} - </MenuItem> - </Select> - </FormControl> - ); -}; diff --git a/explorer/src/components/Icons.ts b/explorer/src/components/Icons.ts deleted file mode 100644 index 88761b9d21..0000000000 --- a/explorer/src/components/Icons.ts +++ /dev/null @@ -1,28 +0,0 @@ -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; -import { MixnodeStatus } from '../typeDefs/explorer-api'; - -export const Icons = { - Mixnodes: { - Status: { - Active: CheckCircleOutlineIcon, - Standby: PauseCircleOutlineIcon, - Inactive: CircleOutlinedIcon, - }, - }, -}; - -export const getMixNodeIcon = (value: any) => { - if (value && typeof value === 'string') { - switch (value) { - case MixnodeStatus.active: - return Icons.Mixnodes.Status.Active; - case MixnodeStatus.standby: - return Icons.Mixnodes.Status.Standby; - default: - return Icons.Mixnodes.Status.Inactive; - } - } - return Icons.Mixnodes.Status.Inactive; -}; diff --git a/explorer/src/components/MixNodes/BondBreakdown.tsx b/explorer/src/components/MixNodes/BondBreakdown.tsx deleted file mode 100644 index 7fd66efb24..0000000000 --- a/explorer/src/components/MixNodes/BondBreakdown.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import * as React from 'react'; -import { Alert, Box, CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import { ExpandMore } from '@mui/icons-material'; -import { currencyToString } from '../../utils/currency'; -import { useMixnodeContext } from '../../context/mixnode'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export const BondBreakdownTable: FCWithChildren = () => { - const { mixNode, delegations, uniqDelegations } = useMixnodeContext(); - const [showDelegations, toggleShowDelegations] = React.useState<boolean>(false); - - const [bonds, setBonds] = React.useState({ - delegations: '0', - pledges: '0', - bondsTotal: '0', - hasLoaded: false, - }); - const theme = useTheme(); - const isMobile = useIsMobile(); - - React.useEffect(() => { - if (mixNode?.data) { - // delegations - const decimalisedDelegations = currencyToString({ - amount: mixNode.data.total_delegation.amount.toString(), - denom: mixNode.data.total_delegation.denom, - }); - - // pledges - const decimalisedPledges = currencyToString({ - amount: mixNode.data.pledge_amount.amount.toString(), - denom: mixNode.data.pledge_amount.denom, - }); - - // bonds total (del + pledges) - const pledgesSum = Number(mixNode.data.pledge_amount.amount); - const delegationsSum = Number(mixNode.data.total_delegation.amount); - const bondsTotal = currencyToString({ - amount: (pledgesSum + delegationsSum).toString(), - }); - - setBonds({ - delegations: decimalisedDelegations, - pledges: decimalisedPledges, - bondsTotal, - hasLoaded: true, - }); - } - }, [mixNode]); - - const expandDelegations = () => { - if (delegations?.data && delegations.data.length > 0) { - toggleShowDelegations(!showDelegations); - } - }; - const calcBondPercentage = (num: number) => { - if (mixNode?.data) { - const rawDelegationAmount = Number(mixNode.data.total_delegation.amount); - const rawPledgeAmount = Number(mixNode.data.pledge_amount.amount); - const rawTotalBondsAmount = rawDelegationAmount + rawPledgeAmount; - return ((num * 100) / rawTotalBondsAmount).toFixed(1); - } - return 0; - }; - - if (mixNode?.isLoading || delegations?.isLoading) { - return <CircularProgress />; - } - - if (mixNode?.error) { - return <Alert severity="error">Mixnode not found</Alert>; - } - if (delegations?.error) { - return <Alert severity="error">Unable to get delegations for mixnode</Alert>; - } - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals"> - <TableBody> - <TableRow sx={isMobile ? { minWidth: '70vw' } : null}> - <TableCell - sx={{ - fontWeight: 400, - width: '150px', - }} - align="left" - > - Stake total - </TableCell> - <TableCell align="left" data-testid="bond-total-amount"> - {bonds.bondsTotal} - </TableCell> - </TableRow> - <TableRow> - <TableCell align="left">Bond</TableCell> - <TableCell align="left" data-testid="pledge-total-amount"> - {bonds.pledges} - </TableCell> - </TableRow> - <TableRow> - <TableCell onClick={expandDelegations} align="left"> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - }} - > - Delegation total {'\u00A0'} - {delegations?.data && delegations?.data?.length > 0 && <ExpandMore />} - </Box> - </TableCell> - <TableCell align="left" data-testid="delegation-total-amount"> - {bonds.delegations} - </TableCell> - </TableRow> - </TableBody> - </Table> - - {showDelegations && ( - <Box - sx={{ - maxHeight: 400, - overflowY: 'scroll', - p: 2, - background: theme.palette.background.paper, - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'baseline', - width: '100%', - p: 2, - borderBottom: `1px solid ${theme.palette.divider}`, - }} - data-testid="delegations-total-amount" - > - <Typography - sx={{ - fontSize: 16, - fontWeight: 600, - }} - > - Delegations   - </Typography> - </Box> - <Table stickyHeader> - <TableHead> - <TableRow> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Delegators - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Amount - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - width: '200px', - }} - align="left" - > - Share of stake - </TableCell> - </TableRow> - </TableHead> - - <TableBody> - {uniqDelegations?.data?.map(({ owner, amount: { amount } }) => ( - <TableRow key={owner}> - <TableCell sx={isMobile ? { width: 190 } : null} align="left"> - {owner} - </TableCell> - <TableCell align="left">{currencyToString({ amount: amount.toString() })}</TableCell> - <TableCell align="left">{calcBondPercentage(amount)}%</TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </Box> - )} - </TableContainer> - ); -}; diff --git a/explorer/src/components/MixNodes/DetailSection.tsx b/explorer/src/components/MixNodes/DetailSection.tsx deleted file mode 100644 index 6becf5efe4..0000000000 --- a/explorer/src/components/MixNodes/DetailSection.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import * as React from 'react'; -import { Box, Button, Grid, Typography, useTheme } from '@mui/material'; -import Identicon from 'react-identicons'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { MixnodeRowType } from '.'; -import { getMixNodeStatusText, MixNodeStatus } from './Status'; -import { MixNodeDescriptionResponse } from '../../typeDefs/explorer-api'; - -interface MixNodeDetailProps { - mixNodeRow: MixnodeRowType; - mixnodeDescription: MixNodeDescriptionResponse; -} - -export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNodeRow, mixnodeDescription }) => { - const theme = useTheme(); - const palette = [theme.palette.text.primary]; - const isMobile = useIsMobile(); - const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]); - - return ( - <Grid container> - <Grid item xs={12} md={6}> - <Box display="flex" flexDirection={isMobile ? 'column' : 'row'} width="100%"> - <Box - width={72} - height={72} - sx={{ - minWidth: 72, - minHeight: 72, - borderWidth: 1, - borderColor: theme.palette.text.primary, - borderStyle: 'solid', - borderRadius: '50%', - display: 'grid', - placeItems: 'center', - }} - > - <Identicon size={43} string={mixNodeRow.identity_key} palette={palette} /> - </Box> - <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: isMobile ? 2 : 4, - borderRadius: '30px', - fontWeight: 600, - padding: 0, - }} - href={mixnodeDescription.link} - target="_blank" - > - <Typography - component="span" - textOverflow="ellipsis" - whiteSpace="nowrap" - overflow="hidden" - maxWidth="250px" - > - {mixnodeDescription.link} - </Typography> - </Button> - </Box> - </Box> - </Grid> - <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={isMobile ? 'start' : 'self-end'}> - Node status: - </Typography> - <Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}> - <MixNodeStatus status={mixNodeRow.status} /> - </Box> - <Typography - mt={1} - alignSelf={isMobile ? 'start' : 'self-end'} - color={theme.palette.text.secondary} - fontSize="smaller" - > - This node is {statusText} in this epoch - </Typography> - </Box> - </Grid> - </Grid> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/Columns.ts b/explorer/src/components/MixNodes/Economics/Columns.ts deleted file mode 100644 index b2115c0749..0000000000 --- a/explorer/src/components/MixNodes/Economics/Columns.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ColumnsType } from '../../DetailTable'; - -export const EconomicsInfoColumns: ColumnsType[] = [ - { - field: 'estimatedTotalReward', - title: 'Estimated Total Reward', - 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', - 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', - 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: 'profitMargin', - title: 'Profit Margin', - 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', - 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: 'nodePerformance', - title: 'Routing Score', - width: '10%', - 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.", - }, - { - field: 'avgUptime', - title: 'Avg. Score', - tooltipInfo: "Mixnode's average routing score in the last 24 hour", - }, -]; diff --git a/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx b/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx deleted file mode 100644 index aef36113df..0000000000 --- a/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { EconomicsProgress } from './EconomicsProgress'; - -export default { - title: 'Mix Node Detail/Economics/ProgressBar', - component: EconomicsProgress, -} as ComponentMeta<typeof EconomicsProgress>; - -const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />; - -export const Empty = Template.bind({}); -Empty.args = {}; - -export const OverThreshold = Template.bind({}); -OverThreshold.args = { - threshold: 100, - value: 120, -}; - -export const UnderThreshold = Template.bind({}); -UnderThreshold.args = { - threshold: 100, - value: 80, -}; - -export const OnThreshold = Template.bind({}); -OnThreshold.args = { - threshold: 100, - value: 100, -}; diff --git a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx b/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx deleted file mode 100644 index 24db62d167..0000000000 --- a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/system'; - -const parseToNumber = (value: number | undefined | string) => - typeof value === 'string' ? parseInt(value || '', 10) : value || 0; - -export const EconomicsProgress: FCWithChildren< - LinearProgressProps & { - threshold?: number; - color: string; - } -> = ({ threshold, color, ...props }) => { - const theme = useTheme(); - const { value } = props; - - const valueNumber: number = parseToNumber(value); - const thresholdNumber: number = parseToNumber(threshold); - const percentageToDisplay = Math.min(valueNumber, thresholdNumber); - - return ( - <Box - sx={{ - width: 6 / 10, - color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee, - }} - > - <LinearProgress - {...props} - variant="determinate" - color={color} - value={percentageToDisplay} - sx={{ width: '100%', borderRadius: '5px' }} - /> - </Box> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx deleted file mode 100644 index c9c82c0438..0000000000 --- a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { DelegatorsInfoTable } from './Table'; -import { EconomicsInfoColumns } from './Columns'; -import { EconomicsInfoRowWithIndex } from './types'; - -export default { - title: 'Mix Node Detail/Economics', - component: DelegatorsInfoTable, -} as ComponentMeta<typeof DelegatorsInfoTable>; - -const row: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: 'High', - }, - - estimatedOperatorReward: { - value: '80000.123456 NYM', - }, - estimatedTotalReward: { - value: '80000.123456 NYM', - }, - profitMargin: { - value: '10 %', - }, - operatingCost: { - value: '11121 NYM', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Good', - }, -}; - -const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Low', - }, -}; - -const emptyRow: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: '-', - progressBarValue: 0, - }, - - estimatedOperatorReward: { - value: '-', - }, - estimatedTotalReward: { - value: '-', - }, - profitMargin: { - value: '-', - }, - operatingCost: { - value: '-', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />; - -export const Empty = Template.bind({}); -Empty.args = { - rows: [emptyRow], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceHigh = Template.bind({}); -selectionChanceHigh.args = { - rows: [row], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceGood = Template.bind({}); -selectionChanceGood.args = { - rows: [rowGoodProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceLow = Template.bind({}); -selectionChanceLow.args = { - rows: [rowLowProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts deleted file mode 100644 index ce947b0a4f..0000000000 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { currencyToString, unymToNym } from '../../../utils/currency'; -import { useMixnodeContext } from '../../../context/mixnode'; -import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api'; -import { EconomicsInfoRowWithIndex } from './types'; -import { toPercentIntegerString } from '../../../utils'; - -const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => - economicDynamicsStats?.data?.active_set_inclusion_probability || '-'; - -export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { - const { economicDynamicsStats, mixNode } = useMixnodeContext(); - - const estimatedNodeRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_total_node_reward.toString() || '', - }) || '-'; - const estimatedOperatorRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_operator_reward.toString() || '', - }) || '-'; - const profitMargin = mixNode?.data?.profit_margin_percent - ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) - : '-'; - 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; - - return { - id: 1, - estimatedTotalReward: { - value: estimatedNodeRewards, - }, - estimatedOperatorReward: { - value: estimatedOperatorRewards, - }, - selectionChance: { - value: selectionChance(economicDynamicsStats), - }, - profitMargin: { - value: profitMargin ? `${profitMargin} %` : '-', - }, - operatingCost: { - value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-', - }, - avgUptime: { - value: avgUptime ? `${avgUptime} %` : '-', - }, - nodePerformance: { - value: nodePerformance, - }, - }; -}; diff --git a/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx b/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx deleted file mode 100644 index 918d024643..0000000000 --- a/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -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> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/Table.tsx b/explorer/src/components/MixNodes/Economics/Table.tsx deleted file mode 100644 index e6630334db..0000000000 --- a/explorer/src/components/MixNodes/Economics/Table.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react'; -import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material'; -import { Box } from '@mui/system'; -import { useTheme } from '@mui/material/styles'; -import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; -import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types'; -import { UniversalTableProps } from '../../DetailTable'; -import { textColour } from '../../../utils'; - -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, - columnsData, - rows, -}) => { - const theme = useTheme(); - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, tooltipInfo, width }) => ( - <TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}> - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <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> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows?.map((eachRow) => ( - <TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> - {columnsData?.map((_, index: number) => { - const { field } = columnsData[index]; - const value: EconomicsRowsType = (eachRow as any)[field]; - return ( - <TableCell - key={_.title} - sx={{ - color: textColour(value, field, theme), - }} - data-testid={`${_.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(value, columnsData[index].field)} - </TableCell> - ); - })} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/index.ts b/explorer/src/components/MixNodes/Economics/index.ts deleted file mode 100644 index 6dec752246..0000000000 --- a/explorer/src/components/MixNodes/Economics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { DelegatorsInfoTable } from './Table'; -export { EconomicsInfoColumns } from './Columns'; -export { EconomicsInfoRows } from './Rows'; diff --git a/explorer/src/components/MixNodes/Economics/types.ts b/explorer/src/components/MixNodes/Economics/types.ts deleted file mode 100644 index 0bc550253f..0000000000 --- a/explorer/src/components/MixNodes/Economics/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type EconomicsRowsType = { - progressBarValue?: number; - value: string; -}; - -type TEconomicsInfoProperties = - | 'estimatedTotalReward' - | 'estimatedOperatorReward' - | 'estimatedOperatorReward' - | 'selectionChance' - | 'profitMargin' - | 'avgUptime' - | 'nodePerformance' - | 'operatingCost'; - -export type EconomicsInfoRow = { - [k in TEconomicsInfoProperties]: EconomicsRowsType; -}; - -export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number }; diff --git a/explorer/src/components/MixNodes/Status.tsx b/explorer/src/components/MixNodes/Status.tsx deleted file mode 100644 index 627b1f5964..0000000000 --- a/explorer/src/components/MixNodes/Status.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; -import { getMixNodeIcon } from '@src/components/Icons'; -import { MixnodeStatus } from '@src/typeDefs/explorer-api'; -import { useGetMixNodeStatusColor } from '@src/hooks/useGetMixnodeStatusColor'; - -interface MixNodeStatusProps { - status: MixnodeStatus; -} -// TODO: should be done with i18n -export const getMixNodeStatusText = (status: MixnodeStatus) => { - switch (status) { - case MixnodeStatus.active: - return 'active'; - case MixnodeStatus.standby: - return 'on standby'; - default: - return 'inactive'; - } -}; - -export const MixNodeStatus: FCWithChildren<MixNodeStatusProps> = ({ status }) => { - const Icon = React.useMemo(() => getMixNodeIcon(status), [status]); - const color = useGetMixNodeStatusColor(status); - - return ( - <Typography color={color} display="flex" alignItems="center"> - <Icon /> - <Typography ml={1} component="span" color="inherit"> - {`${status[0].toUpperCase()}${status.slice(1)}`} - </Typography> - </Typography> - ); -}; diff --git a/explorer/src/components/MixNodes/StatusDropdown.tsx b/explorer/src/components/MixNodes/StatusDropdown.tsx deleted file mode 100644 index 9cb34a2f0c..0000000000 --- a/explorer/src/components/MixNodes/StatusDropdown.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as React from 'react'; -import { MenuItem } from '@mui/material'; -import Select from '@mui/material/Select'; -import { SelectChangeEvent } from '@mui/material/Select/SelectInput'; -import { SxProps } from '@mui/system'; -import { MixNodeStatus } from './Status'; -import { MixnodeStatus, MixnodeStatusWithAll } from '../../typeDefs/explorer-api'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -// TODO: replace with i18n -const ALL_NODES = 'All nodes'; - -interface MixNodeStatusDropdownProps { - status?: MixnodeStatusWithAll; - sx?: SxProps; - onSelectionChanged?: (status?: MixnodeStatusWithAll) => void; -} - -export const MixNodeStatusDropdown: FCWithChildren<MixNodeStatusDropdownProps> = ({ - status, - onSelectionChanged, - sx, -}) => { - const isMobile = useIsMobile(); - const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>(status || MixnodeStatusWithAll.all); - const onChange = React.useCallback( - (event: SelectChangeEvent) => { - setStatusValue(event.target.value as MixnodeStatusWithAll); - if (onSelectionChanged) { - onSelectionChanged(event.target.value as MixnodeStatusWithAll); - } - }, - [onSelectionChanged], - ); - - return ( - <Select - labelId="mixnodeStatusSelect_label" - id="mixnodeStatusSelect" - value={statusValue} - onChange={onChange} - renderValue={(value) => { - switch (value) { - case MixnodeStatusWithAll.active: - case MixnodeStatusWithAll.standby: - case MixnodeStatusWithAll.inactive: - return <MixNodeStatus status={value as unknown as MixnodeStatus} />; - default: - return ALL_NODES; - } - }} - sx={{ - width: isMobile ? '50%' : 200, - ...sx, - }} - > - <MenuItem value={MixnodeStatus.active} data-testid="mixnodeStatusSelectOption_active"> - <MixNodeStatus status={MixnodeStatus.active} /> - </MenuItem> - <MenuItem value={MixnodeStatus.standby} data-testid="mixnodeStatusSelectOption_standby"> - <MixNodeStatus status={MixnodeStatus.standby} /> - </MenuItem> - <MenuItem value={MixnodeStatus.inactive} data-testid="mixnodeStatusSelectOption_inactive"> - <MixNodeStatus status={MixnodeStatus.inactive} /> - </MenuItem> - <MenuItem value={MixnodeStatusWithAll.all} data-testid="mixnodeStatusSelectOption_allNodes"> - {ALL_NODES} - </MenuItem> - </Select> - ); -}; - -MixNodeStatusDropdown.defaultProps = { - onSelectionChanged: undefined, - status: undefined, - sx: undefined, -}; diff --git a/explorer/src/components/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts deleted file mode 100644 index ccb6c5a8b3..0000000000 --- a/explorer/src/components/MixNodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Status'; -export * from './StatusDropdown'; -export * from './mappings'; diff --git a/explorer/src/components/MixNodes/mappings.ts b/explorer/src/components/MixNodes/mappings.ts deleted file mode 100644 index aba4c6fdca..0000000000 --- a/explorer/src/components/MixNodes/mappings.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable camelcase */ -import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; -import { toPercentInteger, toPercentIntegerString } from '../../utils'; -import { unymToNym } from '../../utils/currency'; - -export type MixnodeRowType = { - mix_id: number; - id: string; - status: MixnodeStatus; - owner: string; - location: string; - identity_key: string; - bond: number; - self_percentage: string; - pledge_amount: number; - host: string; - layer: string; - profit_percentage: number; - avg_uptime: string; - stake_saturation: React.ReactNode; - operating_cost: number; - node_performance: number; - blacklisted: boolean; -}; - -export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { - return (arrayOfMixnodes || []).map(mixNodeResponseItemToMixnodeRowType); -} - -export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): MixnodeRowType { - const pledge = Number(item.pledge_amount.amount) || 0; - const delegations = Number(item.total_delegation.amount) || 0; - const totalBond = pledge + delegations; - const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); - const profitPercentage = toPercentInteger(item.profit_margin_percent) || 0; - const uncappedSaturation = typeof item.uncapped_saturation === 'number' ? item.uncapped_saturation * 100 : 0; - - return { - mix_id: item.mix_id, - id: item.owner, - status: item.status, - owner: item.owner, - identity_key: item.mix_node.identity_key || '', - bond: totalBond || 0, - location: item?.location?.country_name || '', - self_percentage: selfPercentage, - pledge_amount: pledge, - host: item?.mix_node?.host || '', - layer: item?.layer || '', - profit_percentage: profitPercentage, - avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`, - stake_saturation: Number(uncappedSaturation.toFixed(2)), - operating_cost: Number(unymToNym(item.operating_cost?.amount, 6)) || 0, - node_performance: toPercentInteger(item.node_performance.most_recent), - blacklisted: item.blacklisted, - }; -} diff --git a/explorer/src/components/MobileNav.tsx b/explorer/src/components/MobileNav.tsx deleted file mode 100644 index ac4b313467..0000000000 --- a/explorer/src/components/MobileNav.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; -import { AppBar, Box, Drawer, IconButton, List, ListItem, ListItemButton, ListItemIcon, Toolbar } from '@mui/material'; -import { Menu } from '@mui/icons-material'; -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { useMainContext } from '../context/main'; -import { MobileDrawerClose } from '../icons/MobileDrawerClose'; -import { Footer } from './Footer'; -import { ExpandableButton } from './Nav'; -import { ConnectKeplrWallet } from './Wallet/ConnectKeplrWallet'; -import NetworkTitle from './NetworkTitle'; - -export const MobileNav: FCWithChildren = ({ children }) => { - const theme = useTheme(); - const { navState, updateNavState } = useMainContext(); - const [drawerOpen, setDrawerOpen] = React.useState(false); - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false); - const isSmallMobile = useIsMobile(400); - - const toggleDrawer = () => { - setDrawerOpen(!drawerOpen); - }; - - const handleClick = (url: string) => { - updateNavState(url); - toggleDrawer(); - }; - - const openDrawer = () => { - setDrawerOpen(true); - }; - - return ( - <Box sx={{ display: 'flex', flexDirection: 'column' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner open={openMaintenance} onClick={() => setOpenMaintenance(false)} /> - <Toolbar - sx={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }} - > - <IconButton onClick={toggleDrawer}> - <Menu sx={{ color: 'primary.contrastText' }} /> - </IconButton> - {!isSmallMobile && <NetworkTitle />} - </Box> - <ConnectKeplrWallet /> - </Toolbar> - </AppBar> - <Drawer - anchor="left" - open={drawerOpen} - onClose={toggleDrawer} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - }, - }} - > - <Box role="presentation"> - <List sx={{ pt: 0, pb: 0 }}> - <ListItem - disablePadding - disableGutters - sx={{ - height: 64, - background: theme.palette.nym.networkExplorer.nav.background, - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - }} - > - <ListItemButton - onClick={toggleDrawer} - sx={{ - pt: 2, - pb: 2, - background: theme.palette.nym.networkExplorer.nav.background, - display: 'flex', - justifyContent: 'flex-start', - }} - > - <ListItemIcon> - <MobileDrawerClose /> - </ListItemIcon> - </ListItemButton> - </ListItem> - {navState.map((props) => ( - <ExpandableButton - key={props.url} - title={props.title} - openDrawer={openDrawer} - url={props.url} - drawIsTempOpen={drawerOpen === true} - drawIsFixed={false} - Icon={props.Icon} - setToActive={handleClick} - nested={props.nested} - isMobile - isActive={props.isActive} - /> - ))} - </List> - </Box> - </Drawer> - - <Box sx={{ width: '100%', p: 4, mt: 7 }}> - {children} - <Footer /> - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/Nav.tsx b/explorer/src/components/Nav.tsx deleted file mode 100644 index a0f5b16fec..0000000000 --- a/explorer/src/components/Nav.tsx +++ /dev/null @@ -1,401 +0,0 @@ -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { ExpandLess, ExpandMore, Menu } from '@mui/icons-material'; -import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles'; -import Button from '@mui/material/Button'; -import MuiLink from '@mui/material/Link'; -import Box from '@mui/material/Box'; -import ListItem from '@mui/material/ListItem'; -import MuiDrawer from '@mui/material/Drawer'; -import AppBar from '@mui/material/AppBar'; -import Toolbar from '@mui/material/Toolbar'; -import List from '@mui/material/List'; -import Typography from '@mui/material/Typography'; -import IconButton from '@mui/material/IconButton'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner'; -import { NYM_WEBSITE } from '../api/constants'; -import { useMainContext } from '../context/main'; -import { MobileDrawerClose } from '../icons/MobileDrawerClose'; -import { Footer } from './Footer'; -import { DarkLightSwitchDesktop } from './Switch'; -import { NavOptionType } from '../context/nav'; -import { ConnectKeplrWallet } from './Wallet/ConnectKeplrWallet'; - -const drawerWidth = 255; -const bannerHeight = 80; - -const openedMixin = (theme: Theme): CSSObject => ({ - width: drawerWidth, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - overflowX: 'hidden', -}); - -const closedMixin = (theme: Theme): CSSObject => ({ - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - overflowX: 'hidden', - width: `calc(${theme.spacing(7)} + 1px)`, -}); - -const DrawerHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - padding: theme.spacing(0, 1), - height: 64, -})); - -const Drawer = styled(MuiDrawer, { - shouldForwardProp: (prop) => prop !== 'open', -})(({ theme, open }) => ({ - width: drawerWidth, - flexShrink: 0, - whiteSpace: 'nowrap', - boxSizing: 'border-box', - ...(open && { - ...openedMixin(theme), - '& .MuiDrawer-paper': openedMixin(theme), - }), - ...(!open && { - ...closedMixin(theme), - '& .MuiDrawer-paper': closedMixin(theme), - }), -})); - -type ExpandableButtonType = { - title: string; - url: string; - isActive?: boolean; - Icon?: React.ReactNode; - nested?: NavOptionType[]; - isChild?: boolean; - openDrawer: () => void; - closeDrawer?: () => void; - drawIsTempOpen: boolean; - drawIsFixed: boolean; - fixDrawerClose?: () => void; - isMobile: boolean; - setToActive: (url: string) => void; -}; - -export const ExpandableButton: FCWithChildren<ExpandableButtonType> = ({ - url, - setToActive, - isActive, - openDrawer, - closeDrawer, - drawIsTempOpen, - drawIsFixed, - fixDrawerClose, - Icon, - title, - nested, - isMobile, - isChild, -}) => { - const [dynamicStyle, setDynamicStyle] = React.useState({}); - const [nestedOptions, toggleNestedOptions] = React.useState(false); - const [isExternal, setIsExternal] = React.useState<boolean>(false); - const { palette } = useTheme(); - - const handleClick = () => { - setToActive(url); - if (title === 'Network Components' && nested) { - openDrawer(); - toggleNestedOptions(!nestedOptions); - } - if (!nested && !drawIsFixed) { - closeDrawer?.(); - } - if (!nested && isMobile) { - fixDrawerClose?.(); - } - }; - - React.useEffect(() => { - if (url) { - setIsExternal(url.includes('http')); - } - if (nested) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - }); - } - if (isChild) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.nested, - fontWeight: 600, - }); - } - if (!nested && !isChild) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - }); - } - }, [url]); - - React.useEffect(() => { - if (!drawIsTempOpen && nestedOptions) { - toggleNestedOptions(false); - } - }, [drawIsTempOpen]); - - const linkProps = isExternal - ? { - component: 'a', - href: url, - target: '_blank', - } - : { component: !nested ? Link : 'div', to: url }; - - return ( - <> - <ListItem - disablePadding - disableGutters - {...linkProps} - sx={{ - borderBottom: isChild ? 'none' : '1px solid rgba(255, 255, 255, 0.1)', - ...(isActive - ? dynamicStyle - : { - background: palette.nym.networkExplorer.nav.background, - borderRight: 'none', - }), - }} - > - <ListItemButton - onClick={handleClick} - sx={{ - pt: 2, - pb: 2, - background: isChild ? palette.nym.networkExplorer.nav.selected.nested : 'none', - }} - > - <ListItemIcon sx={{ minWidth: '39px' }}>{Icon}</ListItemIcon> - <ListItemText - primary={title} - sx={{ - color: palette.nym.networkExplorer.nav.text, - }} - primaryTypographyProps={{ - style: { - fontWeight: isActive ? 600 : 400, - }, - }} - /> - {nested && nestedOptions && <ExpandLess />} - {nested && !nestedOptions && <ExpandMore />} - </ListItemButton> - </ListItem> - {nestedOptions && - nested?.map((each) => ( - <ExpandableButton - url={each.url} - key={each.title} - title={each.title} - openDrawer={openDrawer} - drawIsTempOpen={drawIsTempOpen} - closeDrawer={closeDrawer} - setToActive={setToActive} - drawIsFixed={drawIsFixed} - fixDrawerClose={fixDrawerClose} - isMobile={isMobile} - isChild - /> - ))} - </> - ); -}; - -ExpandableButton.defaultProps = { - Icon: null, - nested: undefined, - isChild: false, - isActive: false, - fixDrawerClose: undefined, - closeDrawer: undefined, -}; - -export const Nav: FCWithChildren = ({ children }) => { - const { updateNavState, navState, environment } = useMainContext(); - const [drawerIsOpen, setDrawerToOpen] = React.useState(false); - const [fixedOpen, setFixedOpen] = React.useState(false); - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false); - const theme = useTheme(); - - const explorerName = - `${environment && environment.charAt(0).toUpperCase() + environment.slice(1)} Explorer` || 'Mainnet Explorer'; - - const switchNetworkText = environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet'; - const switchNetworkLink = - environment === 'mainnet' ? 'https://sandbox-explorer.nymtech.net' : 'https://explorer.nymtech.net'; - - const setToActive = (url: string) => { - updateNavState(url); - }; - - const fixDrawerOpen = () => { - setFixedOpen(true); - setDrawerToOpen(true); - }; - - const fixDrawerClose = () => { - setFixedOpen(false); - setDrawerToOpen(false); - }; - - const tempDrawerOpen = () => { - if (!fixedOpen) { - setDrawerToOpen(true); - } - }; - - const tempDrawerClose = () => { - if (!fixedOpen) { - setDrawerToOpen(false); - } - }; - - return ( - <Box sx={{ display: 'flex' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner open={openMaintenance} onClick={() => setOpenMaintenance(false)} height={bannerHeight} /> - <Toolbar - disableGutters - sx={{ - display: 'flex', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - ml: 0.5, - }} - > - <IconButton component="a" href={NYM_WEBSITE} target="_blank"> - <NymLogo height="40px" width="40px" /> - </IconButton> - <Typography - variant="h6" - noWrap - sx={{ - color: theme.palette.nym.networkExplorer.nav.text, - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink component={Link} to="/" underline="none" color="inherit"> - {explorerName} - </MuiLink> - <Button - size="small" - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ borderRadius: 2, textTransform: 'none', width: 150, ml: 4, fontSize: 14, fontWeight: 600 }} - > - {switchNetworkText} - </Button> - </Typography> - </Box> - <Box - sx={{ - mr: 2, - alignItems: 'center', - display: 'flex', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - pr: 0, - pl: 2, - justifyContent: 'flex-end', - alignItems: 'center', - }} - > - <Box sx={{ mr: 1 }}> - <ConnectKeplrWallet /> - </Box> - <DarkLightSwitchDesktop defaultChecked /> - </Box> - </Box> - </Toolbar> - </AppBar> - <Drawer - variant="permanent" - open={drawerIsOpen} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - borderRadius: 0, - top: openMaintenance ? bannerHeight : 0, - }, - }} - > - <DrawerHeader - sx={{ - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - justifyContent: 'flex-start', - paddingLeft: 0, - }} - > - <IconButton - onClick={drawerIsOpen ? fixDrawerClose : fixDrawerOpen} - sx={{ - padding: 1, - ml: 1, - color: theme.palette.nym.networkExplorer.nav.text, - }} - > - {drawerIsOpen ? <MobileDrawerClose /> : <Menu />} - </IconButton> - </DrawerHeader> - - <List sx={{ pt: 0, pb: 0 }} onMouseEnter={tempDrawerOpen} onMouseLeave={tempDrawerClose}> - {navState.map((props) => ( - <ExpandableButton - key={props.url} - closeDrawer={tempDrawerClose} - drawIsTempOpen={drawerIsOpen} - drawIsFixed={fixedOpen} - fixDrawerClose={fixDrawerClose} - openDrawer={tempDrawerOpen} - setToActive={setToActive} - isMobile={false} - {...props} - /> - ))} - </List> - </Drawer> - <Box sx={{ width: '100%', py: 5, px: 6, mt: 7 }}> - {children} - <Footer /> - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/NetworkTitle.tsx b/explorer/src/components/NetworkTitle.tsx deleted file mode 100644 index 7dd87d7e42..0000000000 --- a/explorer/src/components/NetworkTitle.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Button, Typography } from '@mui/material'; -import { Link } from 'react-router-dom'; -import MuiLink from '@mui/material/Link'; -import { useMainContext } from '@src/context/main'; - -type NetworkTitleProps = { - showToggleNetwork?: boolean; -}; - -const NetworkTitle = ({ showToggleNetwork }: NetworkTitleProps) => { - const { environment } = useMainContext(); - - const explorerName = - `${environment && environment.charAt(0).toUpperCase() + environment.slice(1)} Explorer` || 'Mainnet Explorer'; - - const switchNetworkText = environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet'; - const switchNetworkLink = - environment === 'mainnet' ? 'https://sandbox-explorer.nymtech.net' : 'https://explorer.nymtech.net'; - return ( - <Typography - variant="h6" - noWrap - sx={{ - color: 'nym.networkExplorer.nav.text', - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink component={Link} to="/overview" underline="none" color="inherit" fontWeight={700}> - {explorerName} - </MuiLink> - {showToggleNetwork && ( - <Button - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ textTransform: 'none', width: 114, fontSize: '12px', fontWeight: 600, ml: 1 }} - > - {switchNetworkText} - </Button> - )} - </Typography> - ); -}; - -export default NetworkTitle; diff --git a/explorer/src/components/Socials.tsx b/explorer/src/components/Socials.tsx deleted file mode 100644 index e1bb33970b..0000000000 --- a/explorer/src/components/Socials.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { Box, IconButton } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { TelegramIcon } from '../icons/socials/TelegramIcon'; -import { GitHubIcon } from '../icons/socials/GitHubIcon'; -import { TwitterIcon } from '../icons/socials/TwitterIcon'; -import { DiscordIcon } from '../icons/socials/DiscordIcon'; - -// socials -export const TELEGRAM_LINK = 'https://nymtech.net/go/telegram'; -export const TWITTER_LINK = 'https://nymtech.net/go/x'; -export const GITHUB_LINK = 'https://nymtech.net/go/github'; -export const DISCORD_LINK = 'https://nymtech.net/go/discord'; - -export const Socials: FCWithChildren<{ isFooter?: boolean }> = ({ isFooter }) => { - const theme = useTheme(); - const color = isFooter - ? theme.palette.nym.networkExplorer.footer.socialIcons - : theme.palette.nym.networkExplorer.topNav.socialIcons; - return ( - <Box> - <IconButton component="a" href={TELEGRAM_LINK} target="_blank" data-testid="telegram"> - <TelegramIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord"> - <DiscordIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={TWITTER_LINK} target="_blank" data-testid="twitter"> - <TwitterIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={GITHUB_LINK} target="_blank" data-testid="github"> - <GitHubIcon color={color} size={24} /> - </IconButton> - </Box> - ); -}; - -Socials.defaultProps = { - isFooter: false, -}; diff --git a/explorer/src/components/StatsCard.tsx b/explorer/src/components/StatsCard.tsx deleted file mode 100644 index 36355b1759..0000000000 --- a/explorer/src/components/StatsCard.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { Box, Card, CardContent, IconButton, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import EastIcon from '@mui/icons-material/East'; - -interface StatsCardProps { - icon: React.ReactNode; - title: string; - count?: string | number; - errorMsg?: Error | string; - onClick?: () => void; - color?: string; -} -export const StatsCard: FCWithChildren<StatsCardProps> = ({ - icon, - title, - count, - onClick, - errorMsg, - color: colorProp, -}) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <Card onClick={onClick} sx={{ height: '100%' }}> - <CardContent - sx={{ - padding: 1.5, - paddingLeft: 3, - '&:last-child': { - paddingBottom: 1.5, - }, - cursor: 'pointer', - fontSize: 14, - fontWeight: 600, - }} - > - <Box display="flex" alignItems="center" color={color}> - <Box display="flex"> - {icon} - <Typography ml={3} mr={0.75} fontSize="inherit" fontWeight="inherit" data-testid={`${title}-amount`}> - {count === undefined || count === null ? '' : count} - </Typography> - <Typography mr={1} fontSize="inherit" fontWeight="inherit" data-testid={title}> - {title} - </Typography> - </Box> - <IconButton color="inherit" sx={{ fontSize: '16px' }}> - <EastIcon fontSize="inherit" /> - </IconButton> - </Box> - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {typeof errorMsg === 'string' ? errorMsg : errorMsg.message || 'Oh no! An error occurred'} - </Typography> - )} - </CardContent> - </Card> - ); -}; - -StatsCard.defaultProps = { - onClick: undefined, - errorMsg: undefined, - color: undefined, - count: undefined, -}; diff --git a/explorer/src/components/StyledLink.tsx b/explorer/src/components/StyledLink.tsx deleted file mode 100644 index fd101e5b4a..0000000000 --- a/explorer/src/components/StyledLink.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Link as MuiLink, SxProps } from '@mui/material'; -import { Link as RRDL } from 'react-router-dom'; - -type StyledLinkProps = { - to: string; - children: string; - target?: React.HTMLAttributeAnchorTarget; - dataTestId?: string; - color?: string; - sx?: SxProps; -}; - -const StyledLink = ({ to, children, dataTestId, target, color = 'inherit', sx }: StyledLinkProps) => ( - <MuiLink - sx={{ ...sx }} - color={color} - target={target} - underline="none" - component={RRDL} - to={to} - data-testid={dataTestId} - > - {children} - </MuiLink> -); - -export default StyledLink; diff --git a/explorer/src/components/Switch.tsx b/explorer/src/components/Switch.tsx deleted file mode 100644 index 840530a327..0000000000 --- a/explorer/src/components/Switch.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import Switch from '@mui/material/Switch'; -import { Button } from '@mui/material'; -import { useMainContext } from '../context/main'; -import { LightSwitchSVG } from '../icons/LightSwitchSVG'; - -export const DarkLightSwitch = styled(Switch)(({ theme }) => ({ - width: 55, - height: 34, - padding: 7, - '& .MuiSwitch-switchBase': { - margin: 1, - padding: 2, - transform: 'translateX(4px)', - '&.Mui-checked': { - color: '#fff', - transform: 'translateX(22px)', - '& .MuiSwitch-thumb:before': { - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')', - }, - '& + .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - }, - }, - }, - '& .MuiSwitch-thumb': { - backgroundColor: theme.palette.nym.networkExplorer.nav.text, - width: 25, - height: 25, - marginTop: '2px', - '&:before': { - content: "''", - position: 'absolute', - width: '100%', - height: '100%', - left: 0, - top: 0, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')', - }, - }, - '& .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - borderRadius: 20 / 2, - }, -})); - -export const DarkLightSwitchMobile: FCWithChildren = () => { - const { toggleMode } = useMainContext(); - return ( - <Button onClick={() => toggleMode()} data-testid="switch-button" sx={{ p: 0, minWidth: 0 }}> - <LightSwitchSVG /> - </Button> - ); -}; - -export const DarkLightSwitchDesktop: FCWithChildren<{ defaultChecked: boolean }> = ({ defaultChecked }) => { - const { toggleMode } = useMainContext(); - return ( - <Button sx={{ paddingLeft: 0 }} onClick={() => toggleMode()} data-testid="switch-button"> - <DarkLightSwitch defaultChecked={defaultChecked} /> - </Button> - ); -}; diff --git a/explorer/src/components/TableToolbar.tsx b/explorer/src/components/TableToolbar.tsx deleted file mode 100644 index d3af522168..0000000000 --- a/explorer/src/components/TableToolbar.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { Box, TextField, MenuItem, FormControl, IconButton, Select, SelectChangeEvent } from '@mui/material'; -import { Close } from '@mui/icons-material'; -import { Filters } from './Filters/Filters'; -import { useIsMobile } from '../hooks/useIsMobile'; - -const fieldsHeight = '42.25px'; - -type TableToolBarProps = { - onChangeSearch?: (arg: string) => void; - onChangePageSize: (event: SelectChangeEvent<string>) => void; - pageSize: string; - searchTerm?: string; - withFilters?: boolean; - childrenBefore?: React.ReactNode; - childrenAfter?: React.ReactNode; -}; - -export const TableToolbar: FCWithChildren<TableToolBarProps> = ({ - searchTerm, - onChangeSearch, - onChangePageSize, - pageSize, - childrenBefore, - childrenAfter, - withFilters, -}) => { - const isMobile = useIsMobile(); - return ( - <Box - sx={{ - width: '100%', - marginBottom: 2, - display: 'flex', - flexDirection: isMobile ? 'column' : 'row', - justifyContent: 'space-between', - }} - > - <Box sx={{ display: 'flex', flexDirection: isMobile ? 'column-reverse' : 'row', alignItems: 'middle' }}> - <Box sx={{ display: 'flex', justifyContent: 'space-between', height: fieldsHeight }}> - {childrenBefore} - <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> - {!!onChangeSearch && ( - <TextField - sx={{ - width: isMobile ? '100%' : 200, - marginBottom: isMobile ? 2 : 0, - }} - size="small" - value={searchTerm} - data-testid="search-box" - placeholder="Search" - InputProps={{ - endAdornment: searchTerm?.length ? ( - <IconButton size="small" onClick={() => onChangeSearch('')}> - <Close fontSize="small" /> - </IconButton> - ) : undefined, - }} - onChange={(event) => onChangeSearch(event.target.value)} - /> - )} - </Box> - - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'end', - gap: 1, - marginTop: isMobile ? 2 : 0, - }} - > - {withFilters && <Filters />} - {childrenAfter} - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/Title.tsx b/explorer/src/components/Title.tsx deleted file mode 100644 index 032837d3c8..0000000000 --- a/explorer/src/components/Title.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { Typography } from '@mui/material'; - -export const Title: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - variant="h5" - sx={{ - fontWeight: 600, - }} - data-testid={text} - > - {text} - </Typography> -); diff --git a/explorer/src/components/Tooltip.tsx b/explorer/src/components/Tooltip.tsx deleted file mode 100644 index 0febd9f891..0000000000 --- a/explorer/src/components/Tooltip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material'; - -type ValueType<T> = T[keyof T]; - -type Props = { - text: string; - id: string; - placement?: ValueType<Pick<TooltipProps, 'placement'>>; - tooltipSx?: TooltipComponentsPropsOverrides; - children: React.ReactNode; -}; - -export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => ( - <MUITooltip - title={text} - id={id} - placement={placement || 'top-start'} - componentsProps={{ - tooltip: { - sx: { - maxWidth: 200, - background: (t) => t.palette.nym.networkExplorer.tooltip.background, - color: (t) => t.palette.nym.networkExplorer.tooltip.color, - '& .MuiTooltip-arrow': { - color: (t) => t.palette.nym.networkExplorer.tooltip.background, - }, - }, - ...tooltipSx, - }, - }} - arrow - > - {children as ReactElement<any, any>} - </MUITooltip> -); diff --git a/explorer/src/components/TwoColSmallTable.tsx b/explorer/src/components/TwoColSmallTable.tsx deleted file mode 100644 index a211c29bdd..0000000000 --- a/explorer/src/components/TwoColSmallTable.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp'; -import ErrorIcon from '@mui/icons-material/Error'; - -interface TableProps { - title?: string; - icons?: boolean[]; - keys: string[]; - values: number[]; - marginBottom?: boolean; - error?: string; - loading: boolean; -} - -export const TwoColSmallTable: FCWithChildren<TableProps> = ({ - loading, - title, - icons, - keys, - values, - marginBottom, - error, -}) => ( - <> - {title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>} - - <TableContainer component={Paper} sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}> - <Table aria-label="two col small table"> - <TableBody> - {keys.map((each: string, i: number) => ( - <TableRow key={each}> - {icons && <TableCell>{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}</TableCell>} - <TableCell sx={error ? { opacity: 0.4 } : null} data-testid={each.replace(/ /g, '')}> - {each} - </TableCell> - <TableCell - sx={error ? { opacity: 0.4 } : null} - align="right" - data-testid={`${each.replace(/ /g, '-')}-value`} - > - {values[i]} - </TableCell> - {error && ( - <TableCell align="right" sx={{ opacity: 0.4 }}> - {values[i]} - </TableCell> - )} - {!error && loading && ( - <TableCell align="right"> - <CircularProgress /> - </TableCell> - )} - {error && !icons && ( - <TableCell sx={{ opacity: 0.2 }} align="right"> - <ErrorIcon /> - </TableCell> - )} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </> -); - -TwoColSmallTable.defaultProps = { - title: undefined, - icons: undefined, - marginBottom: false, - error: undefined, -}; diff --git a/explorer/src/components/Universal-DataGrid.tsx b/explorer/src/components/Universal-DataGrid.tsx deleted file mode 100644 index 6598dfb485..0000000000 --- a/explorer/src/components/Universal-DataGrid.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import * as React from 'react'; -import { makeStyles } from '@mui/styles'; -import { DataGrid, GridColDef, GridEventListener, useGridApiContext, useGridState } from '@mui/x-data-grid'; -import Pagination from '@mui/material/Pagination'; -import { LinearProgress } from '@mui/material'; -import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity'; - -const useStyles = makeStyles({ - root: { - display: 'flex', - }, -}); - -const CustomPagination = () => { - const apiRef = useGridApiContext(); - const [state] = useGridState(apiRef); - - const classes = useStyles(); - - return ( - <Pagination - className={classes.root} - sx={{ mt: 2 }} - color="primary" - count={state.pagination.pageCount} - page={state.pagination.page + 1} - onChange={(_, value) => apiRef.current.setPage(value - 1)} - /> - ); -}; - -type DataGridProps = { - columns: GridColDef[]; - pagination?: true | undefined; - pageSize?: string | undefined; - rows: any; - loading?: boolean; - initialState?: GridInitialStateCommunity; - onRowClick?: GridEventListener<'rowClick'> | undefined; -}; -export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({ - rows, - columns, - loading, - pagination, - pageSize, - initialState, - onRowClick, -}) => { - if (loading) return <LinearProgress />; - - return ( - <DataGrid - onRowClick={onRowClick} - pagination={pagination} - rows={rows} - components={{ - Pagination: CustomPagination, - }} - columns={columns} - pageSize={Number(pageSize)} - disableSelectionOnClick - autoHeight - hideFooter={!pagination} - initialState={initialState} - style={{ - width: '100%', - border: 'none', - }} - sx={{ - '*::-webkit-scrollbar': { - width: '1em', - }, - '*::-webkit-scrollbar-track': { - background: (t) => t.palette.nym.networkExplorer.scroll.backgroud, - outline: (t) => `1px solid ${t.palette.nym.networkExplorer.scroll.border}`, - boxShadow: 'auto', - borderRadius: 'auto', - }, - '*::-webkit-scrollbar-thumb': { - backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color, - borderRadius: '20px', - width: '.4em', - border: (t) => `3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`, - shadow: 'auto', - }, - }} - /> - ); -}; - -UniversalDataGrid.defaultProps = { - loading: false, - pagination: undefined, - pageSize: '10', -}; diff --git a/explorer/src/components/UptimeChart.tsx b/explorer/src/components/UptimeChart.tsx deleted file mode 100644 index 00a981a3af..0000000000 --- a/explorer/src/components/UptimeChart.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Chart } from 'react-google-charts'; -import { format } from 'date-fns'; -import { ApiState, UptimeStoryResponse } from '../typeDefs/explorer-api'; - -interface ChartProps { - title?: string; - xLabel: string; - yLabel?: string; - uptimeStory: ApiState<UptimeStoryResponse>; - loading: boolean; -} - -type FormattedDateRecord = [string, number]; -type FormattedChartHeadings = string[]; -type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord]; - -export const UptimeChart: FCWithChildren<ChartProps> = ({ title, xLabel, yLabel, uptimeStory, loading }) => { - const [formattedChartData, setFormattedChartData] = React.useState<FormattedChartData>(); - const theme = useTheme(); - const color = theme.palette.text.primary; - React.useEffect(() => { - if (uptimeStory.data?.history) { - const allFormattedChartData: FormattedChartData = [['Date', 'Score']]; - uptimeStory.data.history.forEach((eachDate) => { - const formattedDateUptimeRecord: FormattedDateRecord = [ - format(new Date(eachDate.date), 'MMM dd'), - eachDate.uptime, - ]; - allFormattedChartData.push(formattedDateUptimeRecord); - }); - setFormattedChartData(allFormattedChartData); - } else { - const emptyData: any = [ - ['Date', 'Score'], - ['Jul 27', 10], - ]; - setFormattedChartData(emptyData); - } - }, [uptimeStory]); - - return ( - <> - {title && <Typography>{title}</Typography>} - {loading && <CircularProgress />} - - {!loading && uptimeStory && ( - <Chart - style={{ minHeight: 480 }} - chartType="LineChart" - loader={<p>...</p>} - data={ - uptimeStory.data - ? formattedChartData - : [ - ['Date', 'Routing Score'], - [format(new Date(Date.now()), 'MMM dd'), 0], - ] - } - options={{ - backgroundColor: - theme.palette.mode === 'dark' ? theme.palette.nym.networkExplorer.background.tertiary : undefined, - color: uptimeStory.error ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 1)', - colors: ['#FB7A21'], - legend: { - textStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - - intervals: { style: 'sticks' }, - hAxis: { - // horizontal / date - title: xLabel, - titleTextStyle: { - color, - }, - textStyle: { - color, - // fontSize: 11 - }, - gridlines: { - count: -1, - }, - }, - vAxis: { - // vertical / % Routing Score - viewWindow: { - min: 0, - max: 100, - }, - title: yLabel, - titleTextStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - textStyle: { - color, - fontSize: 11, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - }} - /> - )} - </> - ); -}; - -UptimeChart.defaultProps = { - title: undefined, -}; diff --git a/explorer/src/components/Wallet/ConnectKeplrWallet.tsx b/explorer/src/components/Wallet/ConnectKeplrWallet.tsx deleted file mode 100644 index ff5d2691c6..0000000000 --- a/explorer/src/components/Wallet/ConnectKeplrWallet.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { Button, IconButton, Stack, CircularProgress } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { useWalletContext } from '@src/context/wallet'; -import { WalletAddress, WalletBalance } from '@src/components/Wallet'; - -export const ConnectKeplrWallet = () => { - const { connectWallet, disconnectWallet, isWalletConnected, isWalletConnecting } = useWalletContext(); - const isMobile = useIsMobile(1200); - - if (!connectWallet || !disconnectWallet) { - return null; - } - - if (isWalletConnected) { - return ( - <Stack direction="row" spacing={1}> - <WalletBalance /> - <WalletAddress /> - <IconButton - size="small" - onClick={async () => { - await disconnectWallet(); - }} - > - <CloseIcon fontSize="small" sx={{ color: 'white' }} /> - </IconButton> - </Stack> - ); - } - - return ( - <Button - variant="outlined" - onClick={() => connectWallet()} - disabled={isWalletConnecting} - endIcon={isWalletConnecting && <CircularProgress size={14} color="inherit" />} - > - Connect {isMobile ? '' : ' Wallet'} - </Button> - ); -}; diff --git a/explorer/src/components/Wallet/WalletAddress.tsx b/explorer/src/components/Wallet/WalletAddress.tsx deleted file mode 100644 index 8be6c9f291..0000000000 --- a/explorer/src/components/Wallet/WalletAddress.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { ElipsSVG } from '@src/icons/ElipsSVG'; -import { trimAddress } from '@src/utils'; -import { useWalletContext } from '@src/context/wallet'; - -export const WalletAddress = () => { - const { address } = useWalletContext(); - - const displayAddress = trimAddress(address, 7); - - return ( - <Box display="flex" alignItems="center" gap={0.5}> - <ElipsSVG /> - <Typography variant="body1" fontWeight={600}> - {displayAddress} - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Wallet/WalletBalance.tsx b/explorer/src/components/Wallet/WalletBalance.tsx deleted file mode 100644 index f94935ac23..0000000000 --- a/explorer/src/components/Wallet/WalletBalance.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { useWalletContext } from '@src/context/wallet'; -import { useIsMobile } from '@src/hooks'; -import { TokenSVG } from '@src/icons/TokenSVG'; - -export const WalletBalance = () => { - const { balance } = useWalletContext(); - const isMobile = useIsMobile(1200); - - const showBalance = !isMobile && balance.status === 'success'; - - if (!showBalance) { - return null; - } - - return ( - <Box display="flex" alignItems="center" gap={1}> - <TokenSVG /> - <Typography variant="body1" fontWeight={600}> - {balance.data} NYM - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Wallet/index.ts b/explorer/src/components/Wallet/index.ts deleted file mode 100644 index 645c8b4ffb..0000000000 --- a/explorer/src/components/Wallet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './WalletBalance'; -export * from './WalletAddress'; diff --git a/explorer/src/components/WorldMap.tsx b/explorer/src/components/WorldMap.tsx deleted file mode 100644 index 7a86f34095..0000000000 --- a/explorer/src/components/WorldMap.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import * as React from 'react'; -import { scaleLinear } from 'd3-scale'; -import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps'; -import ReactTooltip from 'react-tooltip'; -import { CircularProgress } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { ApiState, CountryDataResponse } from '../typeDefs/explorer-api'; -import MAP_TOPOJSON from '../assets/world-110m.json'; - -type MapProps = { - userLocation?: [number, number]; - countryData?: ApiState<CountryDataResponse>; - loading: boolean; -}; - -export const WorldMap: FCWithChildren<MapProps> = ({ countryData, userLocation, loading }) => { - const { palette } = useTheme(); - - const colorScale = React.useMemo(() => { - if (countryData?.data) { - const heighestNumberOfNodes = Math.max(...Object.values(countryData.data).map((country) => country.nodes)); - return scaleLinear<string, string>() - .domain([0, 1, heighestNumberOfNodes / 4, heighestNumberOfNodes / 2, heighestNumberOfNodes]) - .range(palette.nym.networkExplorer.map.fills) - .unknown(palette.nym.networkExplorer.map.fills[0]); - } - return () => palette.nym.networkExplorer.map.fills[0]; - }, [countryData, palette]); - - const [tooltipContent, setTooltipContent] = React.useState<string | null>(null); - - if (loading) { - return <CircularProgress />; - } - - return ( - <> - <ComposableMap - data-tip="" - style={{ - backgroundColor: palette.nym.networkExplorer.background.tertiary, - width: '100%', - height: 'auto', - }} - viewBox="0, 50, 800, 350" - projection="geoMercator" - projectionConfig={{ - scale: userLocation ? 200 : 100, - center: userLocation, - }} - > - <ZoomableGroup> - <Geographies geography={MAP_TOPOJSON}> - {({ geographies }) => - geographies.map((geo) => { - const d = (countryData?.data || {})[geo.properties.ISO_A3]; - return ( - <Geography - key={geo.rsmKey} - geography={geo} - fill={colorScale(d?.nodes || 0)} - stroke={palette.nym.networkExplorer.map.stroke} - strokeWidth={0.2} - onMouseEnter={() => { - const { NAME_LONG } = geo.properties; - if (!userLocation) { - setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`); - } - }} - onMouseLeave={() => { - setTooltipContent(''); - }} - style={{ - hover: - !userLocation && countryData - ? { - fill: palette.nym.highlight, - outline: 'white', - } - : undefined, - }} - /> - ); - }) - } - </Geographies> - - {userLocation && ( - <Marker coordinates={userLocation}> - <g - fill="grey" - stroke="#FF5533" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - transform="translate(-12, -10)" - > - <circle cx="12" cy="10" r="5" /> - </g> - </Marker> - )} - </ZoomableGroup> - </ComposableMap> - <ReactTooltip>{tooltipContent}</ReactTooltip> - </> - ); -}; - -WorldMap.defaultProps = { - userLocation: undefined, - countryData: undefined, -}; diff --git a/explorer/src/components/delegatorsInfo/types.ts b/explorer/src/components/delegatorsInfo/types.ts deleted file mode 100644 index b0e3fd6e99..0000000000 --- a/explorer/src/components/delegatorsInfo/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type RowsType = { - value?: string | number; - visualProgressValue?: number; -}; - -export interface DelegatorsInfoRow { - estimated_total_reward: RowsType; - estimated_operator_reward: RowsType; - active_set_probability: RowsType; - stake_saturation: RowsType; - profit_margin: RowsType; - avg_uptime: RowsType; -} - -export type DelegatorsInfoRowWithIndex = DelegatorsInfoRow & { id: number }; diff --git a/explorer/src/components/index.ts b/explorer/src/components/index.ts deleted file mode 100644 index 92eeebd010..0000000000 --- a/explorer/src/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './CustomColumnHeading'; -export * from './Title'; -export * from './Universal-DataGrid'; -export * from './Tooltip'; -export { default as StyledLink } from './StyledLink'; -export * from './Delegations'; -export * from './MixNodes'; -export * from './TableToolbar'; -export * from './Icons'; diff --git a/explorer/src/context/cosmos-kit.tsx b/explorer/src/context/cosmos-kit.tsx deleted file mode 100644 index ce82b21a17..0000000000 --- a/explorer/src/context/cosmos-kit.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { ChainProvider } from '@cosmos-kit/react'; -import { wallets as keplr } from '@cosmos-kit/keplr-extension'; -import { assets, chains } from 'chain-registry'; -import { Chain, AssetList } from '@chain-registry/types'; -import { VALIDATOR_BASE_URL } from '@src/api/constants'; - -const nymSandbox: Chain = { - chain_name: 'sandbox', - chain_id: 'sandbox', - bech32_prefix: 'n', - network_type: 'devnet', - pretty_name: 'Nym Sandbox', - status: 'active', - slip44: 118, - apis: { - rpc: [ - { - address: 'https://rpc.sandbox.nymtech.net', - }, - ], - }, -}; - -const nymSandboxAssets = { - chain_name: 'sandbox', - assets: [ - { - name: 'Nym', - base: 'unym', - symbol: 'NYM', - display: 'NYM', - denom_units: [], - }, - ], -}; - -const CosmosKitProvider = ({ children }: { children: React.ReactNode }) => { - // Only use the nyx chains - const chainsFixedUp = React.useMemo(() => { - const nyx = chains.find((chain) => chain.chain_id === 'nyx'); - - return nyx ? [nymSandbox, nyx] : [nymSandbox]; - }, [chains]); - - // Only use the nyx assets - const assetsFixedUp = React.useMemo(() => { - const nyx = assets.find((asset) => asset.chain_name === 'nyx'); - - return nyx ? [nyx] : [nymSandboxAssets]; - }, [assets]) as AssetList[]; - - return ( - <ChainProvider - chains={chainsFixedUp} - assetLists={assetsFixedUp} - wallets={[...keplr]} - endpointOptions={{ - endpoints: { - nyx: { - rpc: [VALIDATOR_BASE_URL], - }, - }, - }} - > - {children} - </ChainProvider> - ); -}; - -export default CosmosKitProvider; diff --git a/explorer/src/context/delegations.tsx b/explorer/src/context/delegations.tsx deleted file mode 100644 index 28a50345e0..0000000000 --- a/explorer/src/context/delegations.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { Delegation, PendingEpochEvent, PendingEpochEventKind } from '@nymproject/contract-clients/Mixnet.types'; -import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'; -import { useWalletContext } from './wallet'; -import { useMainContext } from './main'; - -const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] }; - -export type PendingEvent = ReturnType<typeof getEventsByAddress>; - -export type DelegationWithRewards = Delegation & { - rewards: string; - identityKey: string; - pending: PendingEvent; -}; - -const getEventsByAddress = (kind: PendingEpochEventKind, address: String) => { - if ('delegate' in kind && kind.delegate.owner === address) { - return { - kind: 'delegate' as const, - mixId: kind.delegate.mix_id, - amount: kind.delegate.amount, - }; - } - - if ('undelegate' in kind && kind.undelegate.owner === address) { - return { - kind: 'undelegate' as const, - mixId: kind.undelegate.mix_id, - }; - } - - return undefined; -}; - -interface DelegationsState { - delegations?: DelegationWithRewards[]; - handleGetDelegations: () => Promise<void>; - handleDelegate: (mixId: number, amount: string) => Promise<ExecuteResult | undefined>; - handleUndelegate: (mixId: number) => Promise<ExecuteResult | undefined>; -} - -export const DelegationsContext = createContext<DelegationsState>({ - delegations: undefined, - handleGetDelegations: async () => { - throw new Error('Please connect your wallet'); - }, - handleDelegate: async () => { - throw new Error('Please connect your wallet'); - }, - handleUndelegate: async () => { - throw new Error('Please connect your wallet'); - }, -}); - -export const DelegationsProvider = ({ children }: { children: React.ReactNode }) => { - const [delegations, setDelegations] = useState<DelegationWithRewards[]>(); - const { address, nymQueryClient, nymClient } = useWalletContext(); - const { fetchMixnodes } = useMainContext(); - - const handleGetPendingEvents = async () => { - if (!nymQueryClient) { - return undefined; - } - - if (!address) { - return undefined; - } - - const response = await nymQueryClient.getPendingEpochEvents({}); - const pendingEvents: PendingEvent[] = []; - - response.events.forEach((e: PendingEpochEvent) => { - const event = getEventsByAddress(e.event.kind, address); - if (event) { - pendingEvents.push(event); - } - }); - - return pendingEvents; - }; - - const handleGetDelegationRewards = async (mixId: number) => { - if (!nymQueryClient) { - return undefined; - } - - if (!address) { - return undefined; - } - - const response = await nymQueryClient.getPendingDelegatorReward({ address, mixId }); - - return response; - }; - - const handleGetDelegations = useCallback(async () => { - if (!nymQueryClient) { - setDelegations(undefined); - return undefined; - } - - if (!address) { - setDelegations(undefined); - return undefined; - } - - // Get all mixnodes - Required to get the identity key for each delegation - const mixnodes = await fetchMixnodes(); - - // Get delegations - const delegationsResponse = await nymQueryClient.getDelegatorDelegations({ delegator: address }); - - // Get rewards for each delegation - const rewardsResponse = await Promise.all( - delegationsResponse.delegations.map((d: Delegation) => handleGetDelegationRewards(d.mix_id)), - ); - - // Get all pending events - const pendingEvents = await handleGetPendingEvents(); - - const delegationsWithRewards: DelegationWithRewards[] = []; - - // Merge delegations with rewards and pending events - delegationsResponse.delegations.forEach((d: Delegation, index: number) => { - delegationsWithRewards.push({ - ...d, - pending: pendingEvents?.find((e: PendingEvent) => (e?.mixId === d.mix_id ? e.kind : undefined)), - identityKey: mixnodes?.find((m) => m.mix_id === d.mix_id)?.mix_node.identity_key || '', - rewards: rewardsResponse[index]?.amount_earned_detailed || '0', - }); - }); - - // Add pending events that are not in the delegations list - pendingEvents?.forEach((e) => { - if (e && !delegationsWithRewards.find((d: DelegationWithRewards) => d.mix_id === e.mixId)) { - delegationsWithRewards.push({ - mix_id: e.mixId, - height: 0, - cumulative_reward_ratio: '0', - owner: address, - amount: { - amount: '0', - denom: 'unym', - }, - rewards: '0', - identityKey: mixnodes?.find((m) => m.mix_id === e.mixId)?.mix_node.identity_key || '', - pending: e, - }); - } - }); - - setDelegations(delegationsWithRewards); - - return undefined; - }, [address, nymQueryClient]); - - const handleDelegate = async (mixId: number, amount: string) => { - if (!address) { - throw new Error('Please connect your wallet'); - } - - const amountToDelegate = (Number(amount) * 1000000).toString(); - const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }]; - try { - const tx = await nymClient?.delegateToMixnode({ mixId }, fee, 'Delegation from Nym Explorer', uNymFunds); - - return tx as unknown as ExecuteResult; - } catch (e) { - console.error('Failed to delegate to mixnode', e); - throw e; - } - }; - - const handleUndelegate = async (mixId: number) => { - const tx = await nymClient?.undelegateFromMixnode({ mixId }, fee, 'Undelegation from Nym Explorer'); - - return tx as unknown as ExecuteResult; - }; - - const contextValue: DelegationsState = useMemo( - () => ({ - delegations, - handleGetDelegations, - handleDelegate, - handleUndelegate, - }), - [delegations, handleGetDelegations], - ); - - return <DelegationsContext.Provider value={contextValue}>{children}</DelegationsContext.Provider>; -}; - -export const useDelegationsContext = () => { - const context = useContext(DelegationsContext); - if (!context) { - throw new Error('useDelegationsContext must be used within a DelegationsProvider'); - } - return context; -}; diff --git a/explorer/src/context/gateway.tsx b/explorer/src/context/gateway.tsx deleted file mode 100644 index dd96634518..0000000000 --- a/explorer/src/context/gateway.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api'; -import { Api } from '../api'; -import { useApiState } from './hooks'; - -/** - * This context provides the state for a single gateway by identity key. - */ - -interface GatewayState { - uptimeReport?: ApiState<GatewayReportResponse>; - uptimeStory?: ApiState<UptimeStoryResponse>; -} - -export const GatewayContext = React.createContext<GatewayState>({}); - -export const useGatewayContext = (): React.ContextType<typeof GatewayContext> => - React.useContext<GatewayState>(GatewayContext); - -/** - * Provides a state context for a gateway by identity - * @param gatewayIdentityKey The identity key of the gateway - */ -export const GatewayContextProvider = ({ - gatewayIdentityKey, - children, -}: { - gatewayIdentityKey: string; - children: JSX.Element; -}) => { - const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState<GatewayReportResponse>( - gatewayIdentityKey, - Api.fetchGatewayReportById, - 'Failed to fetch gateway uptime report by id', - ); - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>( - gatewayIdentityKey, - Api.fetchGatewayUptimeStoryById, - 'Failed to fetch gateway uptime history', - ); - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearUptimeReportById(); - clearUptimeHistory(); - Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]); - }, [gatewayIdentityKey]); - - const state = React.useMemo<GatewayState>( - () => ({ - uptimeReport, - uptimeStory, - }), - [uptimeReport, uptimeStory], - ); - - return <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider>; -}; diff --git a/explorer/src/context/hooks.ts b/explorer/src/context/hooks.ts deleted file mode 100644 index 9c5579c899..0000000000 --- a/explorer/src/context/hooks.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { ApiState } from '../typeDefs/explorer-api'; - -/** - * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously - * @param id The id to fetch - * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) - * @param errorMessage A static error message, to use when no dynamic error message is returned - */ -export const useApiState = <T>( - id: string, - fn: (argId: string) => Promise<T>, - errorMessage: string, -): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => { - // stores the state - const [value, setValue] = React.useState<ApiState<T>>(); - - // clear the value - const clearValueFn = () => setValue(undefined); - - // this provides a method to trigger the delegate to fetch data - const wrappedFetchFn = React.useCallback(async () => { - setValue({ isLoading: true }); - try { - // keep previous state and set to loading - setValue((prevState) => ({ ...prevState, isLoading: true })); - - // delegate to user function to get data and set if successful - const data = await fn(id); - const newValue: ApiState<T> = { - isLoading: false, - data, - }; - setValue(newValue); - return newValue; - } catch (error) { - // return the caught error or create a new error with the static error message - const newValue: ApiState<T> = { - error: error instanceof Error ? error : new Error(errorMessage), - isLoading: false, - }; - setValue(newValue); - return newValue; - } - }, [setValue, fn, id, errorMessage]); - return [value, wrappedFetchFn, clearValueFn]; -}; diff --git a/explorer/src/context/main.tsx b/explorer/src/context/main.tsx deleted file mode 100644 index c7a74a3d0d..0000000000 --- a/explorer/src/context/main.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import * as React from 'react'; -import { PaletteMode } from '@mui/material'; -import { - ApiState, - BlockResponse, - CountryDataResponse, - GatewayResponse, - MixNodeResponse, - MixnodeStatus, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - DirectoryServiceProvider, -} from '../typeDefs/explorer-api'; -import { EnumFilterKey } from '../typeDefs/filters'; -import { Api, getEnvironment } from '../api'; -import { NavOptionType, originalNavOptions } from './nav'; -import { toPercentIntegerString } from '../utils'; - -interface StateData { - summaryOverview?: ApiState<SummaryOverviewResponse>; - block?: ApiState<BlockResponse>; - countryData?: ApiState<CountryDataResponse>; - gateways?: ApiState<GatewayResponse>; - globalError?: string | undefined; - mixnodes?: ApiState<MixNodeResponse>; - mode: PaletteMode; - navState: NavOptionType[]; - validators?: ApiState<ValidatorsResponse>; - environment?: Environment; - serviceProviders?: ApiState<DirectoryServiceProvider[]>; -} - -interface StateApi { - fetchMixnodes: (status?: MixnodeStatus) => Promise<MixNodeResponse | undefined>; - filterMixnodes: (filters: any, status: any) => void; - toggleMode: () => void; - updateNavState: (title: string) => void; -} - -type State = StateData & StateApi; - -export const MainContext = React.createContext<State>({ - mode: 'dark', - updateNavState: () => null, - navState: originalNavOptions, - toggleMode: () => undefined, - filterMixnodes: () => null, - fetchMixnodes: () => Promise.resolve(undefined), -}); - -export const useMainContext = (): React.ContextType<typeof MainContext> => React.useContext<State>(MainContext); - -export const MainContextProvider: FCWithChildren = ({ children }) => { - // network explorer environment - const [environment, setEnvironment] = React.useState<Environment>(); - - // light/dark mode - const [mode, setMode] = React.useState<PaletteMode>('dark'); - - // nav state - const [navState, updateNav] = React.useState<NavOptionType[]>(originalNavOptions); - - // global / banner error messaging - const [globalError] = React.useState<string>(); - - // various APIs for Overview page - const [summaryOverview, setSummaryOverview] = React.useState<ApiState<SummaryOverviewResponse>>(); - const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>(); - const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>(); - 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<DirectoryServiceProvider[]>>(); - - const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark')); - - const fetchOverviewSummary = async () => { - try { - const data = await Api.fetchOverviewSummary(); - setSummaryOverview({ data, isLoading: false }); - } catch (error) { - setSummaryOverview({ - error: error instanceof Error ? error : new Error('Overview summary api fail'), - isLoading: false, - }); - } - }; - - const fetchMixnodes = async (status?: MixnodeStatus) => { - let data; - setMixnodes((d) => ({ ...d, isLoading: true })); - try { - data = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); - setMixnodes({ data, isLoading: false }); - } catch (error) { - setMixnodes({ - error: error instanceof Error ? error : new Error('Mixnode api fail'), - isLoading: false, - }); - } - return data; - }; - - const filterMixnodes = async (filters: { [key in EnumFilterKey]: number[] }, status?: MixnodeStatus) => { - setMixnodes((d) => ({ ...d, isLoading: true })); - const mxns = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); - - const filtered = mxns?.filter( - (m) => - +m.profit_margin_percent >= filters.profitMargin[0] / 100 && - +m.profit_margin_percent <= filters.profitMargin[1] / 100 && - m.stake_saturation >= filters.stakeSaturation[0] && - m.stake_saturation <= filters.stakeSaturation[1] && - m.avg_uptime >= filters.routingScore[0] && - m.avg_uptime <= filters.routingScore[1], - ); - - setMixnodes({ data: filtered, isLoading: false }); - }; - - const fetchGateways = async () => { - setGateways((d) => ({ ...d, isLoading: true })); - try { - const data = await Api.fetchGateways(); - setGateways({ data, isLoading: false }); - } catch (error) { - setGateways({ - error: error instanceof Error ? error : new Error('Gateways api fail'), - isLoading: false, - }); - } - }; - const fetchValidators = async () => { - try { - const data = await Api.fetchValidators(); - setValidators({ data, isLoading: false }); - } catch (error) { - setValidators({ - error: error instanceof Error ? error : new Error('Validators api fail'), - isLoading: false, - }); - } - }; - const fetchBlock = async () => { - try { - const data = await Api.fetchBlock(); - setBlock({ data, isLoading: false }); - } catch (error) { - setBlock({ - error: error instanceof Error ? error : new Error('Block api fail'), - isLoading: false, - }); - } - }; - const fetchCountryData = async () => { - setCountryData({ data: undefined, isLoading: true }); - try { - const res = await Api.fetchCountryData(); - setCountryData({ data: res, isLoading: false }); - } catch (error) { - setCountryData({ - error: error instanceof Error ? error : new Error('Country Data api fail'), - isLoading: false, - }); - } - }; - - const fetchServiceProviders = async () => { - setServiceProviders({ data: undefined, isLoading: true }); - try { - const res = await Api.fetchServiceProviders(); - const resWithRoutingScorePercentage = res.map((item) => ({ - ...item, - routing_score: item.routing_score - ? `${toPercentIntegerString(item.routing_score.toString())}%` - : item.routing_score, - })); - setServiceProviders({ data: resWithRoutingScorePercentage, isLoading: false }); - } catch (error) { - setServiceProviders({ - error: error instanceof Error ? error : new Error('Service provider api fail'), - isLoading: false, - }); - } - }; - - const updateNavState = (url: string) => { - const updated = navState.map((option) => ({ - ...option, - isActive: option.url === url, - })); - updateNav(updated); - }; - - React.useEffect(() => { - if (environment === 'mainnet') { - fetchServiceProviders(); - } - }, [environment]); - - React.useEffect(() => { - setEnvironment(getEnvironment()); - Promise.all([fetchOverviewSummary(), fetchGateways(), fetchValidators(), fetchBlock(), fetchCountryData()]); - }, []); - - const state = React.useMemo<State>( - () => ({ - environment, - block, - countryData, - fetchMixnodes, - filterMixnodes, - gateways, - globalError, - mixnodes, - mode, - navState, - summaryOverview, - toggleMode, - updateNavState, - validators, - serviceProviders, - }), - [ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - navState, - summaryOverview, - validators, - serviceProviders, - ], - ); - - return <MainContext.Provider value={state}>{children}</MainContext.Provider>; -}; diff --git a/explorer/src/context/mixnode.tsx b/explorer/src/context/mixnode.tsx deleted file mode 100644 index 36e43d568b..0000000000 --- a/explorer/src/context/mixnode.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import * as React from 'react'; -import { - ApiState, - DelegationsResponse, - UniqDelegationsResponse, - MixNodeDescriptionResponse, - MixNodeEconomicDynamicsStatsResponse, - MixNodeResponseItem, - StatsResponse, - StatusResponse, - UptimeStoryResponse, -} from '../typeDefs/explorer-api'; -import { Api } from '../api'; -import { useApiState } from './hooks'; -import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes'; - -/** - * This context provides the state for a single mixnode by identity key. - */ - -interface MixnodeState { - delegations?: ApiState<DelegationsResponse>; - uniqDelegations?: ApiState<UniqDelegationsResponse>; - description?: ApiState<MixNodeDescriptionResponse>; - economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse>; - mixNode?: ApiState<MixNodeResponseItem | undefined>; - mixNodeRow?: MixnodeRowType; - stats?: ApiState<StatsResponse>; - status?: ApiState<StatusResponse>; - uptimeStory?: ApiState<UptimeStoryResponse>; -} - -export const MixnodeContext = React.createContext<MixnodeState>({}); - -export const useMixnodeContext = (): React.ContextType<typeof MixnodeContext> => - React.useContext<MixnodeState>(MixnodeContext); - -interface MixnodeContextProviderProps { - mixId: string; - children: React.ReactNode; -} - -/** - * Provides a state context for a mixnode by identity - * @param mixId The mixID of the mixnode - */ -export const MixnodeContextProvider: FCWithChildren<MixnodeContextProviderProps> = ({ mixId, children }) => { - const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState<MixNodeResponseItem | undefined>( - mixId, - Api.fetchMixnodeByID, - 'Failed to fetch mixnode by id', - ); - - const [mixNodeRow, setMixnodeRow] = React.useState<MixnodeRowType | undefined>(); - - const [delegations, fetchDelegations, clearDelegations] = useApiState<DelegationsResponse>( - mixId, - Api.fetchDelegationsById, - 'Failed to fetch delegations for mixnode', - ); - - const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] = useApiState<UniqDelegationsResponse>( - mixId, - Api.fetchUniqDelegationsById, - 'Failed to fetch delegations for mixnode', - ); - - const [status, fetchStatus, clearStatus] = useApiState<StatusResponse>( - mixId, - Api.fetchStatusById, - 'Failed to fetch mixnode status', - ); - - const [stats, fetchStats, clearStats] = useApiState<StatsResponse>( - mixId, - Api.fetchStatsById, - 'Failed to fetch mixnode stats', - ); - - const [description, fetchDescription, clearDescription] = useApiState<MixNodeDescriptionResponse>( - mixId, - Api.fetchMixnodeDescriptionById, - 'Failed to fetch mixnode description', - ); - - const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] = - useApiState<MixNodeEconomicDynamicsStatsResponse>( - mixId, - Api.fetchMixnodeEconomicDynamicsStatsById, - 'Failed to fetch mixnode dynamics stats by id', - ); - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>( - mixId, - Api.fetchUptimeStoryById, - 'Failed to fetch mixnode uptime history', - ); - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearMixnodeById(); - clearDelegations(); - clearUniqDelegations(); - clearStatus(); - clearStats(); - clearDescription(); - clearEconomicDynamicsStats(); - clearUptimeHistory(); - - // fetch the mixnode, then get all the other stuff - fetchMixnodeById().then((value) => { - if (!value.data || value.error) { - setMixnodeRow(undefined); - return; - } - setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data)); - Promise.all([ - fetchDelegations(), - fetchUniqDelegations(), - fetchStatus(), - fetchStats(), - fetchDescription(), - fetchEconomicDynamicsStats(), - fetchUptimeHistory(), - ]); - }); - }, [mixId]); - - const state = React.useMemo<MixnodeState>( - () => ({ - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }), - [ - { - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }, - ], - ); - - return <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>; -}; diff --git a/explorer/src/context/nav.tsx b/explorer/src/context/nav.tsx deleted file mode 100644 index 6df749f585..0000000000 --- a/explorer/src/context/nav.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import { DelegateIcon } from '@src/icons/DelevateSVG'; -import { BIG_DIPPER } from '../api/constants'; -import { OverviewSVG } from '../icons/OverviewSVG'; -import { NodemapSVG } from '../icons/NodemapSVG'; -import { NetworkComponentsSVG } from '../icons/NetworksSVG'; - -export type NavOptionType = { - isActive?: boolean; - url: string; - title: string; - Icon?: React.ReactNode; - nested?: NavOptionType[]; - isExpandedChild?: boolean; -}; - -export const originalNavOptions: NavOptionType[] = [ - { - isActive: false, - url: '/', - title: 'Overview', - Icon: <OverviewSVG />, - }, - { - isActive: false, - url: '/network-components', - title: 'Network Components', - Icon: <NetworkComponentsSVG />, - nested: [ - { - url: '/network-components/mixnodes', - title: 'Mixnodes', - }, - { - url: '/network-components/gateways', - title: 'Gateways', - }, - { - url: `${BIG_DIPPER}/validators`, - title: 'Validators', - }, - { - url: 'network-components/service-providers', - title: 'Service Providers', - }, - ], - }, - { - isActive: false, - url: '/nodemap', - title: 'Nodemap', - Icon: <NodemapSVG />, - }, - { - isActive: false, - url: '/delegations', - title: 'Delegations', - Icon: <DelegateIcon />, - }, -]; diff --git a/explorer/src/context/wallet.tsx b/explorer/src/context/wallet.tsx deleted file mode 100644 index 8ac26ee7ca..0000000000 --- a/explorer/src/context/wallet.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; -import { useChain } from '@cosmos-kit/react'; -import { Wallet } from '@cosmos-kit/core'; -import { unymToNym } from '@src/utils/currency'; -import { useNymClient } from '@src/hooks'; -import { MixnetClient, MixnetQueryClient } from '@nymproject/contract-clients/Mixnet.client'; -import { COSMOS_KIT_USE_CHAIN } from '@src/api/constants'; - -interface WalletState { - balance: { status: 'loading' | 'success'; data?: string }; - address?: string; - isWalletConnected: boolean; - isWalletConnecting: boolean; - wallet?: Wallet; - nymClient?: MixnetClient; - nymQueryClient?: MixnetQueryClient; - connectWallet: () => Promise<void>; - disconnectWallet: () => Promise<void>; -} - -export const WalletContext = createContext<WalletState>({ - address: undefined, - balance: { status: 'loading', data: undefined }, - isWalletConnected: false, - isWalletConnecting: false, - nymClient: undefined, - nymQueryClient: undefined, - connectWallet: async () => { - throw new Error('Please connect your wallet'); - }, - disconnectWallet: async () => { - throw new Error('Please connect your wallet'); - }, -}); - -export const WalletProvider = ({ children }: { children: React.ReactNode }) => { - const [balance, setBalance] = useState<WalletState['balance']>({ status: 'loading', data: undefined }); - - const { connect, disconnect, wallet, address, isWalletConnected, isWalletConnecting, getCosmWasmClient } = - useChain(COSMOS_KIT_USE_CHAIN); - - const { nymClient, nymQueryClient } = useNymClient(address); - - const getBalance = async (walletAddress: string) => { - const account = await getCosmWasmClient(); - const uNYMBalance = await account.getBalance(walletAddress, 'unym'); - const NYMBalance = unymToNym(uNYMBalance.amount); - - return NYMBalance; - }; - - const init = async (walletAddress: string) => { - const walletBalance = await getBalance(walletAddress); - setBalance({ status: 'success', data: walletBalance }); - }; - - useEffect(() => { - if (isWalletConnected && address) { - init(address); - } - }, [address, isWalletConnected]); - - const handleConnectWallet = async () => { - await connect(); - }; - - const handleDisconnectWallet = async () => { - await disconnect(); - setBalance({ status: 'loading', data: undefined }); - }; - - const contextValue: WalletState = useMemo( - () => ({ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - connectWallet: handleConnectWallet, - disconnectWallet: handleDisconnectWallet, - }), - [address, balance, wallet, isWalletConnected, isWalletConnecting, nymClient, nymQueryClient], - ); - - return <WalletContext.Provider value={contextValue}>{children}</WalletContext.Provider>; -}; - -export const useWalletContext = () => useContext(WalletContext); diff --git a/explorer/src/errors/ErrorBoundaryContent.tsx b/explorer/src/errors/ErrorBoundaryContent.tsx deleted file mode 100644 index 0389588e26..0000000000 --- a/explorer/src/errors/ErrorBoundaryContent.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { FallbackProps } from 'react-error-boundary'; -import { Alert, AlertTitle, Container } from '@mui/material'; -import { NymThemeProvider } from '@nymproject/mui-theme'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; - -export const ErrorBoundaryContent: FCWithChildren<FallbackProps> = ({ error }) => ( - <NymThemeProvider mode="dark"> - <Container sx={{ py: 4 }}> - <NymLogo height="75px" width="75px" /> - <h1>Oh no! Sorry, something went wrong</h1> - <Alert severity="error" data-testid="error-message"> - <AlertTitle>{error.name}</AlertTitle> - {error.message} - </Alert> - {process.env.NODE_ENV === 'development' && ( - <Alert severity="info" sx={{ mt: 2 }} data-testid="stack-trace"> - <AlertTitle>Stack trace</AlertTitle> - {error.stack} - </Alert> - )} - </Container> - </NymThemeProvider> -); diff --git a/explorer/src/hooks/index.ts b/explorer/src/hooks/index.ts deleted file mode 100644 index ba04855f77..0000000000 --- a/explorer/src/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './useIsMobile'; -export * from './useIsMounted'; -export * from './useGetMixnodeStatusColor'; -export * from './useNymClient'; diff --git a/explorer/src/hooks/useGetMixnodeStatusColor.ts b/explorer/src/hooks/useGetMixnodeStatusColor.ts deleted file mode 100644 index d9d71fe7e1..0000000000 --- a/explorer/src/hooks/useGetMixnodeStatusColor.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useTheme } from '@mui/material'; -import { MixnodeStatus } from '@src/typeDefs/explorer-api'; - -export const useGetMixNodeStatusColor = (status: MixnodeStatus) => { - const theme = useTheme(); - - switch (status) { - case MixnodeStatus.active: - return theme.palette.nym.networkExplorer.mixnodes.status.active; - - case MixnodeStatus.standby: - return theme.palette.nym.networkExplorer.mixnodes.status.standby; - - default: - return theme.palette.nym.networkExplorer.mixnodes.status.inactive; - } -}; diff --git a/explorer/src/hooks/useIsMobile.ts b/explorer/src/hooks/useIsMobile.ts deleted file mode 100644 index 225964b464..0000000000 --- a/explorer/src/hooks/useIsMobile.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Breakpoint, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; - -export const useIsMobile = (queryInput: number | Breakpoint = 'md') => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(queryInput)); - - return isMobile; -}; diff --git a/explorer/src/hooks/useIsMounted.ts b/explorer/src/hooks/useIsMounted.ts deleted file mode 100644 index b18f3a72b7..0000000000 --- a/explorer/src/hooks/useIsMounted.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useRef, useEffect, useCallback } from 'react'; - -export function useIsMounted(): () => boolean { - const ref = useRef(false); - - useEffect(() => { - ref.current = true; - return () => { - ref.current = false; - }; - }, []); - - return useCallback(() => ref.current, [ref]); -} diff --git a/explorer/src/hooks/useNymClient.tsx b/explorer/src/hooks/useNymClient.tsx deleted file mode 100644 index 26b2f529db..0000000000 --- a/explorer/src/hooks/useNymClient.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useChain } from '@cosmos-kit/react'; -import { contracts } from '@nymproject/contract-clients'; -import { MixnetClient, MixnetQueryClient } from '@nymproject/contract-clients/Mixnet.client'; -import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from '@src/api/constants'; - -export const useNymClient = (address?: string) => { - const [nymClient, setNymClient] = useState<MixnetClient>(); - const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>(); - - const { getCosmWasmClient, getSigningCosmWasmClient } = useChain(COSMOS_KIT_USE_CHAIN); - - useEffect(() => { - if (address) { - const init = async () => { - const cosmWasmSigningClient = await getSigningCosmWasmClient(); - const cosmWasmClient = await getCosmWasmClient(); - - const client = new contracts.Mixnet.MixnetClient(cosmWasmSigningClient as any, address, NYM_MIXNET_CONTRACT); - const queryClient = new contracts.Mixnet.MixnetQueryClient(cosmWasmClient as any, NYM_MIXNET_CONTRACT); - - setNymClient(client); - setNymQueryClient(queryClient); - }; - - init(); - } - }, [address, getCosmWasmClient, getSigningCosmWasmClient]); - - return { nymClient, nymQueryClient }; -}; diff --git a/explorer/src/icons/DelevateSVG.tsx b/explorer/src/icons/DelevateSVG.tsx deleted file mode 100644 index 56180d1b8a..0000000000 --- a/explorer/src/icons/DelevateSVG.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { SvgIcon, SvgIconProps } from '@mui/material'; - -export const DelegateIcon = (props: SvgIconProps) => ( - <SvgIcon {...props}> - <path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" /> - <path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" /> - <rect x="18" y="12" width="2" height="3" /> - <rect x="18" y="17" width="2" height="3" /> - <rect x="4" y="17" width="2" height="3" /> - </SvgIcon> -); diff --git a/explorer/src/icons/ElipsSVG.tsx b/explorer/src/icons/ElipsSVG.tsx deleted file mode 100644 index 4a430d6cb6..0000000000 --- a/explorer/src/icons/ElipsSVG.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -export const ElipsSVG: FCWithChildren = () => ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" /> - <defs> - <radialGradient - id="paint0_angular_2549_7570" - cx="0" - cy="0" - r="1" - gradientUnits="userSpaceOnUse" - gradientTransform="translate(12 12.5) rotate(90) scale(12)" - > - <stop stopColor="#22D27E" /> - <stop offset="1" stopColor="#9002FF" /> - </radialGradient> - </defs> - </svg> -); diff --git a/explorer/src/icons/GatewaysSVG.tsx b/explorer/src/icons/GatewaysSVG.tsx deleted file mode 100644 index 00e4a21198..0000000000 --- a/explorer/src/icons/GatewaysSVG.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const GatewaysSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M16.2 12H22.7" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M1.30005 12H12" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path - d="M20.1 9.40015L22.7 12.0001L20.1 14.6001" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - </svg> - ); -}; diff --git a/explorer/src/icons/LightSwitchSVG.tsx b/explorer/src/icons/LightSwitchSVG.tsx deleted file mode 100644 index 7a32590dfc..0000000000 --- a/explorer/src/icons/LightSwitchSVG.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const LightSwitchSVG: FCWithChildren = () => { - const { palette } = useTheme(); - return ( - <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z" - fill={palette.background.default} - /> - <path d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z" fill={palette.text.primary} /> - </svg> - ); -}; diff --git a/explorer/src/icons/MixnodesSVG.tsx b/explorer/src/icons/MixnodesSVG.tsx deleted file mode 100644 index 56902cb0de..0000000000 --- a/explorer/src/icons/MixnodesSVG.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const MixnodesSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0621L2.97681 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M13.0103 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 2.99512L13.0103 23.0621" stroke={color} strokeMiterlimit="10" /> - <path - d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - </svg> - ); -}; diff --git a/explorer/src/icons/MobileDrawerClose.tsx b/explorer/src/icons/MobileDrawerClose.tsx deleted file mode 100644 index 6c8ecfacbc..0000000000 --- a/explorer/src/icons/MobileDrawerClose.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; - -export const MobileDrawerClose: FCWithChildren = (props) => ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -5 24 24" width="25" height="25" {...props}> - <path - d="M0 12H13V10H0V12ZM0 7H10V5H0V7ZM0 0V2H13V0H0ZM18 9.59L14.42 6L18 2.41L16.59 1L11.59 6L16.59 11L18 9.59Z" - fill="#F2F2F2" - /> - </svg> -); diff --git a/explorer/src/icons/NetworksSVG.tsx b/explorer/src/icons/NetworksSVG.tsx deleted file mode 100644 index 0db2b9bdfb..0000000000 --- a/explorer/src/icons/NetworksSVG.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NetworkComponentsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M17.2 10.5V4.40002L12 1.40002L6.8 4.40002V10.5L12 13.5L17.2 10.5Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M12 19.6V13.5L6.8 10.5L1.5 13.5V19.6L6.8 22.6L12 19.6Z" stroke={color} strokeMiterlimit="10" /> - <path d="M22.5 19.6V13.5L17.2 10.5L12 13.5V19.6L17.2 22.6L22.5 19.6Z" stroke={color} strokeMiterlimit="10" /> - </svg> - ); -}; diff --git a/explorer/src/icons/NodemapSVG.tsx b/explorer/src/icons/NodemapSVG.tsx deleted file mode 100644 index 9486d64dcc..0000000000 --- a/explorer/src/icons/NodemapSVG.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NodemapSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M1 9.6999C1 5.0999 4.7 1.3999 9.3 1.3999C13.9 1.3999 17.6 5.0999 17.6 9.6999C17.6 14.2999 9.3 21.5999 9.3 21.5999C9.3 21.5999 1 14.2999 1 9.6999Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M9.30005 12C11.233 12 12.8 10.433 12.8 8.5C12.8 6.567 11.233 5 9.30005 5C7.36705 5 5.80005 6.567 5.80005 8.5C5.80005 10.433 7.36705 12 9.30005 12Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M1.5 22.5999H17.1" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer/src/icons/NymVpn.tsx b/explorer/src/icons/NymVpn.tsx deleted file mode 100644 index 788b78a4f9..0000000000 --- a/explorer/src/icons/NymVpn.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react'; - -interface DiscordIconProps { - size?: { width: number; height: number }; -} - -export const NymVpnIcon: FCWithChildren<DiscordIconProps> = ({ size }) => ( - <svg width={size?.width} height={size?.height} viewBox="0 0 170 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M19.6118 0.128906H19.5405V0.187854V20.7961L10.7849 0.164277L10.773 0.128906H10.7255H5.75959H0.187819H0.128418V0.187854V23.8142V23.8732H0.187819H5.75959H5.81899V23.8142V3.17063L14.6103 23.8378L14.6222 23.8732H14.6697H19.6118H25.1717H25.2311V23.8142V0.187854V0.128906H25.1717H19.6118Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M19.4121 0H25.3603V24H14.5297L14.4901 23.8819L5.94824 3.80121V24H0V0H10.8663L10.906 0.118132L19.4121 20.1621V0ZM19.5409 20.7951L10.7853 0.163225L10.7734 0.127854H0.128835V23.8721H5.81941V3.16958L14.6107 23.8368L14.6226 23.8721H25.2315V0.127854H19.5409V20.7951Z" - fill="white" - /> - <path - d="M89.8116 0.128906H79.1908H79.1314L79.1195 0.176068L73.6784 20.8904L68.2255 0.176068L68.2136 0.128906H68.1661H57.5215H57.4502V0.187854V23.8142V23.8732H57.5215H63.0814H63.1408V23.8142V3.33568L68.5225 23.826L68.5343 23.8732H68.5937H78.7394H78.7869L78.7988 23.826L84.1804 3.33568V23.8142V23.8732H84.2398H89.8116H89.871V23.8142V0.187854V0.128906H89.8116Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M79.0312 0H90.0003V24H84.052V4.33208L78.9242 23.856L78.9238 23.8572L78.8879 24H68.4342L68.3982 23.8572L68.3979 23.856L63.27 4.33208V24H57.3218V0H68.3146L68.3505 0.142699L68.3509 0.144015L73.6787 20.383L78.9949 0.144015L78.9953 0.142765L79.0312 0ZM73.6788 20.8894L68.2259 0.175015L68.214 0.127854H57.4506V23.8721H63.1412V3.33463L68.5229 23.825L68.5348 23.8721H78.7873L78.7992 23.825L84.1809 3.33463V23.8721H89.8714V0.127854H79.1318L79.1199 0.175015L73.6788 20.8894Z" - fill="white" - /> - <path - d="M48.2909 0.128906H48.2553L48.2434 0.152487L41.4836 11.8124L34.6882 0.152487L34.6763 0.128906H34.6407H28.2135H28.0947L28.1541 0.223225L38.6205 18.2142V23.8142V23.8732H38.6799H44.2517H44.3111V23.8142V18.2142L54.7775 0.223225L54.8369 0.128906H54.7181H48.2909Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M48.1757 0H55.0693L54.8879 0.288036L44.4399 18.2474V24H38.4917V18.2474L28.0437 0.288036L27.8623 0H34.756L34.8017 0.0907854L41.4833 11.5555L48.1299 0.0909153L48.1757 0ZM48.2434 0.151434L41.4836 11.8114L34.6882 0.151434L34.6763 0.127854H28.0948L28.1542 0.222173L38.6205 18.2131V23.8721H44.3111V18.2131L54.7775 0.222173L54.8369 0.127854H48.2553L48.2434 0.151434Z" - fill="white" - /> - <path - d="M169.238 0V24H166.422C166.006 24 165.654 23.9341 165.366 23.8023C165.088 23.6596 164.811 23.418 164.534 23.0776L153.542 8.76321C153.584 9.19149 153.611 9.60878 153.622 10.0151C153.643 10.4104 153.654 10.7838 153.654 11.1352V24H148.886V0H151.734C151.968 0 152.166 0.0109813 152.326 0.032944C152.486 0.0549066 152.63 0.0988326 152.758 0.164722C152.886 0.219629 153.008 0.30199 153.126 0.411805C153.243 0.521619 153.376 0.669869 153.526 0.856553L164.614 15.2697C164.56 14.8085 164.523 14.3638 164.502 13.9355C164.48 13.4962 164.47 13.0844 164.47 12.7001V0H169.238Z" - fill="#A8A6A6" - /> - <path - d="M134.206 11.7776C135.614 11.7776 136.627 11.4317 137.246 10.7399C137.865 10.048 138.174 9.08167 138.174 7.84077C138.174 7.29169 138.094 6.79204 137.934 6.3418C137.774 5.89156 137.529 5.50721 137.198 5.18874C136.878 4.8593 136.467 4.60673 135.966 4.43102C135.475 4.25532 134.889 4.16747 134.206 4.16747H131.39V11.7776H134.206ZM134.206 0C135.849 0 137.257 0.203157 138.43 0.609471C139.614 1.0048 140.585 1.55388 141.342 2.25669C142.11 2.95951 142.675 3.78861 143.038 4.74399C143.401 5.69938 143.582 6.73164 143.582 7.84077C143.582 9.03775 143.395 10.1359 143.022 11.1352C142.649 12.1345 142.078 12.9911 141.31 13.7049C140.542 14.4187 139.566 14.9787 138.382 15.385C137.209 15.7804 135.817 15.978 134.206 15.978H131.39V24H125.982V0H134.206Z" - fill="#A8A6A6" - /> - <path - d="M121.584 0L112.24 24H107.344L98 0H102.352C102.821 0 103.2 0.115305 103.488 0.345915C103.776 0.565545 103.995 0.851064 104.144 1.20247L108.656 14.0508C108.869 14.6108 109.077 15.2258 109.28 15.8957C109.483 16.5546 109.675 17.2464 109.856 17.9712C110.005 17.2464 110.171 16.5546 110.352 15.8957C110.544 15.2258 110.747 14.6108 110.96 14.0508L115.44 1.20247C115.557 0.894989 115.765 0.620452 116.064 0.378861C116.373 0.126287 116.752 0 117.2 0H121.584Z" - fill="#A8A6A6" - /> - </svg> -); - -NymVpnIcon.defaultProps = { - size: { width: 80, height: 12 }, -}; diff --git a/explorer/src/icons/OverviewSVG.tsx b/explorer/src/icons/OverviewSVG.tsx deleted file mode 100644 index 2ced0bb17b..0000000000 --- a/explorer/src/icons/OverviewSVG.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const OverviewSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M1.4 21.6H22.6" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M14.1 2.40002H9.9V21.5H14.1V2.40002Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M20.8 6.59998H16.6V21.5H20.8V6.59998Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M7.4 11.8H3.2V21.6H7.4V11.8Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer/src/icons/TokenSVG.tsx b/explorer/src/icons/TokenSVG.tsx deleted file mode 100644 index 94ab1468c9..0000000000 --- a/explorer/src/icons/TokenSVG.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; - -export const TokenSVG: FCWithChildren = () => { - const color = 'white'; - - return ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <g clipPath="url(#clip0_2549_7563)"> - <path - d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z" - fill={color} - /> - <path - d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_2549_7563"> - <rect width="24" height="24" fill="white" transform="translate(0 0.5)" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer/src/icons/ValidatorsSVG.tsx b/explorer/src/icons/ValidatorsSVG.tsx deleted file mode 100644 index cf03f1c330..0000000000 --- a/explorer/src/icons/ValidatorsSVG.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const ValidatorsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clipPath="url(#clip0)"> - <path - d="M18.2001 18.4V19.7001C18.2001 21.4001 16.9 22.7001 15.2 22.7001H4.30005C2.60005 22.7001 1.30005 21.4001 1.30005 19.7001V4.30005C1.30005 2.60005 2.60005 1.30005 4.30005 1.30005H15.1C16.8 1.30005 18.1 2.60005 18.1 4.30005V5.60005V18.4H18.2001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M13.4 22.7001H17.4C19.1 22.7001 20.4 21.4001 20.4 19.7001V18.4V5.60005V4.30005C20.4 2.60005 19.1 1.30005 17.4 1.30005H11.5" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M15.2 22.7001H19.7C21.4 22.7001 22.7 21.4001 22.7 19.7001V18.4V5.60005V4.30005C22.7 2.60005 21.4 1.30005 19.7 1.30005H13.8" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M5 12.3L7.9 15.3L14.5 8.69995" - stroke={color} - strokeWidth="2" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - </g> - <defs> - <clipPath id="clip0"> - <rect width="24" height="24" fill="white" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer/src/icons/socials/DiscordIcon.tsx b/explorer/src/icons/socials/DiscordIcon.tsx deleted file mode 100644 index 11a9fd1c67..0000000000 --- a/explorer/src/icons/socials/DiscordIcon.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface DiscordIconProps { - size?: number | string; - color?: string; -} - -export const DiscordIcon: FCWithChildren<DiscordIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2296)"> - <path - d="M12.4 0C5.80002 0 0.400024 5.4 0.400024 12C0.400024 18.6 5.80002 24 12.4 24C19 24 24.4 18.6 24.4 12C24.4 5.4 19 0 12.4 0ZM20.1 15.9C18.8 16.9 17.5 17.5 16.2 17.9C16.2 17.9 16.2 17.9 16.1 17.9C15.8 17.5 15.5 17.1 15.3 16.6V16.5C15.7 16.3 16.1 16.1 16.5 15.9V15.8C16.4 15.7 16.3 15.7 16.3 15.6C16.3 15.6 16.3 15.6 16.2 15.6C13.7 16.8 10.9 16.8 8.40002 15.6C8.40002 15.6 8.40002 15.6 8.30002 15.6C8.20002 15.7 8.10002 15.7 8.10002 15.8V15.9C8.50002 16.1 8.90002 16.3 9.30002 16.5C9.30002 16.5 9.30002 16.5 9.30002 16.6C9.10002 17.1 8.80002 17.5 8.50002 17.9C8.50002 17.9 8.50002 17.9 8.40002 17.9C7.10002 17.5 5.90002 16.9 4.50002 15.9C4.40002 13 5.00002 10.1 7.00002 7.1C8.00002 6.6 9.00002 6.3 10.2 6.1C10.2 6.1 10.2 6.1 10.3 6.1C10.4 6.3 10.6 6.7 10.7 6.9C11.9 6.7 13.1 6.7 14.2 6.9C14.3 6.7 14.5 6.3 14.6 6.1C14.6 6.1 14.6 6.1 14.7 6.1C15.8 6.3 16.9 6.6 17.9 7.1C19.5 9.7 20.4 12.6 20.1 15.9Z" - fill={color} - /> - <path - d="M15 11C14.2 11 13.6 11.7 13.6 12.6C13.6 13.5 14.2 14.2 15 14.2C15.8 14.2 16.4 13.5 16.4 12.6C16.4 11.7 15.8 11 15 11Z" - fill={color} - /> - <path - d="M9.80002 11C9.10002 11 8.40002 11.7 8.40002 12.6C8.40002 13.5 9.00002 14.2 9.80002 14.2C10.6 14.2 11.2 13.5 11.2 12.6C11.2 11.7 10.6 11 9.80002 11Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2296"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -DiscordIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/GitHubIcon.tsx b/explorer/src/icons/socials/GitHubIcon.tsx deleted file mode 100644 index a8fde9b0a9..0000000000 --- a/explorer/src/icons/socials/GitHubIcon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface GitHubIconProps { - size?: number | string; - color?: string; -} - -export const GitHubIcon: FCWithChildren<GitHubIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2302)"> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M12.7 0C5.90002 0 0.400024 5.5 0.400024 12.3C0.400024 17.7 3.90002 22.3 8.80002 24C9.40002 24.1 9.60002 23.7 9.60002 23.4C9.60002 23.1 9.60002 22.1 9.60002 21.1C6.50002 21.7 5.70002 20.3 5.50002 19.7C5.40002 19.3 4.80002 18.3 4.20002 18C3.80002 17.8 3.20002 17.2 4.20002 17.2C5.20002 17.2 5.90002 18.1 6.10002 18.5C7.20002 20.4 9.00002 19.8 9.70002 19.5C9.80002 18.7 10.1 18.2 10.5 17.9C7.80002 17.6 4.90002 16.5 4.90002 11.8C4.90002 10.5 5.40002 9.4 6.20002 8.5C6.00002 8 5.60002 6.8 6.30002 5.1C6.30002 5.1 7.30002 4.8 9.70002 6.4C10.7 6.1 11.7 6 12.8 6C13.8 6 14.9 6.1 15.9 6.4C18.3 4.8 19.3 5.1 19.3 5.1C20 6.8 19.5 8.1 19.4 8.4C20.2 9.3 20.7 10.4 20.7 11.7C20.7 16.4 17.8 17.5 15.1 17.8C15.5 18.2 15.9 18.9 15.9 20.1C15.9 21.7 15.9 23.1 15.9 23.5C15.9 23.8 16.1 24.2 16.7 24.1C21.6 22.5 25.1 17.9 25.1 12.4C25 5.5 19.5 0 12.7 0Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2302"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -GitHubIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/TelegramIcon.tsx b/explorer/src/icons/socials/TelegramIcon.tsx deleted file mode 100644 index cf150a4a2f..0000000000 --- a/explorer/src/icons/socials/TelegramIcon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface TelegramIconProps { - size?: number | string; - color?: string; -} - -export const TelegramIcon: FCWithChildren<TelegramIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <path - d="M12.4 24C19.029 24 24.4 18.629 24.4 12C24.4 5.371 19.029 0 12.4 0C5.77102 0 0.400024 5.371 0.400024 12C0.400024 18.629 5.77102 24 12.4 24ZM5.89102 11.74L17.461 7.279C17.998 7.085 18.467 7.41 18.293 8.222L18.294 8.221L16.324 17.502C16.178 18.16 15.787 18.32 15.24 18.01L12.24 15.799L10.793 17.193C10.633 17.353 10.498 17.488 10.188 17.488L10.401 14.435L15.961 9.412C16.203 9.199 15.907 9.079 15.588 9.291L8.71702 13.617L5.75502 12.693C5.11202 12.489 5.09802 12.05 5.89102 11.74Z" - fill={color} - /> - </svg> - ); -}; - -TelegramIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/TwitterIcon.tsx b/explorer/src/icons/socials/TwitterIcon.tsx deleted file mode 100644 index 6f0b545f56..0000000000 --- a/explorer/src/icons/socials/TwitterIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface TwitterIconProps { - size?: number | string; - color?: string; -} - -export const TwitterIcon: FCWithChildren<TwitterIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2294)"> - <path - d="M12.4 0C5.77362 0 0.400024 5.3736 0.400024 12C0.400024 18.6264 5.77362 24 12.4 24C19.0264 24 24.4 18.6264 24.4 12C24.4 5.3736 19.0264 0 12.4 0ZM17.8791 9.35632C17.8844 9.47443 17.887 9.59308 17.887 9.71228C17.887 13.3519 15.1166 17.5488 10.0502 17.549H10.0504H10.0502C8.49475 17.549 7.0473 17.0931 5.82837 16.3118C6.04388 16.3372 6.26324 16.3499 6.48535 16.3499C7.77588 16.3499 8.9635 15.9097 9.90631 15.1708C8.70056 15.1485 7.68396 14.3522 7.33313 13.2578C7.50104 13.29 7.67371 13.3076 7.85077 13.3076C8.10217 13.3076 8.3457 13.2737 8.57715 13.2105C7.31683 12.9582 6.36743 11.8444 6.36743 10.5106C6.36743 10.4982 6.36743 10.487 6.3678 10.4755C6.73895 10.6818 7.16339 10.806 7.6153 10.8199C6.87573 10.3264 6.38959 9.48285 6.38959 8.52722C6.38959 8.02258 6.526 7.5498 6.76257 7.14276C8.12085 8.80939 10.1508 9.90546 12.4399 10.0206C12.3927 9.81885 12.3683 9.60864 12.3683 9.39258C12.3683 7.87207 13.6019 6.63849 15.123 6.63849C15.9153 6.63849 16.6309 6.97339 17.1335 7.50879C17.761 7.38501 18.3502 7.15576 18.8825 6.84027C18.6765 7.48315 18.24 8.02258 17.6713 8.36371C18.2285 8.29706 18.7595 8.14929 19.2529 7.92993C18.8843 8.48236 18.4169 8.96759 17.8791 9.35632V9.35632Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2294"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -TwitterIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/index.html b/explorer/src/index.html deleted file mode 100644 index dd5a4761ee..0000000000 --- a/explorer/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - -<head> - <meta charset="utf-8" /> - <title>Nym Network Explorer - - - - -
- - - diff --git a/explorer/src/index.tsx b/explorer/src/index.tsx deleted file mode 100644 index 0b76be7ab1..0000000000 --- a/explorer/src/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import { createRoot } from 'react-dom/client'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { ErrorBoundary } from 'react-error-boundary'; -import { MainContextProvider } from './context/main'; -import { NetworkExplorerThemeProvider } from './theme'; -import { ErrorBoundaryContent } from './errors/ErrorBoundaryContent'; -import CosmosKitProvider from './context/cosmos-kit'; -import '@interchain-ui/react/styles'; -import { App } from './App'; -import { WalletProvider } from './context/wallet'; -import './styles.css'; - -const elem = document.getElementById('app'); - -if (elem) { - const root = createRoot(elem); - root.render( - - - - - - - - - - - - - , - ); -} diff --git a/explorer/src/pages/404/index.tsx b/explorer/src/pages/404/index.tsx deleted file mode 100644 index 02f4247fc5..0000000000 --- a/explorer/src/pages/404/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from 'react'; -import { Box, Button, Grid, Paper, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { useNavigate } from 'react-router-dom'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; -import { useMainContext } from '../../context/main'; - -export const Page404 = () => { - const navigate = useNavigate(); - const { mode } = useMainContext(); - const theme = useTheme(); - return ( - - - - - - Oh No! - It looks like you might be lost. - - Please try the link again or navigate back to{' '} - - - - - - - ); -}; diff --git a/explorer/src/pages/Delegations/index.tsx b/explorer/src/pages/Delegations/index.tsx deleted file mode 100644 index 32e178f2fc..0000000000 --- a/explorer/src/pages/Delegations/index.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, { useEffect } from 'react'; -import { Alert, AlertTitle, Box, Button, Card, Chip, IconButton, Tooltip, Typography } from '@mui/material'; -import { Link, useNavigate } from 'react-router-dom'; -import { DelegationModal, DelegationModalProps, Title, UniversalDataGrid } from '@src/components'; -import { useWalletContext } from '@src/context/wallet'; -import { GridColDef } from '@mui/x-data-grid'; -import { unymToNym } from '@src/utils/currency'; -import { - DelegationWithRewards, - DelegationsProvider, - PendingEvent, - useDelegationsContext, -} from '@src/context/delegations'; -import { urls } from '@src/utils'; -import { useClipboard } from 'use-clipboard-copy'; -import { Close } from '@mui/icons-material'; - -const mapToDelegationsRow = (delegation: DelegationWithRewards, index: number) => ({ - identity: delegation.identityKey, - mix_id: delegation.mix_id, - amount: `${unymToNym(delegation.amount.amount)} NYM`, - rewards: `${unymToNym(delegation.rewards)} NYM`, - id: index, - pending: delegation.pending, -}); - -const Banner = ({ onClose }: { onClose: () => void }) => { - const { copy } = useClipboard(); - - return ( - - - - } - > - Mobile Delegations Beta - - - This is a beta release for mobile delegations If you have any feedback or feature suggestions contact us at - support@nymte.ch - - - - - ); -}; - -const DelegationsPage = () => { - const [confirmationModalProps, setConfirmationModalProps] = React.useState(); - const [isLoading, setIsLoading] = React.useState(false); - const [showBanner, setShowBanner] = React.useState(true); - - const { isWalletConnected } = useWalletContext(); - const { handleGetDelegations, handleUndelegate, delegations } = useDelegationsContext(); - const navigate = useNavigate(); - - useEffect(() => { - let timeoutId: NodeJS.Timeout; - - const fetchDelegations = async () => { - setIsLoading(true); - try { - await handleGetDelegations(); - } catch (error) { - setConfirmationModalProps({ - status: 'error', - message: "Couldn't fetch delegations. Please try again later.", - }); - } finally { - setIsLoading(false); - - timeoutId = setTimeout(() => { - fetchDelegations(); - }, 60_000); - } - }; - - fetchDelegations(); - - return () => { - clearTimeout(timeoutId); - }; - }, [handleGetDelegations]); - - const getTooltipTitle = (pending: PendingEvent) => { - if (pending?.kind === 'undelegate') { - return 'You have an undelegation pending'; - } - - if (pending?.kind === 'delegate') { - return `You have a delegation pending worth ${unymToNym(pending.amount.amount)} NYM`; - } - - return undefined; - }; - - const onUndelegate = async (mixId: number) => { - setConfirmationModalProps({ status: 'loading' }); - - try { - const tx = await handleUndelegate(mixId); - - if (tx) { - setConfirmationModalProps({ - status: 'success', - message: 'Undelegation can take up to one hour to process', - transactions: [ - { url: `${urls('MAINNET').blockExplorer}/transaction/${tx.transactionHash}`, hash: tx.transactionHash }, - ], - }); - } - } catch (error) { - if (error instanceof Error) { - setConfirmationModalProps({ status: 'error', message: error.message }); - } - } - }; - - const columns: GridColDef[] = [ - { - field: 'identity', - headerName: 'Identity Key', - width: 400, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'mix_id', - headerName: 'Mix ID', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'amount', - headerName: 'Amount', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'rewards', - headerName: 'Rewards', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'undelegate', - headerName: '', - minWidth: 150, - flex: 1, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'right', - renderCell: (params) => { - const { pending } = params.row; - - return ( - - {pending ? ( - e.stopPropagation()} - PopperProps={{}} - > - - - ) : ( - - )} - - ); - }, - }, - ]; - - const handleRowClick = (params: any) => { - navigate(`/network-components/mixnode/${params.row.mix_id}`); - }; - - return ( - - {confirmationModalProps && ( - { - if (confirmationModalProps.status === 'success') { - await handleGetDelegations(); - } - setConfirmationModalProps(undefined); - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - {showBanner && setShowBanner(false)} />} - - - <Button variant="contained" color="primary" component={Link} to="/network-components/mixnodes"> - Delegate - </Button> - </Box> - {!isWalletConnected ? ( - <Box> - <Typography mb={2} variant="h6"> - Connect your wallet to view your delegations. - </Typography> - </Box> - ) : null} - - <Card - sx={{ - mt: 2, - padding: 2, - height: '100%', - }} - > - <UniversalDataGrid - onRowClick={handleRowClick} - rows={delegations?.map(mapToDelegationsRow) || []} - columns={columns} - loading={isLoading} - /> - </Card> - </Box> - ); -}; - -export const Delegations = () => ( - <DelegationsProvider> - <DelegationsPage /> - </DelegationsProvider> -); diff --git a/explorer/src/pages/GatewayDetail/index.tsx b/explorer/src/pages/GatewayDetail/index.tsx deleted file mode 100644 index 3382947512..0000000000 --- a/explorer/src/pages/GatewayDetail/index.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import * as React from 'react'; -import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { GatewayBond } from '../../typeDefs/explorer-api'; -import { ColumnsType, DetailTable } from '../../components/DetailTable'; -import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways'; -import { ComponentError } from '../../components/ComponentError'; -import { ContentCard } from '../../components/ContentCard'; -import { TwoColSmallTable } from '../../components/TwoColSmallTable'; -import { UptimeChart } from '../../components/UptimeChart'; -import { GatewayContextProvider, useGatewayContext } from '../../context/gateway'; -import { useMainContext } from '../../context/main'; -import { Title } from '../../components/Title'; - -const columns: ColumnsType[] = [ - { - field: 'identity_key', - title: 'Identity Key', - headerAlign: 'left', - width: 230, - }, - { - field: 'bond', - title: 'Bond', - headerAlign: 'left', - }, - { - field: 'node_performance', - title: 'Routing Score', - headerAlign: 'left', - tooltipInfo: - "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', - headerAlign: 'left', - tooltipInfo: "Gateway's average routing score in the last 24 hours", - }, - { - field: 'host', - title: 'IP', - headerAlign: 'left', - width: 99, - }, - { - field: 'location', - title: 'Location', - headerAlign: 'left', - }, - { - field: 'owner', - title: 'Owner', - headerAlign: 'left', - }, - { - field: 'version', - title: 'Version', - headerAlign: 'left', - }, -]; - -/** - * Shows gateway details - */ -const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayBond | undefined }) => { - const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>(); - const [status, setStatus] = React.useState<number[] | undefined>(); - const { uptimeReport, uptimeStory } = useGatewayContext(); - - React.useEffect(() => { - if (uptimeReport?.data && selectedGateway) { - setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)); - } - }, [uptimeReport, selectedGateway]); - - React.useEffect(() => { - if (enrichGateway) { - setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]); - } - }, [enrichGateway]); - - return ( - <Box component="main"> - <Title text="Gateway Detail" /> - - <Grid container> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Gateway detail table" - rows={enrichGateway ? [enrichGateway] : []} - /> - </Grid> - </Grid> - - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Gateway Status"> - <TwoColSmallTable - loading={false} - keys={['Mix port', 'Client WS API Port']} - values={status.map((each) => each)} - icons={status.map((elem) => !!elem)} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ); -}; - -/** - * Guard component to handle loading and not found states - */ -const PageGatewayDetailGuard: FCWithChildren = () => { - const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>(); - const { gateways } = useMainContext(); - const { id } = useParams<{ id: string | undefined }>(); - - React.useEffect(() => { - if (gateways?.data) { - setSelectedGateway(gateways.data.find((g) => g.gateway.identity_key === id)); - } - }, [gateways, id]); - - if (gateways?.isLoading) { - return <CircularProgress />; - } - - if (gateways?.error) { - // eslint-disable-next-line no-console - console.error(gateways?.error); - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ); - } - - // loaded, but not found - if (gateways && !gateways.isLoading && !gateways.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Gateway not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ); - } - - return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />; -}; - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -export const PageGatewayDetail: FCWithChildren = () => { - const { id } = useParams<{ id: string | undefined }>(); - - if (!id) { - return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>; - } - - return ( - <GatewayContextProvider gatewayIdentityKey={id}> - <PageGatewayDetailGuard /> - </GatewayContextProvider> - ); -}; diff --git a/explorer/src/pages/Gateways/index.tsx b/explorer/src/pages/Gateways/index.tsx deleted file mode 100644 index c04b7eaf40..0000000000 --- a/explorer/src/pages/Gateways/index.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import * as React from 'react'; -import { Box, Card, Grid, Stack } 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, 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'; -import { TableToolbar } from '../../components/TableToolbar'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { unymToNym } from '../../utils/currency'; -import { Tooltip } from '../../components/Tooltip'; -import { NYM_BIG_DIPPER } from '../../api/constants'; -import { splice } from '../../utils'; -import { VersionDisplaySelector, VersionSelectOptions } from '../../components/Gateways/VersionDisplaySelector'; -import StyledLink from '../../components/StyledLink'; - -export const PageGateways: FCWithChildren = () => { - const { gateways } = useMainContext(); - const [filteredGateways, setFilteredGateways] = React.useState<GatewayResponse>([]); - const [pageSize, setPageSize] = React.useState<string>('50'); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - const [versionFilter, setVersionFilter] = React.useState<VersionSelectOptions>(VersionSelectOptions.all); - - const theme = useTheme(); - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - const highestVersion = React.useMemo(() => { - if (gateways?.data) { - const versions = gateways.data.reduce((a: string[], b) => [...a, b.gateway.version], []); - const [lastestVersion] = versions.sort(rcompare); - return lastestVersion; - } - // fallback value - return '2.0.0'; - }, [gateways]); - - const filterByLatestVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version); - return versionDiff === 'patch' || versionDiff === null; - }); - if (filtered) return filtered; - return []; - }, [gateways]); - - const filterByOlderVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version); - return versionDiff === 'major' || versionDiff === 'minor'; - }); - if (filtered) return filtered; - return []; - }, [gateways]); - - const filteredByVersion = React.useMemo(() => { - switch (versionFilter) { - case VersionSelectOptions.latestVersion: - return filterByLatestVersions; - case VersionSelectOptions.olderVersions: - return filterByOlderVersions; - case VersionSelectOptions.all: - return gateways?.data || []; - default: - return []; - } - }, [versionFilter, gateways]); - - React.useEffect(() => { - if (searchTerm === '') { - setFilteredGateways(filteredByVersion); - } else { - const filtered = filteredByVersion.filter((g) => { - if ( - g.gateway.location.toLowerCase().includes(searchTerm) || - g.gateway.identity_key.toLocaleLowerCase().includes(searchTerm) || - g.owner.toLowerCase().includes(searchTerm) - ) { - return g; - } - return null; - }); - - if (filtered) { - setFilteredGateways(filtered); - } - } - }, [searchTerm, gateways?.data, versionFilter]); - - const columns: GridColDef[] = [ - { - field: 'identity_key', - renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, - headerClassName: 'MuiDataGrid-header-override', - width: 400, - disableColumnMenu: true, - headerAlign: 'center', - renderCell: (params: GridRenderCellParams) => ( - <Stack direction="row" gap={1}> - <CopyToClipboard smallIcons value={params.value} tooltip={`Copy identity key ${params.value} to clipboard`} /> - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`}>{params.value}</StyledLink> - </Stack> - ), - }, - { - field: 'node_performance', - align: 'center', - 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" /> - </> - ), - width: 120, - disableColumnMenu: true, - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`} data-testid="pledge-amount"> - {`${params.value}%`} - </StyledLink> - ), - }, - { - field: 'version', - align: 'center', - renderHeader: () => <CustomColumnHeading headingTitle="Version" />, - width: 150, - disableColumnMenu: true, - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`} data-testid="version"> - {params.value} - </StyledLink> - ), - 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) => ( - <Box - onClick={() => handleSearch(params.value as string)} - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip text={params.value} id="gateway-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {params.value} - </Box> - </Tooltip> - </Box> - ), - }, - { - field: 'host', - renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`} data-testid="host"> - {params.value} - </StyledLink> - ), - }, - { - field: 'owner', - headerName: 'Owner', - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`${NYM_BIG_DIPPER}/account/${params.value}`} target="_blank" data-testid="owner"> - {splice(7, 29, params.value)} - </StyledLink> - ), - }, - { - field: 'bond', - width: 150, - disableColumnMenu: true, - type: 'number', - renderHeader: () => <CustomColumnHeading headingTitle="Bond" />, - headerClassName: 'MuiDataGrid-header-override', - headerAlign: 'left', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`} data-testid="pledge-amount"> - {`${unymToNym(params.value, 6)}`} - </StyledLink> - ), - }, - ]; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - if (gateways?.data) { - return ( - <> - <Box mb={2}> - <Title text="Gateways" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - childrenBefore={ - <VersionDisplaySelector - handleChange={(option) => setVersionFilter(option)} - selected={versionFilter} - /> - } - /> - <UniversalDataGrid - pagination - rows={gatewayToGridRow(filteredGateways)} - columns={columns} - pageSize={pageSize} - /> - </Card> - </Grid> - </Grid> - </> - ); - } - return null; -}; diff --git a/explorer/src/pages/MixnodeDetail/index.tsx b/explorer/src/pages/MixnodeDetail/index.tsx deleted file mode 100644 index 5ceaa36867..0000000000 --- a/explorer/src/pages/MixnodeDetail/index.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import * as React from 'react'; -import { Alert, AlertTitle, Box, CircularProgress, Grid, Typography } from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { ColumnsType, DetailTable } from '../../components/DetailTable'; -import { BondBreakdownTable } from '../../components/MixNodes/BondBreakdown'; -import { DelegatorsInfoTable, EconomicsInfoColumns, EconomicsInfoRows } from '../../components/MixNodes/Economics'; -import { ComponentError } from '../../components/ComponentError'; -import { ContentCard } from '../../components/ContentCard'; -import { TwoColSmallTable } from '../../components/TwoColSmallTable'; -import { UptimeChart } from '../../components/UptimeChart'; -import { WorldMap } from '../../components/WorldMap'; -import { MixNodeDetailSection } from '../../components/MixNodes/DetailSection'; -import { MixnodeContextProvider, useMixnodeContext } from '../../context/mixnode'; -import { Title } from '../../components/Title'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -const columns: ColumnsType[] = [ - { - field: 'owner', - title: 'Owner', - width: '15%', - }, - { - field: 'identity_key', - title: 'Identity Key', - width: '15%', - }, - - { - field: 'bond', - title: 'Stake', - 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 940k NYMs, 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 %', - tooltipInfo: "Percentage of the operator's bond to the total stake on the node", - }, - - { - field: 'host', - width: '10%', - title: 'Host', - }, - { - field: 'location', - title: 'Location', - }, - - { - field: 'layer', - title: 'Layer', - }, -]; - -/** - * Shows mix node details - */ -const PageMixnodeDetailWithState: FCWithChildren = () => { - const { mixNode, mixNodeRow, description, stats, status, uptimeStory, uniqDelegations } = useMixnodeContext(); - const isMobile = useIsMobile(); - return ( - <Box component="main"> - <Title text="Mixnode Detail" /> - <Grid container spacing={2} mt={1} mb={6}> - <Grid item xs={12}> - {mixNodeRow && description?.data && ( - <MixNodeDetailSection mixNodeRow={mixNodeRow} mixnodeDescription={description.data} /> - )} - {mixNodeRow?.blacklisted && ( - <Typography textAlign={isMobile ? 'left' : 'right'} fontSize="smaller" sx={{ color: 'error.main' }}> - This node is having a poor performance - </Typography> - )} - </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 - columnsData={EconomicsInfoColumns} - tableName="Delegators info table" - rows={[EconomicsInfoRows()]} - /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <ContentCard title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`}> - <BondBreakdownTable /> - </ContentCard> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - <ContentCard title="Mixnode Stats"> - {stats && ( - <> - {stats.error && <ComponentError text="There was a problem retrieving this nodes stats." />} - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since startup" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_startup || 0, - stats?.data?.packets_sent_since_startup || 0, - stats?.data?.packets_explicitly_dropped_since_startup || 0, - ]} - /> - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since last update" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_last_update || 0, - stats?.data?.packets_sent_since_last_update || 0, - stats?.data?.packets_explicitly_dropped_since_last_update || 0, - ]} - marginBottom - /> - </> - )} - {!stats && <Typography>No stats information</Typography>} - </ContentCard> - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Mixnode Status"> - {status.error && <ComponentError text="There was a problem retrieving port information" />} - <TwoColSmallTable - loading={status.isLoading} - error={status?.error?.message} - keys={['Mix port', 'Verloc port', 'HTTP port']} - values={[1789, 1790, 8000].map((each) => each)} - icons={(status?.data?.ports && Object.values(status.data.ports)) || [false, false, false]} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {mixNode && ( - <ContentCard title="Location"> - {mixNode?.error && <ComponentError text="There was a problem retrieving this mixnode location" />} - {mixNode?.data?.location?.latitude && mixNode?.data?.location?.longitude && ( - <WorldMap - loading={mixNode.isLoading} - userLocation={[mixNode.data.location.longitude, mixNode.data.location.latitude]} - /> - )} - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ); -}; - -/** - * Guard component to handle loading and not found states - */ -const PageMixnodeDetailGuard: FCWithChildren = () => { - const { mixNode } = useMixnodeContext(); - const { id } = useParams<{ id: string | undefined }>(); - - if (mixNode?.isLoading) { - return <CircularProgress />; - } - - if (mixNode?.error) { - // eslint-disable-next-line no-console - console.error(mixNode?.error); - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ); - } - - // loaded, but not found - if (mixNode && !mixNode.isLoading && !mixNode.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Mixnode not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ); - } - - return <PageMixnodeDetailWithState />; -}; - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -export const PageMixnodeDetail: FCWithChildren = () => { - const { id } = useParams<{ id: string | undefined }>(); - - if (!id) { - return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>; - } - - return ( - <MixnodeContextProvider mixId={id}> - <PageMixnodeDetailGuard /> - </MixnodeContextProvider> - ); -}; diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx deleted file mode 100644 index 79a88f7053..0000000000 --- a/explorer/src/pages/Mixnodes/index.tsx +++ /dev/null @@ -1,427 +0,0 @@ -import * as React from 'react'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { Stack, Card, Grid, Box, Button } from '@mui/material'; -import { useParams, useNavigate, Link } from 'react-router-dom'; -import { SelectChangeEvent } from '@mui/material/Select'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { useMainContext } from '@src/context/main'; -import { - DelegateIconButton, - DelegationModal, - DelegationModalProps, - DelegateModal, - CustomColumnHeading, - StyledLink, - Title, - UniversalDataGrid, - TableToolbar, - Tooltip, - MixNodeStatusDropdown, - mixnodeToGridRow, -} from '@src/components'; -import { MixNodeResponse, MixnodeStatusWithAll, toMixnodeStatus } from '@src/typeDefs/explorer-api'; -import { NYM_BIG_DIPPER } from '@src/api/constants'; -import { currencyToString } from '@src/utils/currency'; -import { splice } from '@src/utils'; -import { useGetMixNodeStatusColor, useIsMobile } from '@src/hooks'; -import { useWalletContext } from '@src/context/wallet'; -import { DelegationsProvider } from '@src/context/delegations'; - -export const PageMixnodes: FCWithChildren = () => { - const { mixnodes, fetchMixnodes } = useMainContext(); - const [filteredMixnodes, setFilteredMixnodes] = React.useState<MixNodeResponse>([]); - const [pageSize, setPageSize] = React.useState<string>('10'); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - const [itemSelectedForDelegation, setItemSelectedForDelegation] = React.useState<{ - mixId: number; - identityKey: string; - }>(); - const [confirmationModalProps, setConfirmationModalProps] = React.useState<DelegationModalProps | undefined>(); - const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); - - const navigate = useNavigate(); - const { isWalletConnected } = useWalletContext(); - const isMobile = useIsMobile(); - - const handleNewDelegation = (delegationModalProps: DelegationModalProps) => { - setItemSelectedForDelegation(undefined); - setConfirmationModalProps(delegationModalProps); - }; - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - React.useEffect(() => { - if (searchTerm === '' && mixnodes?.data) { - setFilteredMixnodes(mixnodes?.data); - } else { - const filtered = mixnodes?.data?.filter((m) => { - if ( - m.location?.country_name.toLowerCase().includes(searchTerm) || - m.mix_node.identity_key.toLocaleLowerCase().includes(searchTerm) || - m.owner.toLowerCase().includes(searchTerm) - ) { - return m; - } - return null; - }); - if (filtered) { - setFilteredMixnodes(filtered); - } - } - }, [searchTerm, mixnodes?.data, mixnodes?.isLoading]); - - React.useEffect(() => { - // when the status changes, get the mixnodes - fetchMixnodes(toMixnodeStatus(status)); - }, [status]); - - const handleMixnodeStatusChanged = (newStatus?: MixnodeStatusWithAll) => { - navigate( - newStatus && newStatus !== MixnodeStatusWithAll.all - ? `/network-components/mixnodes/${newStatus}` - : '/network-components/mixnodes', - ); - }; - - const handleOnDelegate = ({ identityKey, mixId }: { identityKey: string; mixId: number }) => { - if (!isWalletConnected) { - setConfirmationModalProps({ - status: 'info', - message: 'Please connect your wallet to delegate', - }); - } else { - setItemSelectedForDelegation({ identityKey, mixId }); - } - }; - - const columns: GridColDef[] = [ - { - field: 'delegate', - disableColumnMenu: true, - disableReorder: true, - sortable: false, - width: isMobile ? 25 : 100, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderHeader: () => null, - renderCell: (params: GridRenderCellParams) => ( - <DelegateIconButton - size="small" - onDelegate={() => handleOnDelegate({ identityKey: params.row.identity_key, mixId: params.row.mix_id })} - /> - ), - }, - { - field: 'identity_key', - width: 325, - headerName: 'Identity Key', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, - renderCell: (params: GridRenderCellParams) => ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={params.value} - tooltip={`Copy identity key ${params.value} to clipboard`} - /> - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - dataTestId="identity-link" - > - {splice(7, 29, params.value)} - </StyledLink> - </Stack> - ), - }, - { - field: 'mix_id', - width: 85, - align: 'center', - hide: true, - headerName: 'Mix ID', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.value}`} - color={useGetMixNodeStatusColor(params.row.status)} - data-testid="mix-id" - > - {params.value} - </StyledLink> - ), - }, - - { - field: 'bond', - width: 150, - align: 'left', - type: 'number', - disableColumnMenu: true, - headerName: 'Stake', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderHeader: () => <CustomColumnHeading headingTitle="Stake" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - > - {currencyToString({ amount: params.value })} - </StyledLink> - ), - }, - { - field: 'stake_saturation', - width: 185, - align: 'center', - headerName: 'Stake Saturation', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Stake Saturation" - 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 940k NYMs, 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." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value} %`}</StyledLink> - ), - }, - { - field: 'pledge_amount', - width: 150, - align: 'left', - type: 'number', - headerName: 'Bond', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - > - {currencyToString({ amount: params.value })} - </StyledLink> - ), - }, - { - field: 'profit_percentage', - width: 145, - align: 'center', - headerName: 'Profit Margin', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Profit Margin" - tooltipInfo="Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value}%`}</StyledLink> - ), - }, - { - field: 'operating_cost', - width: 170, - align: 'center', - headerName: 'Operating Cost', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Operating Cost" - 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." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value} NYM`}</StyledLink> - ), - }, - { - field: 'node_performance', - width: 165, - align: 'center', - headerName: 'Routing Score', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Routing Score" - 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." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value}%`}</StyledLink> - ), - }, - { - field: 'owner', - width: 120, - headerName: 'Owner', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`${NYM_BIG_DIPPER}/account/${params.value}`} - color={useGetMixNodeStatusColor(params.row.status)} - target="_blank" - data-testid="big-dipper-link" - > - {splice(7, 29, params.value)} - </StyledLink> - ), - }, - { - field: 'location', - width: 150, - headerName: 'Location', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - renderCell: (params: GridRenderCellParams) => ( - <Tooltip text={params.value} id="mixnode-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - cursor: 'pointer', - color: useGetMixNodeStatusColor(params.row.status), - }} - onClick={() => handleSearch(params.value)} - > - {params.value} - </Box> - </Tooltip> - ), - }, - { - field: 'host', - width: 130, - headerName: 'Host', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Host" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - color={useGetMixNodeStatusColor(params.row.status)} - to={`/network-components/mixnode/${params.row.mix_id}`} - > - {params.value} - </StyledLink> - ), - }, - ]; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - return ( - <DelegationsProvider> - <Box mb={2}> - <Title text="Mixnodes" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <MixNodeStatusDropdown sx={{ mr: 2 }} status={status} onSelectionChanged={handleMixnodeStatusChanged} /> - } - childrenAfter={ - isWalletConnected && ( - <Button fullWidth size="large" variant="outlined" color="primary" component={Link} to="/delegations"> - Delegations - </Button> - ) - } - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - withFilters - /> - <UniversalDataGrid - pagination - loading={Boolean(mixnodes?.isLoading)} - rows={mixnodeToGridRow(filteredMixnodes)} - columns={columns} - pageSize={pageSize} - /> - </Card> - </Grid> - </Grid> - - {itemSelectedForDelegation && ( - <DelegateModal - onClose={() => { - setItemSelectedForDelegation(undefined); - }} - header="Delegate" - buttonText="Delegate stake" - denom="nym" - onOk={(delegationModalProps: DelegationModalProps) => handleNewDelegation(delegationModalProps)} - identityKey={itemSelectedForDelegation.identityKey} - mixId={itemSelectedForDelegation.mixId} - /> - )} - - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - setConfirmationModalProps(undefined); - if (confirmationModalProps.status === 'success') { - navigate('/delegations'); - } - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - </DelegationsProvider> - ); -}; diff --git a/explorer/src/pages/MixnodesMap/index.tsx b/explorer/src/pages/MixnodesMap/index.tsx deleted file mode 100644 index a7c0048a11..0000000000 --- a/explorer/src/pages/MixnodesMap/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from 'react'; -import { Alert, Box, CircularProgress, Grid, SelectChangeEvent, Typography } from '@mui/material'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { ContentCard } from '../../components/ContentCard'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; -import { TableToolbar } from '../../components/TableToolbar'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { WorldMap } from '../../components/WorldMap'; -import { useMainContext } from '../../context/main'; -import { CountryDataRowType, countryDataToGridRow } from '../../utils'; - -export const PageMixnodesMap: FCWithChildren = () => { - const { countryData } = useMainContext(); - const [pageSize, setPageSize] = React.useState<string>('10'); - const [formattedCountries, setFormattedCountries] = React.useState<CountryDataRowType[]>([]); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - const columns: GridColDef[] = [ - { - field: 'countryName', - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => <Typography data-testid="country-name">{params.value}</Typography>, - }, - { - field: 'nodes', - renderHeader: () => <CustomColumnHeading headingTitle="Number of Nodes" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Typography data-testid="number-of-nodes">{params.value}</Typography> - ), - }, - { - field: 'percentage', - renderHeader: () => <CustomColumnHeading headingTitle="Percentage %" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => <Typography data-testid="percentage">{params.value}</Typography>, - }, - ]; - - React.useEffect(() => { - if (countryData?.data && searchTerm === '') { - setFormattedCountries(countryDataToGridRow(Object.values(countryData.data))); - } else if (countryData?.data !== undefined && searchTerm !== '') { - const formatted = countryDataToGridRow(Object.values(countryData?.data)); - const filtered = formatted.filter( - (m) => m?.countryName?.toLowerCase().includes(searchTerm) || m?.ISO3?.toLowerCase().includes(searchTerm), - ); - if (filtered) { - setFormattedCountries(filtered); - } - } - }, [searchTerm, countryData?.data]); - - if (countryData?.isLoading) { - return <CircularProgress />; - } - - if (countryData?.data && !countryData.isLoading) { - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item data-testid="mixnodes-globe"> - <Title text="Mixnodes Around the Globe" /> - </Grid> - <Grid item> - <Grid container spacing={2}> - <Grid item xs={12}> - <ContentCard title="Distribution of nodes"> - <WorldMap loading={false} countryData={countryData} /> - <Box sx={{ marginTop: 2 }} /> - <TableToolbar - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - /> - <UniversalDataGrid - pagination - loading={countryData?.isLoading} - columns={columns} - rows={formattedCountries} - pageSize={pageSize} - /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ); - } - - if (countryData?.error) { - return <Alert severity="error">{countryData.error.message}</Alert>; - } - - return null; -}; diff --git a/explorer/src/pages/Overview/index.tsx b/explorer/src/pages/Overview/index.tsx deleted file mode 100644 index 9bc2ebade7..0000000000 --- a/explorer/src/pages/Overview/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as React from 'react'; -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'; -import { BIG_DIPPER } from '../../api/constants'; -import { ValidatorsSVG } from '../../icons/ValidatorsSVG'; -import { GatewaysSVG } from '../../icons/GatewaysSVG'; -import { MixnodesSVG } from '../../icons/MixnodesSVG'; -import { Title } from '../../components/Title'; -import { ContentCard } from '../../components/ContentCard'; -import { StatsCard } from '../../components/StatsCard'; -import { Icons } from '../../components/Icons'; - -export const PageOverview: FCWithChildren = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const { summaryOverview, gateways, validators, block, countryData, serviceProviders } = useMainContext(); - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item paddingBottom={3}> - <Title text="Overview" /> - </Grid> - <Grid item> - <Grid container spacing={3}> - {summaryOverview && ( - <> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/mixnodes')} - title="Mixnodes" - icon={<MixnodesSVG />} - count={summaryOverview.data?.mixnodes.count || ''} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/mixnodes/active')} - title="Active nodes" - icon={<Icons.Mixnodes.Status.Active />} - color={theme.palette.nym.networkExplorer.mixnodes.status.active} - count={summaryOverview.data?.mixnodes.activeset.active} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/mixnodes/standby')} - title="Standby nodes" - color={theme.palette.nym.networkExplorer.mixnodes.status.standby} - icon={<Icons.Mixnodes.Status.Standby />} - count={summaryOverview.data?.mixnodes.activeset.standby} - errorMsg={summaryOverview?.error} - /> - </Grid> - </> - )} - {gateways && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/gateways')} - title="Gateways" - count={gateways?.data?.length || ''} - errorMsg={gateways?.error} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {serviceProviders && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/service-providers')} - title="Service providers" - icon={<PeopleAlt />} - count={serviceProviders.data?.length} - errorMsg={summaryOverview?.error} - /> - </Grid> - )} - {validators && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => window.open(`${BIG_DIPPER}/validators`)} - title="Validators" - count={validators?.data?.count || ''} - errorMsg={validators?.error} - icon={<ValidatorsSVG />} - /> - </Grid> - )} - {block?.data && ( - <Grid item xs={12}> - <Link - href={`${BIG_DIPPER}/blocks`} - target="_blank" - rel="noreferrer" - underline="none" - color="inherit" - marginY={2} - paddingX={3} - paddingY={0.25} - fontSize={14} - fontWeight={600} - display="flex" - alignItems="center" - > - <Typography fontWeight="inherit" fontSize="inherit"> - Current block height is {formatNumber(block.data)} - </Typography> - <OpenInNewIcon fontWeight="inherit" fontSize="inherit" sx={{ ml: 0.5 }} /> - </Link> - </Grid> - )} - <Grid item xs={12}> - <ContentCard title="Distribution of nodes around the world"> - <WorldMap loading={false} countryData={countryData} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ); -}; diff --git a/explorer/src/pages/ServiceProviders/index.tsx b/explorer/src/pages/ServiceProviders/index.tsx deleted file mode 100644 index 618aa72924..0000000000 --- a/explorer/src/pages/ServiceProviders/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { Box, Button, Card, FormControl, Grid, ListItem, Menu, SelectChangeEvent, Typography } from '@mui/material'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { TableToolbar } from '../../components/TableToolbar'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { useMainContext } from '../../context/main'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; - -const columns: GridColDef[] = [ - { - headerName: 'Client ID', - field: 'address', - disableColumnMenu: true, - flex: 3, - }, - { - headerName: 'Type', - field: 'service_type', - disableColumnMenu: true, - flex: 1, - }, - { - headerName: 'Routing score', - field: 'routing_score', - disableColumnMenu: true, - flex: 2, - sortingOrder: ['asc', 'desc'], - sortComparator: (a?: string, b?: string) => { - if (!a) return -1; // Place undefined values at the end - if (!b) return 1; // Place undefined values at the end - - const aToNum = parseInt(a, 10); - const bToNum = parseInt(b, 10); - - if (aToNum > bToNum) return 1; - - return -1; // Sort numbers in ascending order - }, - renderCell: (params: GridRenderCellParams) => (!params.value ? '-' : params.value), - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Routing score" - tooltipInfo="Routing score is only displayed for the service providers that had a successful ping within the last two hours" - /> - ), - }, -]; - -const SupportedApps = () => { - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); - const open = Boolean(anchorEl); - const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { - setAnchorEl(null); - }; - const anchorRef = React.useRef<HTMLButtonElement>(null); - - return ( - <FormControl size="small"> - <Button - ref={anchorRef} - onClick={handleClick} - size="large" - variant="outlined" - color="inherit" - sx={{ mr: 2, textTransform: 'capitalize' }} - > - Supported Apps - </Button> - <Menu anchorEl={anchorEl} open={open} onClose={handleClose}> - <ListItem>Keybase</ListItem> - <ListItem>Telegram</ListItem> - <ListItem>Electrum</ListItem> - <ListItem>Blockstream Green</ListItem> - </Menu> - </FormControl> - ); -}; - -export const ServiceProviders = () => { - const { serviceProviders } = useMainContext(); - const [pageSize, setPageSize] = React.useState('10'); - - const handleOnPageSizeChange = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - return ( - <> - <Box mb={2}> - <Title text="Service Providers" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - }} - > - {serviceProviders?.data ? ( - <> - <TableToolbar - onChangePageSize={handleOnPageSizeChange} - pageSize={pageSize} - childrenBefore={<SupportedApps />} - /> - <UniversalDataGrid - pagination - rows={serviceProviders.data} - columns={columns} - pageSize={pageSize} - initialState={{ - sorting: { - sortModel: [ - { - field: 'routing_score', - sort: 'desc', - }, - ], - }, - }} - /> - </> - ) : ( - <Typography>No service providers to display</Typography> - )} - </Card> - </Grid> - </Grid> - </> - ); -}; diff --git a/explorer/src/routes/index.tsx b/explorer/src/routes/index.tsx deleted file mode 100644 index 67ef8b4621..0000000000 --- a/explorer/src/routes/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { Routes as ReactRouterRoutes, Route } from 'react-router-dom'; -import { Delegations } from '@src/pages/Delegations'; -import { PageOverview } from '../pages/Overview'; -import { PageMixnodesMap } from '../pages/MixnodesMap'; -import { Page404 } from '../pages/404'; -import { NetworkComponentsRoutes } from './network-components'; - -export const Routes: FCWithChildren = () => ( - <ReactRouterRoutes> - <Route path="/" element={<PageOverview />} /> - <Route path="/network-components/*" element={<NetworkComponentsRoutes />} /> - <Route path="/nodemap" element={<PageMixnodesMap />} /> - <Route path="/delegations" element={<Delegations />} /> - <Route path="*" element={<Page404 />} /> - </ReactRouterRoutes> -); diff --git a/explorer/src/routes/network-components.tsx b/explorer/src/routes/network-components.tsx deleted file mode 100644 index 266a75aa2f..0000000000 --- a/explorer/src/routes/network-components.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom'; -import { BIG_DIPPER } from '../api/constants'; -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(); - window.open(`${BIG_DIPPER}/validators`); - navigate(-1); - return null; -}; - -export const NetworkComponentsRoutes: FCWithChildren = () => ( - <ReactRouterRoutes> - <Route path="mixnodes/:status" element={<PageMixnodes />} /> - <Route path="mixnodes" element={<PageMixnodes />} /> - <Route path="mixnode/:id" element={<PageMixnodeDetail />} /> - <Route path="gateways" element={<PageGateways />} /> - <Route path="gateway/:id" element={<PageGatewayDetail />} /> - <Route path="validators" element={<ValidatorRoute />} /> - <Route path="service-providers" element={<ServiceProviders />} /> - </ReactRouterRoutes> -); diff --git a/explorer/src/stories/Introduction.stories.mdx b/explorer/src/stories/Introduction.stories.mdx deleted file mode 100644 index dafab13de0..0000000000 --- a/explorer/src/stories/Introduction.stories.mdx +++ /dev/null @@ -1,7 +0,0 @@ -import { Meta } from '@storybook/addon-docs'; - -<Meta title="Introduction" /> - -# Nym Network Explorer Storybook - -This is the Storybook for the Nym Network Explorer. diff --git a/explorer/src/styles.css b/explorer/src/styles.css deleted file mode 100644 index 818211ccea..0000000000 --- a/explorer/src/styles.css +++ /dev/null @@ -1,30 +0,0 @@ -/* last resort for styles that cannot be handled by Material UI and Emotion JS */ - -/* TODO - this override should take place in either -the theme declaration in index.tsx or the style prop -in <DataGrid /> */ - -.MuiDataGrid-columnSeparator { - 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; -} - -/* 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; -} - -@media screen and (max-width: 900px) { - .MuiDrawer-paperAnchorLeft { - min-width: 100vw; - margin-top: 58px; - } -} diff --git a/explorer/src/tests/Nav.test.tsx b/explorer/src/tests/Nav.test.tsx deleted file mode 100644 index c3e5e949d0..0000000000 --- a/explorer/src/tests/Nav.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { render } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import { Nav } from '../components/Nav'; - -describe('Nav', () => { - beforeEach(() => { - render(<Nav />); - }); - it('should render without exploding', () => { - const { container } = render(<Nav />); - expect(container.firstChild).toBeInTheDocument(); - }); -}); diff --git a/explorer/src/tests/WorldMap.test.tsx b/explorer/src/tests/WorldMap.test.tsx deleted file mode 100644 index 1a5b37d25d..0000000000 --- a/explorer/src/tests/WorldMap.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import { WorldMap } from '../components/WorldMap'; - -describe('WorldMap', () => { - beforeEach(() => { - render(<WorldMap loading={false} />); - }); - it('should render without exploding', () => { - const { container } = render(<WorldMap loading={false} />); - expect(container.firstChild).toBeInTheDocument(); - }); - it('should render the expected container/child element', () => { - expect(screen.getByTestId('worldMap__container')).toBeInTheDocument(); - }); - it('should render the title', () => { - expect(screen.getByText('mix-nodes around the globe')).toBeTruthy(); - }); - it('should render the map/SVG', () => { - expect(screen.getByTestId('svg')).toBeInTheDocument(); - }); - it('should render map at correct size/dims', () => { - const expectedWidth = '1000'; - const expectedHeight = '800'; - expect(screen.getByTestId('svg')).toHaveAttribute('width', expectedWidth); - expect(screen.getByTestId('svg')).toHaveAttribute('height', expectedHeight); - }); -}); diff --git a/explorer/src/tests/todo.test.ts b/explorer/src/tests/todo.test.ts deleted file mode 100644 index 0580a22ccc..0000000000 --- a/explorer/src/tests/todo.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -describe('testing jest config', () => { - test('jest works with typescript', () => { - const foo = () => 42; - - expect(foo()).toBe(42); - }); -}); diff --git a/explorer/src/theme/index.tsx b/explorer/src/theme/index.tsx deleted file mode 100644 index 3542bd9c9a..0000000000 --- a/explorer/src/theme/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from 'react'; -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme'; -import { useMainContext } from '../context/main'; - -export const NetworkExplorerThemeProvider: FCWithChildren = ({ children }) => { - const { mode } = useMainContext(); - return <NymNetworkExplorerThemeProvider mode={mode}>{children}</NymNetworkExplorerThemeProvider>; -}; diff --git a/explorer/src/theme/mui-theme.d.ts b/explorer/src/theme/mui-theme.d.ts deleted file mode 100644 index fc1f856f91..0000000000 --- a/explorer/src/theme/mui-theme.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */ -import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles'; -import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme'; - -/** - * If you are unfamiliar with Material UI theming, please read the following first: - * - https://mui.com/customization/theming/ - * - https://mui.com/customization/palette/ - * - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette - * - * This file adds typings to the theme using Typescript's module augmentation. - * - * Read the following if you are unfamiliar with module augmentation and declaration merging. Then - * look at the recommendations from Material UI docs for implementation: - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces - * - https://mui.com/customization/palette/#adding-new-colors - * - * - * IMPORTANT: - * - * The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than - * `type Foo = { ... }` - this is necessary to merge the definitions. - */ - -declare module '@mui/material/styles' { - /** - * This augments the definitions of the MUI Theme with the Nym theme, as well as - * a partial `ThemeOptions` type used by `createTheme` - * - * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below - */ - interface Theme extends NymTheme {} - interface ThemeOptions extends Partial<NymTheme> {} - interface Palette extends NymPaletteWithExtensions {} - interface PaletteOptions extends NymPaletteWithExtensionsOptions {} -} diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts deleted file mode 100644 index 9319c33952..0000000000 --- a/explorer/src/typeDefs/explorer-api.ts +++ /dev/null @@ -1,277 +0,0 @@ -/* eslint-disable camelcase */ - -export interface ClientConfig { - url: string; - version: string; -} - -export interface SummaryOverviewResponse { - mixnodes: { - count: number; - activeset: { - active: number; - standby: number; - inactive: number; - }; - }; - gateways: { - count: number; - }; - validators: { - count: number; - }; -} - -export interface MixNode { - host: string; - mix_port: number; - http_api_port: number; - verloc_port: number; - sphinx_key: string; - identity_key: string; - version: string; - location: string; -} - -export interface Gateway { - host: string; - mix_port: number; - clients_port: number; - location: string; - sphinx_key: string; - identity_key: string; - version: string; -} - -export interface Amount { - denom: string; - amount: number; -} - -export enum MixnodeStatus { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set -} - -export enum MixnodeStatusWithAll { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set - all = 'all', // any status -} - -export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | undefined => { - if (!status || status === MixnodeStatusWithAll.all) { - return undefined; - } - return status as unknown as MixnodeStatus; -}; - -export interface MixNodeResponseItem { - mix_id: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - layer: string; - status: MixnodeStatus; - location: { - country_name: string; - latitude?: number; - longitude?: number; - three_letter_iso_country_code: string; - two_letter_iso_country_code: string; - }; - mix_node: MixNode; - avg_uptime: number; - node_performance: NodePerformance; - stake_saturation: number; - uncapped_saturation: number; - operating_cost: Amount; - profit_margin_percent: string; - blacklisted: boolean; -} - -export type MixNodeResponse = MixNodeResponseItem[]; - -export interface MixNodeReportResponse { - identity: string; - owner: string; - most_recent_ipv4: boolean; - most_recent_ipv6: boolean; - last_hour_ipv4: number; - last_hour_ipv6: number; - last_day_ipv4: number; - last_day_ipv6: number; -} - -export interface StatsResponse { - update_time: Date; - previous_update_time: Date; - packets_received_since_startup: number; - packets_sent_since_startup: number; - packets_explicitly_dropped_since_startup: number; - packets_received_since_last_update: number; - packets_sent_since_last_update: number; - packets_explicitly_dropped_since_last_update: number; -} - -export interface NodePerformance { - most_recent: string; - last_hour: string; - last_24h: string; -} - -export type MixNodeHistoryResponse = StatsResponse; - -export interface GatewayBond { - block_height: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - gateway: Gateway; - node_performance: NodePerformance; - location?: Location; -} - -export interface GatewayBondAnnotated { - gateway_bond: GatewayBond; - node_performance: NodePerformance; -} - -export interface Location { - two_letter_iso_country_code: string; - three_letter_iso_country_code: string; - country_name: string; - latitude?: number; - longitude?: number; -} - -export interface LocatedGateway { - pledge_amount: Amount; - owner: string; - block_height: number; - gateway: Gateway; - proxy?: string; - location?: Location; -} - -export type GatewayResponse = GatewayBond[]; - -export interface GatewayReportResponse { - identity: string; - owner: string; - most_recent: number; - last_hour: number; - last_day: number; -} - -export type GatewayHistoryResponse = StatsResponse; - -export interface MixNodeDescriptionResponse { - name: string; - description: string; - link: string; - location: string; -} - -export type MixNodeStatsResponse = StatsResponse; - -export interface Validator { - address: string; - proposer_priority: string; - pub_key: { - type: string; - value: string; - }; -} -export interface ValidatorsResponse { - block_height: number; - count: string; - total: string; - validators: Validator[]; -} - -export type CountryData = { - ISO3: string; - nodes: number; -}; - -export type Delegation = { - owner: string; - amount: Amount; - block_height: number; -}; - -export type DelegationUniq = { - owner: string; - amount: Amount; -}; - -export type DelegationsResponse = Delegation[]; - -export type UniqDelegationsResponse = DelegationUniq[]; - -export interface CountryDataResponse { - [threeLetterCountryCode: string]: CountryData; -} - -export type BlockType = number; -export type BlockResponse = BlockType; - -export interface ApiState<RESPONSE> { - isLoading: boolean; - data?: RESPONSE; - error?: Error; -} - -export type StatusResponse = { - pending: boolean; - ports: { - 1789: boolean; - 1790: boolean; - 8000: boolean; - }; -}; - -export type UptimeTime = { - date: string; - uptime: number; -}; - -export type UptimeStoryResponse = { - history: UptimeTime[]; - identity: string; - owner: string; -}; - -export type MixNodeEconomicDynamicsStatsResponse = { - stake_saturation: number; - uncapped_saturation: number; - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - active_set_inclusion_probability: 'High' | 'Good' | 'Low'; - reserve_set_inclusion_probability: 'High' | 'Good' | 'Low'; - estimated_total_node_reward: number; - estimated_operator_reward: number; - estimated_delegators_reward: number; - current_interval_uptime: number; -}; - -export type Environment = 'mainnet' | 'sandbox' | 'qa'; - -export type ServiceProviderType = 'Network Requester'; - -export type DirectoryServiceProvider = { - id: string; - description: string; - address: string; - gateway: string; - routing_score: string | null; - service_type: ServiceProviderType; -}; - -export type DirectoryService = { - id: string; - description: string; - items: DirectoryServiceProvider[]; -}; diff --git a/explorer/src/typeDefs/filters.ts b/explorer/src/typeDefs/filters.ts deleted file mode 100644 index 53de60cb87..0000000000 --- a/explorer/src/typeDefs/filters.ts +++ /dev/null @@ -1,22 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { Mark } from '@mui/base'; - -export enum EnumFilterKey { - profitMargin = 'profitMargin', - stakeSaturation = 'stakeSaturation', - routingScore = 'routingScore', -} - -export type TFilterItem = { - label: string; - id: EnumFilterKey; - value: number[]; - isSmooth?: boolean; - marks: Mark[]; - min?: number; - max?: number; - scale?: (value: number) => number; - tooltipInfo?: string; -}; - -export type TFilters = { [key in EnumFilterKey]: TFilterItem }; diff --git a/explorer/src/typeDefs/network.ts b/explorer/src/typeDefs/network.ts deleted file mode 100644 index f8615cd992..0000000000 --- a/explorer/src/typeDefs/network.ts +++ /dev/null @@ -1 +0,0 @@ -export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; diff --git a/explorer/src/typeDefs/tables.ts b/explorer/src/typeDefs/tables.ts deleted file mode 100644 index 1ab2ffead4..0000000000 --- a/explorer/src/typeDefs/tables.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type TableHeading = { - id: string; - numeric: boolean; - disablePadding: boolean; - label: string; -}; - -export type TableHeadingsType = TableHeading[]; diff --git a/explorer/src/typings/FC.d.ts b/explorer/src/typings/FC.d.ts deleted file mode 100644 index 08ebdfa298..0000000000 --- a/explorer/src/typings/FC.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare type FCWithChildren<P = {}> = React.FC<React.PropsWithChildren<P>>; diff --git a/explorer/src/typings/jpeg.d.ts b/explorer/src/typings/jpeg.d.ts deleted file mode 100644 index af2ed72913..0000000000 --- a/explorer/src/typings/jpeg.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module '*.jpeg' { - const value: any; - export default value; -} - -declare module '*.jpg' { - const value: any; - export default value; -} diff --git a/explorer/src/typings/json.d.ts b/explorer/src/typings/json.d.ts deleted file mode 100644 index b72dd46ee5..0000000000 --- a/explorer/src/typings/json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.json' { - const content: any; - export default content; -} diff --git a/explorer/src/typings/png.d.ts b/explorer/src/typings/png.d.ts deleted file mode 100644 index dd84df40a4..0000000000 --- a/explorer/src/typings/png.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.png' { - const content: any; - export default content; -} diff --git a/explorer/src/typings/react-identicons.d.ts b/explorer/src/typings/react-identicons.d.ts deleted file mode 100644 index 6c460eff99..0000000000 --- a/explorer/src/typings/react-identicons.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare module 'react-identicons' { - import * as React from 'react'; - - interface IdenticonProps { - string: string; - size?: number; - padding?: number; - bg?: string; - fg?: string; - palette?: string[]; - count?: number; - // getColor: Function; - } - - declare function Identicon(props: IdenticonProps): React.ReactElement<IdenticonProps>; - - export default Identicon; -} diff --git a/explorer/src/typings/react-tooltip.d.ts b/explorer/src/typings/react-tooltip.d.ts deleted file mode 100644 index 98cb6d592d..0000000000 --- a/explorer/src/typings/react-tooltip.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'react-tooltip'; diff --git a/explorer/src/typings/svg.d.ts b/explorer/src/typings/svg.d.ts deleted file mode 100644 index 091d25e210..0000000000 --- a/explorer/src/typings/svg.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.svg' { - const content: any; - export default content; -} diff --git a/explorer/src/utils/currency.ts b/explorer/src/utils/currency.ts deleted file mode 100644 index ad6ed1f165..0000000000 --- a/explorer/src/utils/currency.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { printableCoin } from '@nymproject/nym-validator-client'; -import Big from 'big.js'; -import { DecCoin, isValidRawCoin } from '@nymproject/types'; - -const DENOM = process.env.CURRENCY_DENOM || 'unym'; -const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx'; - -export const toDisplay = (val: string | number | Big, dp = 4) => { - let displayValue; - try { - displayValue = Big(val).toFixed(dp); - } catch (e: any) { - console.warn(`${displayValue} not a valid decimal number: ${e}`); - } - return displayValue; -}; - -export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string; dp?: number; denom?: string }) => { - if (!dp) { - printableCoin({ - amount, - denom, - }); - } - - const [printableAmount, printableDenom] = printableCoin({ - amount, - denom, - }).split(/\s+/); - - return `${toDisplay(printableAmount, dp)} ${printableDenom}`; -}; - -export const stakingCurrencyToString = (amount: string, denom: string = DENOM_STAKING) => - printableCoin({ - amount, - denom, - }); - -/** - * Converts a decimal number to a pretty representation - * with fixed decimal places. - * - * @param val - a decimal number of string form - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns A prettyfied decimal number - */ - -/** - * Converts a decimal number of μNYM (micro NYM) to NYM. - * - * @param unym - a decimal number of μNYM - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns The corresponding decimal number in NYM - */ -export const unymToNym = (unym: string | number | Big, dp = 4) => { - let nym; - try { - nym = Big(unym).div(1_000_000).toFixed(dp); - } catch (e: any) { - console.warn(`${unym} not a valid decimal number: ${e}`); - } - return nym; -}; - -export const validateAmount = async ( - majorAmountAsString: DecCoin['amount'], - minimumAmountAsString: DecCoin['amount'], -): Promise<boolean> => { - // tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc - if (!Number(majorAmountAsString)) { - return false; - } - - if (!isValidRawCoin(majorAmountAsString)) { - return false; - } - - const majorValueFloat = parseInt(majorAmountAsString, Number(10)); - - return majorValueFloat >= parseInt(minimumAmountAsString, Number(10)); -}; - -/** - * Takes a DecCoin and prettify its amount to a representation - * with fixed decimal places. - * - * @param coin - a DecCoin - * @param dp - number of decimal places to apply to amount (4 by default ie. 0.0000) - * @returns A DecCoin with prettified amount - */ -export const decCoinToDisplay = (coin: DecCoin, dp = 4) => { - const displayCoin = { ...coin }; - try { - displayCoin.amount = Big(coin.amount).toFixed(dp); - } catch (e: any) { - console.warn(`${coin.amount} not a valid decimal number: ${e}`); - } - return displayCoin; -}; diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts deleted file mode 100644 index 91b082c5ca..0000000000 --- a/explorer/src/utils/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* eslint-disable camelcase */ -import { MutableRefObject } from 'react'; -import { Theme } from '@mui/material/styles'; -import { registerLocale, getName } from 'i18n-iso-countries'; -import Big from 'big.js'; -import { CountryData } from '../typeDefs/explorer-api'; -import { EconomicsRowsType } from '../components/MixNodes/Economics/types'; -import { Network } from '../typeDefs/network'; - -registerLocale(require('i18n-iso-countries/langs/en.json')); - -export function formatNumber(num: number): string { - return new Intl.NumberFormat().format(num); -} - -export function scrollToRef(ref: MutableRefObject<HTMLDivElement | undefined>): void { - if (ref?.current) ref.current.scrollIntoView(); -} - -export type CountryDataRowType = { - id: number; - ISO3: string; - nodes: number; - countryName: string; - percentage: string; -}; - -export function countryDataToGridRow(countriesData: CountryData[]): CountryDataRowType[] { - const totalNodes = countriesData.reduce((acc, obj) => acc + obj.nodes, 0); - const formatted = countriesData.map((each: CountryData, index: number) => { - const updatedCountryRecord: CountryDataRowType = { - ...each, - id: index, - countryName: getName(each.ISO3, 'en', { select: 'alias' }), - percentage: ((each.nodes * 100) / totalNodes).toFixed(1), - }; - return updatedCountryRecord; - }); - - const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1)); - return sorted; -} - -export const splice = (start: number, deleteCount: number, address?: string): string => { - if (address) { - const array = address.split(''); - array.splice(start, deleteCount, '...'); - return array.join(''); - } - return ''; -}; - -export const trimAddress = (address = '', trimBy = 6) => `${address.slice(0, trimBy)}...${address.slice(-trimBy)}`; - -/** - * Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100). - * - * @param value - the percentage to convert - * @returns A stringified integer - */ -export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); -export const toPercentInteger = (value: string) => Math.round(Number(value) * 100); - -export 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; -}; - -export const isGreaterThan = (a: number, b: number) => a > b; - -export const isLessThan = (a: number, b: number) => a < b; - -/** - * - * Checks if the user's balance is enough to pay the fee - * @param balance - The user's current balance - * @param fee - The fee for the tx - * @param tx - The amount of the tx - * @returns boolean - * - */ - -export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => { - try { - return Big(balance).gte(Big(fee).plus(Big(tx))); - } catch (e) { - console.log(e); - return false; - } -}; - -export const urls = (networkName?: Network) => - networkName === 'MAINNET' - ? { - mixnetExplorer: 'https://mixnet.explorers.guru/', - blockExplorer: 'https://blocks.nymtech.net', - networkExplorer: 'https://explorer.nymtech.net', - } - : { - blockExplorer: `https://${networkName}-blocks.nymtech.net`, - networkExplorer: `https://${networkName}-explorer.nymtech.net`, - }; diff --git a/explorer/src/x-data-grid/LICENSE b/explorer/src/x-data-grid/LICENSE deleted file mode 100644 index 126bc57eaa..0000000000 --- a/explorer/src/x-data-grid/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Material-UI SAS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/explorer/src/x-data-grid/README.md b/explorer/src/x-data-grid/README.md deleted file mode 100644 index c163d7ceeb..0000000000 --- a/explorer/src/x-data-grid/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# @mui/x-data-grid - -This package is the community edition of the data grid component. -It's part of Material-UI X, an open core extension of Material-UI, with advanced components. - -## Installation - -Install the package in your project directory with: - -```sh -// with npm -npm install @mui/x-data-grid - -// with yarn -yarn add @mui/x-data-grid -``` - -This component has two peer dependencies that you will need to install as well. - -```json -"peerDependencies": { - "@material-ui/core": "^4.12.0 || ^5.0.0-beta.0", - "react": "^17.0.0" -}, -``` - -## Documentation - -[The documentation](https://material-ui.com/components/data-grid/) diff --git a/explorer/src/x-data-grid/package.json b/explorer/src/x-data-grid/package.json deleted file mode 100644 index 46225fa760..0000000000 --- a/explorer/src/x-data-grid/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "_from": "@mui/x-data-grid", - "_id": "@mui/x-data-grid@4.0.0", - "_inBundle": false, - "_integrity": "sha512-BYn7uLx5tJbMarcWltjjVArWNBdC22/2xOpq3Azhltbb3TRx3h2RLUeKwZI685xmGHmzvtu6QqaoYqQgFe/h+g==", - "_location": "/@mui/x-data-grid", - "_phantomChildren": {}, - "_requested": { - "type": "tag", - "registry": true, - "raw": "@mui/x-data-grid", - "name": "@mui/x-data-grid", - "escapedName": "@mui%2fx-data-grid", - "scope": "@mui", - "rawSpec": "", - "saveSpec": null, - "fetchSpec": "latest" - }, - "_requiredBy": [ - "#USER", - "/" - ], - "_resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-4.0.0.tgz", - "_shasum": "e9e9c33a8b86e85872c48f30f4a8de72ee819153", - "_spec": "@mui/x-data-grid", - "_where": "/Users/adrianthompson/Documents/nym/explorer", - "author": { - "name": "MUI Team" - }, - "bugs": { - "url": "https://github.com/mui-org/material-ui-x/issues" - }, - "bundleDependencies": false, - "dependencies": { - "@material-ui/utils": "^5.0.0-beta.4", - "clsx": "^1.1.1", - "prop-types": "^15.7.2", - "reselect": "^4.0.0" - }, - "deprecated": false, - "description": "The community edition of the data grid component (Material-UI X).", - "engines": { - "node": ">=12.0.0" - }, - "files": [ - "dist/*" - ], - "gitHead": "940d6238fe499f027ee27e818d714f79ca40e9a5", - "homepage": "https://material-ui.com/components/data-grid/", - "keywords": [ - "react", - "react-component", - "material-ui", - "mui", - "react-table", - "table", - "datatable", - "data-table", - "datagrid", - "data-grid" - ], - "license": "MIT", - "main": "dist/index-cjs.js", - "module": "dist/index-esm.js", - "name": "@mui/x-data-grid", - "peerDependencies": { - "@material-ui/core": "^4.12.0 || ^5.0.0-beta.0", - "@mui/styles": "^4.11.4 || ^5.0.0-beta.0", - "react": "^17.0.0" - }, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/mui-org/material-ui-x.git", - "directory": "packages/grid/data-grid" - }, - "scripts": { - "build": "cd ../ && rollup --config rollup.data-grid.config.js", - "typescript": "tsc -p tsconfig.json" - }, - "setupFiles": [ - "<rootDir>/src/setupTests.js" - ], - "sideEffects": false, - "types": "dist/data-grid.d.ts", - "version": "4.0.0" -} \ No newline at end of file diff --git a/explorer/tsconfig.json b/explorer/tsconfig.json deleted file mode 100644 index 093340052d..0000000000 --- a/explorer/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../ts-packages/tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx", - "outDir": "./dist", - "module": "CommonJS", - "baseUrl": ".", - "paths": { - "@src/*": ["./src/*"], - "@assets/*": ["../assets/*"] - } - }, - "include": ["./src/**/*.ts", "./src/**/*.tsx"], - "exclude": ["node_modules", "build", "dist"] -} diff --git a/explorer/webpack.common.js b/explorer/webpack.common.js deleted file mode 100644 index 7bb9cb9032..0000000000 --- a/explorer/webpack.common.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path'); -const { mergeWithRules } = require('webpack-merge'); -const { webpackCommon } = require('@nymproject/webpack'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(webpackCommon(__dirname), { - entry: path.resolve(__dirname, 'src/index.tsx'), - output: { - path: path.resolve(__dirname, 'dist'), - publicPath: '/', - }, - resolve: { - fallback: { - fs: false, - tls: false, - path: false, - http: false, - https: false, - stream: false, - crypto: false, - net: false, - zlib: false, - buffer: require.resolve('buffer'), - }, - }, -}); diff --git a/explorer/webpack.config.js b/explorer/webpack.config.js deleted file mode 100644 index 550951cb52..0000000000 --- a/explorer/webpack.config.js +++ /dev/null @@ -1,76 +0,0 @@ -const { mergeWithRules } = require('webpack-merge'); -const webpack = require('webpack'); -const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const ReactRefreshTypeScript = require('react-refresh-typescript'); -const commonConfig = require('./webpack.common'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(commonConfig, { - mode: 'development', - devtool: 'inline-source-map', - module: { - rules: [ - { - test: /\.m?js/, - resolve: { - fullySpecified: false, - }, - }, - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, - options: { - getCustomTransformers: () => ({ - before: [ReactRefreshTypeScript()], - }), - // `ts-loader` does not work with HMR unless `transpileOnly` is used. - // If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative. - transpileOnly: true, - }, - }, - ], - }, - plugins: [ - new ReactRefreshWebpackPlugin(), - - // this can be included automatically by the dev server, however build mode fails if missing - new webpack.HotModuleReplacementPlugin(), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - }), - ], - - target: 'web', - - devServer: { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }, - historyApiFallback: true, - }, - - // recommended for faster rebuild - optimization: { - runtimeChunk: true, - removeAvailableModules: false, - removeEmptyChunks: false, - splitChunks: false, - }, - - cache: { - type: 'filesystem', - buildDependencies: { - // restart on config change - config: ['./webpack.config.js'], - }, - }, -}); diff --git a/explorer/webpack.prod.js b/explorer/webpack.prod.js deleted file mode 100644 index 259c32a4af..0000000000 --- a/explorer/webpack.prod.js +++ /dev/null @@ -1,52 +0,0 @@ -const webpack = require('webpack'); -const { mergeWithRules } = require('webpack-merge'); -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const commonConfig = require('./webpack.common'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(commonConfig, { - mode: 'production', - - // TODO: no source maps, add back - devtool: false, - - module: { - rules: [ - { - test: /\.m?js/, - resolve: { - fullySpecified: false, - }, - }, - { - test: /\.css$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'], - }, - ], - }, - optimization: { - minimizer: [`...`, new CssMinimizerPlugin()], - splitChunks: { - chunks: 'all', - }, - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: '[name].[contenthash].css', - }), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - }), - ], - output: { - pathinfo: false, - filename: '[name].[contenthash].js', - }, -}); diff --git a/lefthook.yml b/lefthook.yml index 11b130f92a..0687e78972 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -47,3 +47,7 @@ pre-commit: glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,css}" run: yarn biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} stage_fixed: true + rust-lint: + glob: "*.rs" + run: cargo fmt + stage_fixed: true diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 117fdcc07d..3d4447dfff 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -265,6 +265,10 @@ impl RoutingScore { Self { score } } + pub const fn zero() -> RoutingScore { + RoutingScore { score: 0.0 } + } + pub fn legacy_performance(&self) -> Performance { Performance::naive_try_from_f64(self.score).unwrap_or_default() } diff --git a/nym-api/src/circulating_supply_api/cache/data.rs b/nym-api/src/circulating_supply_api/cache/data.rs deleted file mode 100644 index 77ddbaaee2..0000000000 --- a/nym-api/src/circulating_supply_api/cache/data.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::support::caching::Cache; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::Coin; - -pub(crate) struct CirculatingSupplyCacheData { - // no need to cache that one as it's constant, but let's put it here for consistency sake - pub(crate) total_supply: Coin, - pub(crate) mixmining_reserve: Cache<Coin>, - pub(crate) vesting_tokens: Cache<Coin>, - pub(crate) circulating_supply: Cache<Coin>, -} - -impl CirculatingSupplyCacheData { - pub fn new(mix_denom: String) -> CirculatingSupplyCacheData { - let zero_coin = Coin::new(0, &mix_denom); - - CirculatingSupplyCacheData { - total_supply: Coin::new(1_000_000_000_000_000, mix_denom), - mixmining_reserve: Cache::new(zero_coin.clone()), - vesting_tokens: Cache::new(zero_coin.clone()), - circulating_supply: Cache::new(zero_coin), - } - } -} - -impl<'a> From<&'a CirculatingSupplyCacheData> for CirculatingSupplyResponse { - fn from(value: &'a CirculatingSupplyCacheData) -> Self { - CirculatingSupplyResponse { - total_supply: value.total_supply.clone().into(), - mixmining_reserve: value.mixmining_reserve.clone().into(), - vesting_tokens: value.vesting_tokens.clone().into(), - circulating_supply: value.circulating_supply.clone().into(), - } - } -} diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs deleted file mode 100644 index 87e401da9d..0000000000 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use self::data::CirculatingSupplyCacheData; -use cosmwasm_std::Addr; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::error::NyxdError; -use nym_validator_client::nyxd::Coin; -use std::ops::Deref; -use std::{ - sync::{atomic::AtomicBool, Arc}, - time::Duration, -}; -use thiserror::Error; -use tokio::sync::RwLock; -use tokio::time; -use tracing::{error, info}; - -mod data; -pub(crate) mod refresher; - -#[derive(Debug, Error)] -enum CirculatingSupplyCacheError { - // this can only happen if somebody decides to set their staking address - // before https://github.com/nymtech/nym/pull/2796 is deployed - #[error("vesting account owned by {owner} with id {account_id} appeared more than once in the query response")] - DuplicateVestingAccountEntry { owner: Addr, account_id: u32 }, - - // this can happen if somehow the query was incomplete, like some paged sub-query didn't return full result - // or there's a bug with paging. or if, somehow, a vesting account got removed from the contract - #[error("got an inconsistent number of vesting account. received data on {got}, but expected {expected}")] - InconsistentNumberOfVestingAccounts { expected: usize, got: usize }, - - #[error(transparent)] - ClientError { - #[from] - source: NyxdError, - }, -} - -/// A cache for the circulating supply of the network. Circulating supply is calculated by -/// taking the initial supply of 1bn coins, and subtracting the amount of coins that are -/// in the mixmining pool and tied up in vesting. -/// -/// The cache is quite simple and does not include an update listener that the other caches have. -#[derive(Clone)] -pub(crate) struct CirculatingSupplyCache { - initialised: Arc<AtomicBool>, - data: Arc<RwLock<CirculatingSupplyCacheData>>, -} - -impl CirculatingSupplyCache { - pub(crate) fn new(mix_denom: String) -> CirculatingSupplyCache { - CirculatingSupplyCache { - initialised: Arc::new(AtomicBool::new(false)), - data: Arc::new(RwLock::new(CirculatingSupplyCacheData::new(mix_denom))), - } - } - - pub(crate) async fn get_circulating_supply(&self) -> Option<CirculatingSupplyResponse> { - match time::timeout(Duration::from_millis(100), self.data.read()).await { - Ok(cache) => Some(cache.deref().into()), - Err(err) => { - error!("Failed to get circulating supply: {err}"); - None - } - } - } - - pub(crate) async fn update(&self, mixmining_reserve: Coin, vesting_tokens: Coin) { - let mut cache = self.data.write().await; - - let mut circulating_supply = cache.total_supply.clone(); - circulating_supply.amount -= mixmining_reserve.amount; - circulating_supply.amount -= vesting_tokens.amount; - - info!("Updating circulating supply cache"); - info!("the mixmining reserve is now {mixmining_reserve}"); - info!("the number of tokens still vesting is now {vesting_tokens}"); - info!("the circulating supply is now {circulating_supply}"); - - cache.mixmining_reserve.unchecked_update(mixmining_reserve); - cache.vesting_tokens.unchecked_update(vesting_tokens); - cache - .circulating_supply - .unchecked_update(circulating_supply); - } -} diff --git a/nym-api/src/circulating_supply_api/cache/refresher.rs b/nym-api/src/circulating_supply_api/cache/refresher.rs deleted file mode 100644 index 60d7c2bafc..0000000000 --- a/nym-api/src/circulating_supply_api/cache/refresher.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use super::CirculatingSupplyCache; -use crate::circulating_supply_api::cache::CirculatingSupplyCacheError; -use crate::support::nyxd::Client; -use nym_contracts_common::truncate_decimal; -use nym_task::TaskClient; -use nym_validator_client::nyxd::Coin; -use std::collections::HashSet; -use std::sync::atomic::Ordering; -use std::time::Duration; -use tokio::time; -use tracing::{error, trace}; - -pub(crate) struct CirculatingSupplyCacheRefresher { - nyxd_client: Client, - cache: CirculatingSupplyCache, - caching_interval: Duration, -} - -impl CirculatingSupplyCacheRefresher { - pub(crate) fn new( - nyxd_client: Client, - cache: CirculatingSupplyCache, - caching_interval: Duration, - ) -> Self { - CirculatingSupplyCacheRefresher { - nyxd_client, - cache, - caching_interval, - } - } - - pub(crate) async fn run(&self, mut shutdown: TaskClient) { - let mut interval = time::interval(self.caching_interval); - while !shutdown.is_shutdown() { - tokio::select! { - _ = interval.tick() => { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("CirculatingSupplyCacheRefresher: Received shutdown"); - } - ret = self.refresh() => { - if let Err(err) = ret { - error!("Failed to refresh circulating supply cache - {err}"); - } else { - // relaxed memory ordering is fine here. worst case scenario network monitor - // will just have to wait for an additional backoff to see the change. - // And so this will not really incur any performance penalties by setting it every loop iteration - self.cache.initialised.store(true, Ordering::Relaxed) - } - } - } - } - _ = shutdown.recv() => { - trace!("CirculatingSupplyCacheRefresher: Received shutdown"); - } - } - } - } - - async fn get_mixmining_reserve( - &self, - mix_denom: &str, - ) -> Result<Coin, CirculatingSupplyCacheError> { - let reward_pool = self - .nyxd_client - .get_current_rewarding_parameters() - .await? - .interval - .reward_pool; - - Ok(Coin::new(truncate_decimal(reward_pool).u128(), mix_denom)) - } - - async fn get_total_vesting_tokens( - &self, - mix_denom: &str, - ) -> Result<Coin, CirculatingSupplyCacheError> { - let all_vesting = self.nyxd_client.get_all_vesting_coins().await?; - - // sanity check invariants to make sure all accounts got considered and we got no duplicates - // the cache refreshes so infrequently that the performance penalty is negligible - let mut owners = HashSet::new(); - let mut ids = HashSet::new(); - for acc in &all_vesting { - if !owners.insert(acc.owner.clone()) { - return Err(CirculatingSupplyCacheError::DuplicateVestingAccountEntry { - owner: acc.owner.clone(), - account_id: acc.account_id, - }); - } - - if !ids.insert(acc.account_id) { - return Err(CirculatingSupplyCacheError::DuplicateVestingAccountEntry { - owner: acc.owner.clone(), - account_id: acc.account_id, - }); - } - } - - let current_storage_key = self - .nyxd_client - .get_current_vesting_account_storage_key() - .await?; - if all_vesting.len() != current_storage_key as usize { - return Err( - CirculatingSupplyCacheError::InconsistentNumberOfVestingAccounts { - expected: current_storage_key as usize, - got: all_vesting.len(), - }, - ); - } - - let mut total = Coin::new(0, mix_denom); - for account in all_vesting { - total.amount += account.still_vesting.amount.u128(); - } - - Ok(total) - } - - async fn refresh(&self) -> Result<(), CirculatingSupplyCacheError> { - let chain_details = self.nyxd_client.chain_details().await; - let mix_denom = &chain_details.mix_denom.base; - - let mixmining_reserve = self.get_mixmining_reserve(mix_denom).await?; - let vesting_tokens = self.get_total_vesting_tokens(mix_denom).await?; - - self.cache.update(mixmining_reserve, vesting_tokens).await; - Ok(()) - } -} diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs index afc04c0b12..3299afa7f1 100644 --- a/nym-api/src/circulating_supply_api/handlers.rs +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -1,10 +1,11 @@ // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::extract::Query; -use axum::{extract, Router}; +use axum::extract::{Query, State}; +use axum::Router; use nym_api_requests::models::CirculatingSupplyResponse; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_validator_client::nyxd::Coin; @@ -34,15 +35,11 @@ pub(crate) fn circulating_supply_routes() -> Router<AppState> { )] async fn get_full_circulating_supply( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, + State(contract_cache): State<MixnetContractCache>, ) -> AxumResult<FormattedResponse<CirculatingSupplyResponse>> { let output = output.output.unwrap_or_default(); - match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + match contract_cache.get_circulating_supply().await { Some(value) => Ok(output.to_response(value)), None => Err(AxumErrorResponse::internal_msg("unavailable")), } @@ -63,14 +60,10 @@ async fn get_full_circulating_supply( )] async fn get_total_supply( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, + State(contract_cache): State<MixnetContractCache>, ) -> AxumResult<FormattedResponse<f64>> { let output = output.output.unwrap_or_default(); - let full_circulating_supply = match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + let full_circulating_supply = match contract_cache.get_circulating_supply().await { Some(res) => res, None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; @@ -95,15 +88,11 @@ async fn get_total_supply( )] async fn get_circulating_supply( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, + State(contract_cache): State<MixnetContractCache>, ) -> AxumResult<FormattedResponse<f64>> { let output = output.output.unwrap_or_default(); - let full_circulating_supply = match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + let full_circulating_supply = match contract_cache.get_circulating_supply().await { Some(res) => res, None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 3253ccaaab..d1a19b2932 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -1,27 +1,4 @@ // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use self::cache::refresher::CirculatingSupplyCacheRefresher; -use crate::support::{config, nyxd}; -use nym_task::TaskManager; - -pub(crate) mod cache; pub(crate) mod handlers; - -/// Spawn the circulating supply cache refresher. -pub(crate) fn start_cache_refresh( - config: &config::CirculatingSupplyCacher, - nyxd_client: nyxd::Client, - circulating_supply_cache: &cache::CirculatingSupplyCache, - shutdown: &TaskManager, -) { - if config.enabled { - let refresher = CirculatingSupplyCacheRefresher::new( - nyxd_client, - circulating_supply_cache.to_owned(), - config.debug.caching_interval, - ); - let shutdown_listener = shutdown.subscribe(); - tokio::spawn(async move { refresher.run(shutdown_listener).await }); - } -} diff --git a/nym-api/src/ecash/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs index 41cfd11d36..1dc0b0908e 100644 --- a/nym-api/src/ecash/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -399,7 +399,13 @@ impl<R: RngCore + CryptoRng> DkgController<R> { for (dealer_index, dealing) in dealings.into_iter() { // attempt to decrypt our portion let dk = self.state.dkg_keypair().private_key(); - let share = match decrypt_share(dk, receiver_index, &dealing.ciphertexts, None) { + let share = match decrypt_share( + dkg::params(), + dk, + receiver_index, + &dealing.ciphertexts, + None, + ) { Ok(share) => share, Err(err) => { error!("failed to decrypt share {human_index}/{total} generated from dealer {dealer_index}: {err} - can't generate the full key"); diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 22ec524a29..faa142c2ea 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,20 +1,20 @@ // Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::api_routes::handlers::ecash_routes; use crate::ecash::error::{EcashError, Result}; use crate::ecash::keys::KeyPairWithEpoch; use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::config; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::http::state::AppState; use crate::support::nyxd::Client; @@ -1279,9 +1279,8 @@ impl TestFixture { chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)), address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000), forced_refresh: ForcedRefresh::new(true), - nym_contract_cache: NymContractCache::new(), + mixnet_contract_cache: MixnetContractCache::new(), node_status_cache: NodeStatusCache::new(), - circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()), storage, described_nodes_cache: SharedCache::<DescribedNodes>::new(), network_details: NetworkDetails::new( @@ -1289,6 +1288,7 @@ impl TestFixture { NymNetworkDetails::new_empty(), ), node_info_cache: unstable::NodeInfoCache::default(), + contract_info_cache: ContractDetailsCache::new(Duration::from_secs(42)), api_status: ApiStatusState::new(None), ecash_state: Arc::new(ecash_state), } diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index dd2c64945e..572b941c99 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -12,9 +12,9 @@ // 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation // and hence this might be a good place for it. +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::{NodeStatusCache, ONE_DAY}; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; @@ -37,7 +37,7 @@ mod transition_beginning; // this is struct responsible for advancing an epoch pub struct EpochAdvancer { nyxd_client: Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, status_cache: NodeStatusCache, storage: NymApiStorage, @@ -52,7 +52,7 @@ impl EpochAdvancer { pub(crate) fn new( nyxd_client: Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, status_cache: NodeStatusCache, described_cache: SharedCache<DescribedNodes>, storage: NymApiStorage, @@ -239,7 +239,7 @@ impl EpochAdvancer { pub(crate) fn start( nyxd_client: Client, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, status_cache: &NodeStatusCache, described_cache: SharedCache<DescribedNodes>, storage: &NymApiStorage, diff --git a/nym-api/src/key_rotation/mod.rs b/nym-api/src/key_rotation/mod.rs index dfae12a75c..c6bc0cbe8b 100644 --- a/nym-api/src/key_rotation/mod.rs +++ b/nym-api/src/key_rotation/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::NymContractCache; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::support::caching::refresher::{CacheUpdateWatcher, RefreshRequester}; use nym_mixnet_contract_common::{Interval, KeyRotationState}; use nym_task::TaskClient; @@ -59,14 +59,14 @@ pub(crate) struct KeyRotationController { pub(crate) describe_cache_refresher: RefreshRequester, pub(crate) contract_cache_watcher: CacheUpdateWatcher, - pub(crate) contract_cache: NymContractCache, + pub(crate) contract_cache: MixnetContractCache, } impl KeyRotationController { pub(crate) fn new( describe_cache_refresher: RefreshRequester, contract_cache_watcher: CacheUpdateWatcher, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, ) -> KeyRotationController { KeyRotationController { last_described_refreshed_for: None, diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 76bd1ab3dd..c07a6c2e8f 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,17 +1,14 @@ // Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -#![warn(clippy::todo)] -#![warn(clippy::dbg_macro)] - use crate::epoch_operations::EpochAdvancer; use crate::support::cli; use crate::support::storage; use ::nym_config::defaults::setup_env; use clap::Parser; +use mixnet_contract_cache::cache::MixnetContractCache; use node_status_api::NodeStatusCache; use nym_bin_common::logging::setup_tracing_logger; -use nym_contract_cache::cache::NymContractCache; use support::nyxd; use tracing::{info, trace}; @@ -19,11 +16,12 @@ mod circulating_supply_api; mod ecash; mod epoch_operations; mod key_rotation; +pub(crate) mod mixnet_contract_cache; pub(crate) mod network; mod network_monitor; pub(crate) mod node_describe_cache; +mod node_performance; pub(crate) mod node_status_api; -pub(crate) mod nym_contract_cache; pub(crate) mod nym_nodes; mod status; pub(crate) mod support; diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/mixnet_contract_cache/cache/data.rs similarity index 63% rename from nym-api/src/nym_contract_cache/cache/data.rs rename to nym-api/src/mixnet_contract_cache/cache/data.rs index f54f9c48f1..d648c1037b 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/mixnet_contract_cache/cache/data.rs @@ -3,14 +3,11 @@ use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::ConfigScoreDataResponse; -use nym_contracts_common::ContractBuildInformation; use nym_mixnet_contract_common::{ ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, KeyRotationState, NymNodeDetails, RewardingParams, }; use nym_topology::CachedEpochRewardedSet; -use nym_validator_client::nyxd::AccountId; -use std::collections::HashMap; #[derive(Clone)] pub(crate) struct ConfigScoreData { @@ -31,7 +28,9 @@ impl From<ConfigScoreData> for ConfigScoreDataResponse { } } -pub(crate) struct ContractCacheData { +pub(crate) struct MixnetContractCacheData { + pub(crate) rewarding_denom: String, + pub(crate) legacy_mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, pub(crate) legacy_gateways: Vec<LegacyGatewayBondWithId>, pub(crate) nym_nodes: Vec<NymNodeDetails>, @@ -41,30 +40,4 @@ pub(crate) struct ContractCacheData { pub(crate) current_reward_params: RewardingParams, pub(crate) current_interval: Interval, pub(crate) key_rotation_state: KeyRotationState, - - pub(crate) contracts_info: CachedContractsInfo, -} - -type ContractAddress = String; -pub type CachedContractsInfo = HashMap<ContractAddress, CachedContractInfo>; - -#[derive(Clone)] -pub struct CachedContractInfo { - pub(crate) address: Option<AccountId>, - pub(crate) base: Option<cw2::ContractVersion>, - pub(crate) detailed: Option<ContractBuildInformation>, -} - -impl CachedContractInfo { - pub fn new( - address: Option<&AccountId>, - base: Option<cw2::ContractVersion>, - detailed: Option<ContractBuildInformation>, - ) -> Self { - Self { - address: address.cloned(), - base, - detailed, - } - } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/mixnet_contract_cache/cache/mod.rs similarity index 78% rename from nym-api/src/nym_contract_cache/cache/mod.rs rename to nym-api/src/mixnet_contract_cache/cache/mod.rs index e73dde0f41..39915845e8 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/mixnet_contract_cache/cache/mod.rs @@ -1,52 +1,56 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; use crate::node_describe_cache::refresh::RefreshData; -use crate::nym_contract_cache::cache::data::{CachedContractsInfo, ConfigScoreData}; use crate::support::caching::cache::{SharedCache, UninitialisedCache}; use crate::support::caching::Cache; -use data::ContractCacheData; +use data::MixnetContractCacheData; use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; -use nym_api_requests::models::MixnodeStatus; +use nym_api_requests::models::{CirculatingSupplyResponse, MixnodeStatus}; +use nym_contracts_common::truncate_decimal; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ Interval, KeyRotationState, NodeId, NymNodeDetails, RewardingParams, }; use nym_topology::CachedEpochRewardedSet; +use nym_validator_client::nyxd::Coin; use time::OffsetDateTime; use tokio::sync::RwLockReadGuard; pub(crate) mod data; pub(crate) mod refresher; +const TOTAL_SUPPLY_AMOUNT: u128 = 1_000_000_000_000_000; // 1B tokens + #[derive(Clone)] -pub struct NymContractCache { - pub(crate) inner: SharedCache<ContractCacheData>, +pub struct MixnetContractCache { + pub(crate) inner: SharedCache<MixnetContractCacheData>, } -impl NymContractCache { +impl MixnetContractCache { pub(crate) fn new() -> Self { - NymContractCache { + MixnetContractCache { inner: SharedCache::new(), } } - pub(crate) fn inner(&self) -> SharedCache<ContractCacheData> { + pub(crate) fn inner(&self) -> SharedCache<MixnetContractCacheData> { self.inner.clone() } async fn get_owned<T>( &self, - fn_arg: impl FnOnce(&ContractCacheData) -> T, + fn_arg: impl FnOnce(&MixnetContractCacheData) -> T, ) -> Result<T, UninitialisedCache> { Ok(fn_arg(&**self.inner.get().await?)) } async fn get<'a, T: 'a>( &'a self, - fn_arg: impl FnOnce(&Cache<ContractCacheData>) -> &T, + fn_arg: impl FnOnce(&Cache<MixnetContractCacheData>) -> &T, ) -> Result<RwLockReadGuard<'a, T>, UninitialisedCache> { let guard = self.inner.get().await?; Ok(RwLockReadGuard::map(guard, fn_arg)) @@ -157,12 +161,6 @@ impl NymContractCache { .key_rotation_id(current_absolute_epoch_id)) } - pub(crate) async fn contract_details(&self) -> CachedContractsInfo { - self.get_owned(|c| c.contracts_info.clone()) - .await - .unwrap_or_default() - } - pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { let Ok(cache) = self.inner.get().await else { return Default::default(); @@ -215,6 +213,31 @@ impl NymContractCache { None } + pub(crate) async fn get_circulating_supply(&self) -> Option<CirculatingSupplyResponse> { + let mix_denom = self.get_owned(|c| c.rewarding_denom.clone()).await.ok()?; + let reward_pool = self + .interval_reward_params() + .await + .ok()? + .interval + .reward_pool; + + let mixmining_reserve_amount = truncate_decimal(reward_pool).u128(); + let mixmining_reserve = Coin::new(mixmining_reserve_amount, &mix_denom).into(); + + // given all tokens have already vested, the circulating supply is total supply - mixmining reserve + let circulating_supply = + Coin::new(TOTAL_SUPPLY_AMOUNT - mixmining_reserve_amount, &mix_denom).into(); + + Some(CirculatingSupplyResponse { + total_supply: Coin::new(TOTAL_SUPPLY_AMOUNT, &mix_denom).into(), + mixmining_reserve, + // everything has already vested + vesting_tokens: Coin::new(0, &mix_denom).into(), + circulating_supply, + }) + } + pub(crate) async fn naive_wait_for_initial_values(&self) { self.inner.naive_wait_for_initial_values().await } diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/mixnet_contract_cache/cache/refresher.rs similarity index 59% rename from nym-api/src/nym_contract_cache/cache/refresher.rs rename to nym-api/src/mixnet_contract_cache/cache/refresher.rs index 87af6d2d04..7e0735ca21 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/mixnet_contract_cache/cache/refresher.rs @@ -1,9 +1,7 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::data::{ - CachedContractInfo, CachedContractsInfo, ConfigScoreData, ContractCacheData, -}; +use crate::mixnet_contract_cache::cache::data::{ConfigScoreData, MixnetContractCacheData}; use crate::nyxd::Client; use crate::support::caching::refresher::CacheItemProvider; use anyhow::Result; @@ -12,9 +10,6 @@ use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; use nym_mixnet_contract_common::LegacyMixLayer; -use nym_validator_client::nyxd::contract_traits::{ - MixnetQueryClient, NymContractsProvider, VestingQueryClient, -}; use nym_validator_client::nyxd::error::NyxdError; use rand::prelude::SliceRandom; use rand::rngs::OsRng; @@ -22,90 +17,29 @@ use std::collections::HashMap; use std::collections::HashSet; use tracing::info; -pub struct ContractDataProvider { +pub struct MixnetContractDataProvider { nyxd_client: Client, } #[async_trait] -impl CacheItemProvider for ContractDataProvider { - type Item = ContractCacheData; +impl CacheItemProvider for MixnetContractDataProvider { + type Item = MixnetContractCacheData; type Error = NyxdError; - async fn try_refresh(&self) -> std::result::Result<Self::Item, Self::Error> { - self.refresh().await + async fn try_refresh(&mut self) -> std::result::Result<Option<Self::Item>, Self::Error> { + self.refresh().await.map(Some) } } -impl ContractDataProvider { +impl MixnetContractDataProvider { pub(crate) fn new(nyxd_client: Client) -> Self { - ContractDataProvider { nyxd_client } + MixnetContractDataProvider { nyxd_client } } - async fn get_nym_contracts_info(&self) -> Result<CachedContractsInfo, NyxdError> { - use crate::query_guard; - - let mut updated = HashMap::new(); - - let client_guard = self.nyxd_client.read().await; - - let mixnet = query_guard!(client_guard, mixnet_contract_address()); - let vesting = query_guard!(client_guard, vesting_contract_address()); - let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); - let group = query_guard!(client_guard, group_contract_address()); - let multisig = query_guard!(client_guard, multisig_contract_address()); - let ecash = query_guard!(client_guard, ecash_contract_address()); - - for (address, name) in [ - (mixnet, "nym-mixnet-contract"), - (vesting, "nym-vesting-contract"), - (coconut_dkg, "nym-coconut-dkg-contract"), - (group, "nym-cw4-group-contract"), - (multisig, "nym-cw3-multisig-contract"), - (ecash, "nym-ecash-contract"), - ] { - let (cw2, build_info) = if let Some(address) = address { - let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); - let mut build_info = query_guard!( - client_guard, - try_get_contract_build_information(address).await - ); - - // for backwards compatibility until we migrate the contracts - if build_info.is_none() { - match name { - "nym-mixnet-contract" => { - build_info = Some(query_guard!( - client_guard, - get_mixnet_contract_version().await - )?) - } - "nym-vesting-contract" => { - build_info = Some(query_guard!( - client_guard, - get_vesting_contract_version().await - )?) - } - _ => (), - } - } - - (cw2, build_info) - } else { - (None, None) - }; - - updated.insert( - name.to_string(), - CachedContractInfo::new(address, cw2, build_info), - ); - } - - Ok(updated) - } - - async fn refresh(&self) -> Result<ContractCacheData, NyxdError> { + async fn refresh(&self) -> Result<MixnetContractCacheData, NyxdError> { let current_reward_params = self.nyxd_client.get_current_rewarding_parameters().await?; let current_interval = self.nyxd_client.get_current_interval().await?.interval; + let contract_state = self.nyxd_client.get_mixnet_contract_state().await?; let nym_nodes = self.nyxd_client.get_nymnodes().await?; let mixnode_details = self.nyxd_client.get_mixnodes().await?; @@ -184,7 +118,6 @@ impl ContractDataProvider { let key_rotation_state = self.nyxd_client.get_key_rotation_state().await?; let config_score_params = self.nyxd_client.get_config_score_params().await?; let nym_node_version_history = self.nyxd_client.get_nym_node_version_history().await?; - let contracts_info = self.get_nym_contracts_info().await?; info!( "Updating validator cache. There are {} [legacy] mixnodes, {} [legacy] gateways and {} nym nodes", @@ -193,7 +126,8 @@ impl ContractDataProvider { nym_nodes.len(), ); - Ok(ContractCacheData { + Ok(MixnetContractCacheData { + rewarding_denom: contract_state.rewarding_denom, legacy_mixnodes, legacy_gateways, nym_nodes, @@ -205,7 +139,6 @@ impl ContractDataProvider { current_reward_params, current_interval, key_rotation_state, - contracts_info, }) } } diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/mixnet_contract_cache/handlers.rs similarity index 100% rename from nym-api/src/nym_contract_cache/handlers.rs rename to nym-api/src/mixnet_contract_cache/handlers.rs diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/mixnet_contract_cache/mod.rs similarity index 50% rename from nym-api/src/nym_contract_cache/mod.rs rename to nym-api/src/mixnet_contract_cache/mod.rs index cdb6bcd8e0..9911c55683 100644 --- a/nym-api/src/nym_contract_cache/mod.rs +++ b/nym-api/src/mixnet_contract_cache/mod.rs @@ -1,9 +1,9 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::nym_contract_cache::cache::data::ContractCacheData; -use crate::nym_contract_cache::cache::refresher::ContractDataProvider; -use crate::nym_contract_cache::cache::NymContractCache; +use crate::mixnet_contract_cache::cache::data::MixnetContractCacheData; +use crate::mixnet_contract_cache::cache::refresher::MixnetContractDataProvider; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::support::caching::refresher::CacheRefresher; use crate::support::{config, nyxd}; use nym_validator_client::nyxd::error::NyxdError; @@ -12,14 +12,14 @@ pub(crate) mod cache; pub(crate) mod handlers; pub(crate) fn build_refresher( - config: &config::NodeStatusAPI, - nym_contract_cache_state: &NymContractCache, + config: &config::MixnetContractCache, + nym_contract_cache_state: &MixnetContractCache, nyxd_client: nyxd::Client, -) -> CacheRefresher<ContractCacheData, NyxdError> { +) -> CacheRefresher<MixnetContractCacheData, NyxdError> { CacheRefresher::new_with_initial_value( - Box::new(ContractDataProvider::new(nyxd_client)), + Box::new(MixnetContractDataProvider::new(nyxd_client)), config.debug.caching_interval, nym_contract_cache_state.inner(), ) - .named("contract-cache-refresher") + .named("mixnet-contract-cache-refresher") } diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index f5a3e12736..4d6c8e01f7 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -5,7 +5,7 @@ use crate::network::models::{ContractInformation, NetworkDetails}; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::{extract, Router}; +use axum::Router; use nym_api_requests::models::ChainStatusResponse; use nym_contracts_common::ContractBuildInformation; use nym_http_api_common::{FormattedResponse, OutputParams}; @@ -40,7 +40,7 @@ pub(crate) fn nym_network_routes() -> Router<AppState> { )] async fn network_details( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, + State(state): State<AppState>, ) -> FormattedResponse<NetworkDetails> { let output = output.output.unwrap_or_default(); @@ -116,13 +116,18 @@ pub struct ContractInformationContractVersion { )] async fn nym_contracts( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, -) -> FormattedResponse<HashMap<String, ContractInformation<cw2::ContractVersion>>> { + State(state): State<AppState>, +) -> AxumResult<FormattedResponse<HashMap<String, ContractInformation<cw2::ContractVersion>>>> { let output = output.output.unwrap_or_default(); - let info = state.nym_contract_cache().contract_details().await; - output.to_response( - info.iter() + let contract_info = state + .contract_info_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + Ok(output.to_response( + contract_info + .iter() .map(|(contract, info)| { ( contract.to_owned(), @@ -133,7 +138,7 @@ async fn nym_contracts( ) }) .collect::<HashMap<_, _>>(), - ) + )) } #[allow(dead_code)] // not dead, used in OpenAPI docs @@ -158,13 +163,18 @@ pub struct ContractInformationBuildInformation { )] async fn nym_contracts_detailed( Query(output): Query<OutputParams>, - extract::State(state): extract::State<AppState>, -) -> FormattedResponse<HashMap<String, ContractInformation<ContractBuildInformation>>> { + State(state): State<AppState>, +) -> AxumResult<FormattedResponse<HashMap<String, ContractInformation<ContractBuildInformation>>>> { let output = output.output.unwrap_or_default(); - let info = state.nym_contract_cache().contract_details().await; - output.to_response( - info.iter() + let contract_info = state + .contract_info_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + Ok(output.to_response( + contract_info + .iter() .map(|(contract, info)| { ( contract.to_owned(), @@ -175,5 +185,5 @@ async fn nym_contracts_detailed( ) }) .collect::<HashMap<_, _>>(), - ) + )) } diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index ea1421105f..d0134cafa6 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network_monitor::monitor::preparer::PacketPreparer; use crate::network_monitor::monitor::processor::{ ReceivedProcessor, ReceivedProcessorReceiver, ReceivedProcessorSender, @@ -13,7 +14,6 @@ use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::storage::NymApiStorage; use crate::support::caching::cache::SharedCache; use crate::support::config::Config; @@ -38,7 +38,7 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( config: &'a Config, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, storage: &NymApiStorage, @@ -58,7 +58,7 @@ pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, } @@ -68,7 +68,7 @@ impl<'a> NetworkMonitorBuilder<'a> { config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, ) -> Self { @@ -178,7 +178,7 @@ impl<R: MessageReceiver + Send + Sync + 'static> NetworkMonitorRunnables<R> { } fn new_packet_preparer( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, per_node_test_packets: usize, @@ -236,7 +236,7 @@ fn new_packet_receiver( // TODO: 2) how do we make it non-async as other 'start' methods? pub(crate) async fn start<R: MessageReceiver + Send + Sync + 'static>( config: &Config, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, storage: &NymApiStorage, diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index f0d6c4ebfa..49d48ab783 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -1,12 +1,12 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_describe_cache::NodeDescriptionTopologyExt; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; @@ -78,7 +78,7 @@ pub(crate) struct PreparedPackets { #[derive(Clone)] pub(crate) struct PacketPreparer { - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, @@ -96,7 +96,7 @@ pub(crate) struct PacketPreparer { impl PacketPreparer { pub(crate) fn new( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, per_node_test_packets: usize, diff --git a/nym-api/src/node_describe_cache/provider.rs b/nym-api/src/node_describe_cache/provider.rs index da74b2a5f9..259b14ba1e 100644 --- a/nym-api/src/node_describe_cache/provider.rs +++ b/nym-api/src/node_describe_cache/provider.rs @@ -1,10 +1,10 @@ // Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_describe_cache::refresh::RefreshData; use crate::node_describe_cache::NodeDescribeCacheError; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; use crate::support::config; @@ -15,7 +15,7 @@ use std::collections::HashMap; use tracing::{error, info}; pub struct NodeDescriptionProvider { - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, allow_all_ips: bool, batch_size: usize, @@ -23,7 +23,7 @@ pub struct NodeDescriptionProvider { impl NodeDescriptionProvider { pub(crate) fn new( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, allow_all_ips: bool, ) -> NodeDescriptionProvider { NodeDescriptionProvider { @@ -49,7 +49,7 @@ impl CacheItemProvider for NodeDescriptionProvider { self.contract_cache.naive_wait_for_initial_values().await } - async fn try_refresh(&self) -> Result<Self::Item, Self::Error> { + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> { // we need to query: // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) @@ -110,45 +110,37 @@ impl CacheItemProvider for NodeDescriptionProvider { info!("refreshed self described data for {} nodes", nodes.len()); info!("with {} unique ip addresses", addresses_cache.len()); - Ok(DescribedNodes { + Ok(Some(DescribedNodes { nodes, addresses_cache, - }) + })) } } // currently dead code : ( #[allow(dead_code)] pub(crate) fn new_refresher( - config: &config::TopologyCacher, - contract_cache: NymContractCache, + config: &config::DescribeCache, + contract_cache: MixnetContractCache, ) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> { CacheRefresher::new( - Box::new( - NodeDescriptionProvider::new( - contract_cache, - config.debug.node_describe_allow_illegal_ips, - ) - .with_batch_size(config.debug.node_describe_batch_size), - ), - config.debug.node_describe_caching_interval, + NodeDescriptionProvider::new(contract_cache, config.debug.allow_illegal_ips) + .with_batch_size(config.debug.batch_size), + config.debug.caching_interval, ) } pub(crate) fn new_provider_with_initial_value( - config: &config::TopologyCacher, - contract_cache: NymContractCache, + config: &config::DescribeCache, + contract_cache: MixnetContractCache, initial: SharedCache<DescribedNodes>, ) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> { CacheRefresher::new_with_initial_value( Box::new( - NodeDescriptionProvider::new( - contract_cache, - config.debug.node_describe_allow_illegal_ips, - ) - .with_batch_size(config.debug.node_describe_batch_size), + NodeDescriptionProvider::new(contract_cache, config.debug.allow_illegal_ips) + .with_batch_size(config.debug.batch_size), ), - config.debug.node_describe_caching_interval, + config.debug.caching_interval, initial, ) } diff --git a/nym-api/src/node_performance/contract_cache/data.rs b/nym-api/src/node_performance/contract_cache/data.rs new file mode 100644 index 0000000000..985ee0a4d7 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/data.rs @@ -0,0 +1,88 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::provider::PerformanceRetrievalFailure; +use nym_api_requests::models::RoutingScore; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use nym_validator_client::nyxd::contract_traits::performance_query_client::NodePerformance; +use std::collections::{BTreeMap, HashMap}; + +pub(crate) struct PerformanceContractEpochCacheData { + pub(crate) epoch_id: EpochId, + pub(crate) median_performance: HashMap<NodeId, Performance>, +} + +impl PerformanceContractEpochCacheData { + pub(crate) fn from_node_performance( + performance: Vec<NodePerformance>, + epoch_id: EpochId, + ) -> Self { + let median_performance = performance + .into_iter() + .map(|node_performance| (node_performance.node_id, node_performance.performance)) + .collect(); + PerformanceContractEpochCacheData { + epoch_id, + median_performance, + } + } +} + +pub(crate) struct PerformanceContractCacheData { + pub(crate) epoch_performance: BTreeMap<EpochId, PerformanceContractEpochCacheData>, +} + +impl PerformanceContractCacheData { + pub(crate) fn update( + &mut self, + update: PerformanceContractEpochCacheData, + values_to_retain: usize, + ) { + self.epoch_performance.insert(update.epoch_id, update); + if self.epoch_performance.len() > values_to_retain { + // remove the oldest entry, i.e. one with the lowest epoch id + self.epoch_performance.pop_first(); + } + } + + pub(crate) fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + // TODO: somehow send a signal to refresh this epoch + let epoch_scores = self.epoch_performance.get(&epoch_id).ok_or_else(|| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + format!("no cached performance results for epoch {epoch_id}"), + ) + })?; + + let node_score = epoch_scores + .median_performance + .get(&node_id) + .ok_or_else(|| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + format!( + "no cached performance results for node {node_id} for epoch {epoch_id}" + ), + ) + })?; + + Ok(RoutingScore::new(node_score.naive_to_f64())) + } +} + +// needed for cache initialisation +impl From<PerformanceContractEpochCacheData> for PerformanceContractCacheData { + fn from(cache_data: PerformanceContractEpochCacheData) -> Self { + let mut epoch_performance = BTreeMap::new(); + epoch_performance.insert(cache_data.epoch_id, cache_data); + PerformanceContractCacheData { epoch_performance } + } +} diff --git a/nym-api/src/node_performance/contract_cache/mod.rs b/nym-api/src/node_performance/contract_cache/mod.rs new file mode 100644 index 0000000000..412b2f00b1 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/mod.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::contract_cache::data::PerformanceContractCacheData; +use crate::node_performance::contract_cache::refresher::{ + refresher_update_fn, PerformanceContractDataProvider, +}; +use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::CacheRefresher; +use crate::support::{config, nyxd}; +use anyhow::bail; +use nym_task::TaskManager; + +pub(crate) mod data; +pub(crate) mod refresher; + +pub(crate) async fn start_cache_refresher( + config: &config::PerformanceProvider, + nyxd_client: nyxd::Client, + mixnet_contract_cache: MixnetContractCache, + task_manager: &TaskManager, +) -> anyhow::Result<SharedCache<PerformanceContractCacheData>> { + let values_to_retain = config.debug.max_epoch_entries_to_retain; + + let mut item_provider = + PerformanceContractDataProvider::new(nyxd_client, mixnet_contract_cache); + + if !item_provider.cache_has_values().await { + bail!("performance contract is empty - can't use it as source of node performance") + } + + let warmed_up_cache = SharedCache::new_with_value( + item_provider + .provide_initial_warmed_up_cache(values_to_retain) + .await?, + ); + + CacheRefresher::new_with_initial_value( + Box::new(item_provider), + config.debug.contract_polling_interval, + warmed_up_cache.clone(), + ) + .named("performance-contract-cache-refresher") + .with_update_fn(move |main_cache, update| { + refresher_update_fn(main_cache, update, values_to_retain) + }) + .start(task_manager.subscribe_named("performance-contract-cache-refresher")); + + Ok(warmed_up_cache) +} diff --git a/nym-api/src/node_performance/contract_cache/refresher.rs b/nym-api/src/node_performance/contract_cache/refresher.rs new file mode 100644 index 0000000000..887374b018 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/refresher.rs @@ -0,0 +1,135 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::contract_cache::data::{ + PerformanceContractCacheData, PerformanceContractEpochCacheData, +}; +use crate::support::caching::refresher::CacheItemProvider; +use crate::support::nyxd::Client; +use async_trait::async_trait; +use nym_validator_client::nyxd::contract_traits::performance_query_client::LastSubmission; +use nym_validator_client::nyxd::error::NyxdError; +use std::collections::BTreeMap; + +pub struct PerformanceContractDataProvider { + nyxd_client: Client, + mixnet_contract_cache: MixnetContractCache, + last_submission: Option<LastSubmission>, +} + +pub(crate) fn refresher_update_fn( + main_cache: &mut PerformanceContractCacheData, + update: PerformanceContractEpochCacheData, + values_to_retain: usize, +) { + main_cache.update(update, values_to_retain); +} + +#[async_trait] +impl CacheItemProvider for PerformanceContractDataProvider { + type Item = PerformanceContractEpochCacheData; + type Error = NyxdError; + + async fn wait_until_ready(&self) { + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await + } + + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> { + self.refresh().await + } +} + +impl PerformanceContractDataProvider { + pub(crate) fn new(nyxd_client: Client, mixnet_contract_cache: MixnetContractCache) -> Self { + PerformanceContractDataProvider { + nyxd_client, + mixnet_contract_cache, + last_submission: None, + } + } + + pub(crate) async fn cache_has_values(&self) -> bool { + let Ok(last_submitted) = self + .nyxd_client + .get_last_performance_contract_submission() + .await + else { + return false; + }; + last_submitted.data.is_some() + } + + pub(crate) async fn provide_initial_warmed_up_cache( + &mut self, + values_to_keep: usize, + ) -> Result<PerformanceContractCacheData, NyxdError> { + let last_submitted = self + .nyxd_client + .get_last_performance_contract_submission() + .await?; + + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await; + + // SAFETY: we just waited for cache to be available + #[allow(clippy::unwrap_used)] + let current_epoch = self + .mixnet_contract_cache + .current_interval() + .await + .unwrap() + .current_epoch_absolute_id(); + + let last = current_epoch.saturating_sub(values_to_keep as u32); + + let mut epoch_performance = BTreeMap::default(); + for epoch in current_epoch..last { + let performance = self.nyxd_client.get_full_epoch_performance(epoch).await?; + let per_epoch_performance = + PerformanceContractEpochCacheData::from_node_performance(performance, epoch); + epoch_performance.insert(epoch, per_epoch_performance); + } + + self.last_submission = Some(last_submitted); + + Ok(PerformanceContractCacheData { epoch_performance }) + } + + async fn refresh(&mut self) -> Result<Option<PerformanceContractEpochCacheData>, NyxdError> { + let last_submitted = self + .nyxd_client + .get_last_performance_contract_submission() + .await?; + + // no updates + if let Some(prior_submission) = &self.last_submission { + if prior_submission == &last_submitted { + return Ok(None); + } + } + + // SAFETY: refresher is not started until the mixnet contract cache had been initialised + #[allow(clippy::unwrap_used)] + let current_epoch = self + .mixnet_contract_cache + .current_interval() + .await + .unwrap() + .current_epoch_absolute_id(); + + let performance = self + .nyxd_client + .get_full_epoch_performance(current_epoch) + .await?; + + self.last_submission = Some(last_submitted); + + Ok(Some( + PerformanceContractEpochCacheData::from_node_performance(performance, current_epoch), + )) + } +} diff --git a/nym-api/src/node_performance/mod.rs b/nym-api/src/node_performance/mod.rs new file mode 100644 index 0000000000..f7f5eb5dd1 --- /dev/null +++ b/nym-api/src/node_performance/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod contract_cache; +pub(crate) mod provider; diff --git a/nym-api/src/node_performance/provider/contract_provider.rs b/nym-api/src/node_performance/provider/contract_provider.rs new file mode 100644 index 0000000000..29923ccd20 --- /dev/null +++ b/nym-api/src/node_performance/provider/contract_provider.rs @@ -0,0 +1,94 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::contract_cache::data::PerformanceContractCacheData; +use crate::node_performance::provider::{NodesRoutingScores, PerformanceRetrievalFailure}; +use crate::support::caching::cache::SharedCache; +use crate::support::config; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use std::collections::HashMap; +use tracing::warn; + +pub(crate) struct ContractPerformanceProvider { + cached: SharedCache<PerformanceContractCacheData>, + max_epochs_fallback: u32, +} + +impl ContractPerformanceProvider { + pub(crate) fn new( + config: &config::PerformanceProvider, + contract_cache: SharedCache<PerformanceContractCacheData>, + ) -> Self { + ContractPerformanceProvider { + cached: contract_cache, + max_epochs_fallback: config.debug.max_performance_fallback_epochs, + } + } + + fn node_routing_score_with_fallback( + &self, + contract_cache: &PerformanceContractCacheData, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let err = match contract_cache.node_routing_score(node_id, epoch_id) { + Ok(res) => return Ok(res), + Err(err) => err, + }; + + warn!("failed to retrieve performance score of node {node_id} for epoch {epoch_id}. falling back to at most {} past epochs", self.max_epochs_fallback); + + let threshold = epoch_id.saturating_sub(self.max_epochs_fallback); + let start = epoch_id.saturating_sub(1); + for epoch_id in start..threshold { + if let Ok(res) = contract_cache.node_routing_score(node_id, epoch_id) { + return Ok(res); + } + } + + Err(err) + } + + pub(crate) async fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let contract_cache = self.cached.get().await.map_err(|_| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + "performance contract cache has not been initialised yet", + ) + })?; + + self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id) + } + + pub(crate) async fn node_routing_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + let Some(first) = node_ids.first() else { + return Ok(NodesRoutingScores::empty()); + }; + + let contract_cache = self.cached.get().await.map_err(|_| { + PerformanceRetrievalFailure::new( + *first, + epoch_id, + "performance contract cache has not been initialised yet", + ) + })?; + + let mut scores = HashMap::new(); + for node_id in node_ids { + let score = self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id); + scores.insert(node_id, score); + } + + Ok(NodesRoutingScores { inner: scores }) + } +} diff --git a/nym-api/src/node_performance/provider/legacy_storage_provider.rs b/nym-api/src/node_performance/provider/legacy_storage_provider.rs new file mode 100644 index 0000000000..99a886a19b --- /dev/null +++ b/nym-api/src/node_performance/provider/legacy_storage_provider.rs @@ -0,0 +1,91 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::provider::PerformanceRetrievalFailure; +use crate::support::caching::cache::UninitialisedCache; +use crate::support::storage::NymApiStorage; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; + +pub(crate) struct LegacyStoragePerformanceProvider { + storage: NymApiStorage, + mixnet_contract_cache: MixnetContractCache, +} + +impl LegacyStoragePerformanceProvider { + pub(crate) fn new(storage: NymApiStorage, mixnet_contract_cache: MixnetContractCache) -> Self { + LegacyStoragePerformanceProvider { + storage, + mixnet_contract_cache, + } + } + + async fn map_epoch_id_to_end_unix_timestamp( + &self, + epoch_id: EpochId, + ) -> Result<i64, UninitialisedCache> { + let interval_details = self.mixnet_contract_cache.current_interval().await?; + let duration = interval_details.epoch_length(); + let current_end = interval_details.current_epoch_end(); + let current_id = interval_details.current_epoch_absolute_id(); + + if current_id == epoch_id { + return Ok(current_end.unix_timestamp()); + } + + if current_id < epoch_id { + let diff = epoch_id - current_id; + let end = current_end + diff * duration; + return Ok(end.unix_timestamp()); + } + + // epoch_id > current_id + let diff = current_id - epoch_id; + let end = current_end - diff * duration; + Ok(end.unix_timestamp()) + } + + pub(crate) async fn epoch_id_unix_timestamp( + &self, + epoch_id: EpochId, + ) -> Result<i64, PerformanceRetrievalFailure> { + self.map_epoch_id_to_end_unix_timestamp(epoch_id) + .await + .map_err(|_| { + PerformanceRetrievalFailure::new( + 0, + epoch_id, + "mixnet contract cache has not been initialised yet", + ) + }) + } + + pub(crate) async fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let end_ts = self.epoch_id_unix_timestamp(epoch_id).await?; + self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, end_ts) + .await + } + + pub(crate) async fn get_node_routing_score_with_unix_timestamp( + &self, + node_id: NodeId, + epoch_id: EpochId, + end_ts: i64, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let reliability = self + .storage + .get_average_node_reliability_in_the_last_24hrs(node_id, end_ts) + .await + .map_err(|err| PerformanceRetrievalFailure::new(node_id, epoch_id, err.to_string()))?; + + // reliability: 0-100 + // score: 0-1 + let score = reliability / 100.; + Ok(RoutingScore::new(score as f64)) + } +} diff --git a/nym-api/src/node_performance/provider/mod.rs b/nym-api/src/node_performance/provider/mod.rs new file mode 100644 index 0000000000..aadc61a1d5 --- /dev/null +++ b/nym-api/src/node_performance/provider/mod.rs @@ -0,0 +1,123 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::provider::contract_provider::ContractPerformanceProvider; +use async_trait::async_trait; +use legacy_storage_provider::LegacyStoragePerformanceProvider; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use std::collections::HashMap; +use thiserror::Error; +use tracing::{debug, error}; + +pub(crate) mod contract_provider; +pub(crate) mod legacy_storage_provider; + +#[derive(Debug, Error)] +#[error("failed to retrieve performance score for node {node_id} for epoch {epoch_id}: {error}")] +pub(crate) struct PerformanceRetrievalFailure { + pub(crate) node_id: NodeId, + pub(crate) epoch_id: EpochId, + pub(crate) error: String, +} + +impl PerformanceRetrievalFailure { + pub(crate) fn new(node_id: NodeId, epoch_id: EpochId, error: impl Into<String>) -> Self { + PerformanceRetrievalFailure { + node_id, + epoch_id, + error: error.into(), + } + } +} + +pub(crate) struct NodesRoutingScores { + inner: HashMap<NodeId, Result<RoutingScore, PerformanceRetrievalFailure>>, +} + +impl NodesRoutingScores { + pub(crate) fn empty() -> Self { + NodesRoutingScores { + inner: HashMap::new(), + } + } + pub(crate) fn get_or_log(&self, node_id: NodeId) -> RoutingScore { + match self.inner.get(&node_id) { + Some(Ok(score)) => *score, + Some(Err(err)) => { + debug!("{err}"); + RoutingScore::zero() + } + None => RoutingScore::zero(), + } + } +} + +#[async_trait] +pub(crate) trait NodePerformanceProvider { + /// Obtain a performance/routing score of a particular node for given epoch + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure>; + + /// An optimisation for obtaining node scores of multiple nodes at once + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure>; +} + +#[async_trait] +impl NodePerformanceProvider for ContractPerformanceProvider { + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + self.node_routing_score(node_id, epoch_id).await + } + + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + self.node_routing_scores(node_ids, epoch_id).await + } +} + +#[async_trait] +impl NodePerformanceProvider for LegacyStoragePerformanceProvider { + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + self.node_routing_score(node_id, epoch_id).await + } + + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + let mut scores = HashMap::new(); + + let epoch_timestamp = self.epoch_id_unix_timestamp(epoch_id).await?; + for node_id in node_ids { + scores.insert( + node_id, + self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, epoch_timestamp) + .await, + ); + } + + Ok(NodesRoutingScores { inner: scores }) + } +} diff --git a/nym-api/src/node_status_api/cache/config_score.rs b/nym-api/src/node_status_api/cache/config_score.rs new file mode 100644 index 0000000000..30f24d0b40 --- /dev/null +++ b/nym-api/src/node_status_api/cache/config_score.rs @@ -0,0 +1,63 @@ +// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; +use nym_api_requests::models::{ConfigScore, NymNodeDescription}; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::VersionScoreFormulaParams; + +fn versions_behind_factor_to_config_score( + versions_behind: u32, + params: VersionScoreFormulaParams, +) -> f64 { + let penalty = params.penalty.naive_to_f64(); + let scaling = params.penalty_scaling.naive_to_f64(); + + // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) + penalty.powf((versions_behind as f64).powf(scaling)) +} + +pub(crate) fn calculate_config_score( + config_score_data: &ConfigScoreData, + described_data: Option<&NymNodeDescription>, +) -> ConfigScore { + let Some(described) = described_data else { + return ConfigScore::unavailable(); + }; + + let node_version = &described.description.build_information.build_version; + let Ok(reported_semver) = node_version.parse::<semver::Version>() else { + return ConfigScore::bad_semver(); + }; + let versions_behind = config_score_data + .config_score_params + .version_weights + .versions_behind_factor( + &reported_semver, + &config_score_data.nym_node_version_history, + ); + + let runs_nym_node = described.description.build_information.binary_name == "nym-node"; + let accepted_terms_and_conditions = described + .description + .auxiliary_details + .accepted_operator_terms_and_conditions; + + let version_score = if !runs_nym_node || !accepted_terms_and_conditions { + 0. + } else { + versions_behind_factor_to_config_score( + versions_behind, + config_score_data + .config_score_params + .version_score_formula_params, + ) + }; + + ConfigScore::new( + version_score, + versions_behind, + accepted_terms_and_conditions, + runs_nym_node, + ) +} diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 01d1be13bb..73aca9c848 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use self::data::NodeStatusCacheData; +use crate::node_performance::provider::PerformanceRetrievalFailure; use crate::support::caching::cache::UninitialisedCache; use crate::support::caching::Cache; use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; @@ -16,9 +17,9 @@ use tracing::error; const CACHE_TIMEOUT_MS: u64 = 100; +mod config_score; pub mod data; mod inclusion_probabilities; -mod node_sets; pub mod refresher; #[derive(Debug, Error)] @@ -28,6 +29,9 @@ enum NodeStatusCacheError { #[error("the self-described cache data is not available")] UnavailableDescribedCache, + + #[error(transparent)] + PerformanceRetrievalFailure(#[from] PerformanceRetrievalFailure), } impl From<UninitialisedCache> for NodeStatusCacheError { diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs deleted file mode 100644 index 09ea897c33..0000000000 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::cache::DescribedNodes; -use crate::node_status_api::helpers::RewardedSetStatus; -use crate::node_status_api::models::Uptime; -use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; -use crate::nym_contract_cache::cache::data::ConfigScoreData; -use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; -use crate::support::storage::NymApiStorage; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::{ - ConfigScore, DescribedNodeType, DetailedNodePerformance, GatewayBondAnnotated, - MixNodeBondAnnotated, NodeAnnotation, NodePerformance, NymNodeDescription, RoutingScore, -}; -use nym_contracts_common::NaiveFloat; -use nym_mixnet_contract_common::{Interval, NodeId, VersionScoreFormulaParams}; -use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; -use nym_topology::CachedEpochRewardedSet; -use std::collections::HashMap; -use tracing::trace; - -pub(super) async fn get_mixnode_reliability_from_storage( - storage: &NymApiStorage, - mix_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_mixnode_reliability_in_the_last_24hrs( - mix_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -pub(super) async fn get_gateway_reliability_from_storage( - storage: &NymApiStorage, - node_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_gateway_reliability_in_the_last_24hrs( - node_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -pub(super) async fn get_node_reliability_from_storage( - storage: &NymApiStorage, - node_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_node_reliability_in_the_last_24hrs( - node_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -async fn get_routing_score( - storage: &NymApiStorage, - node_id: NodeId, - typ: DescribedNodeType, - epoch: Interval, -) -> RoutingScore { - let maybe_reliability = match typ { - DescribedNodeType::LegacyMixnode => { - get_mixnode_reliability_from_storage(storage, node_id, epoch).await - } - DescribedNodeType::LegacyGateway => { - get_gateway_reliability_from_storage(storage, node_id, epoch).await - } - DescribedNodeType::NymNode => { - get_node_reliability_from_storage(storage, node_id, epoch).await - } - }; - // reliability: 0-100 - // score: 0-1 - let reliability = maybe_reliability.unwrap_or_default(); - let score = reliability / 100.; - - trace!("reliability for {node_id}: {maybe_reliability:?}. routing score: {score}"); - RoutingScore::new(score as f64) -} - -fn versions_behind_factor_to_config_score( - versions_behind: u32, - params: VersionScoreFormulaParams, -) -> f64 { - let penalty = params.penalty.naive_to_f64(); - let scaling = params.penalty_scaling.naive_to_f64(); - - // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) - penalty.powf((versions_behind as f64).powf(scaling)) -} - -fn calculate_config_score( - config_score_data: &ConfigScoreData, - described_data: Option<&NymNodeDescription>, -) -> ConfigScore { - let Some(described) = described_data else { - return ConfigScore::unavailable(); - }; - - let node_version = &described.description.build_information.build_version; - let Ok(reported_semver) = node_version.parse::<semver::Version>() else { - return ConfigScore::bad_semver(); - }; - let versions_behind = config_score_data - .config_score_params - .version_weights - .versions_behind_factor( - &reported_semver, - &config_score_data.nym_node_version_history, - ); - - let runs_nym_node = described.description.build_information.binary_name == "nym-node"; - let accepted_terms_and_conditions = described - .description - .auxiliary_details - .accepted_operator_terms_and_conditions; - - let version_score = if !runs_nym_node || !accepted_terms_and_conditions { - 0. - } else { - versions_behind_factor_to_config_score( - versions_behind, - config_score_data - .config_score_params - .version_score_formula_params, - ) - }; - - ConfigScore::new( - version_score, - versions_behind, - accepted_terms_and_conditions, - runs_nym_node, - ) -} - -// TODO: this might have to be moved to a different file if other places also rely on this functionality -fn get_rewarded_set_status( - rewarded_set: &CachedEpochRewardedSet, - node_id: NodeId, -) -> RewardedSetStatus { - if rewarded_set.is_standby(&node_id) { - RewardedSetStatus::Standby - } else if rewarded_set.is_active_mixnode(&node_id) { - RewardedSetStatus::Active - } else { - RewardedSetStatus::Inactive - } -} - -#[deprecated] -pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( - storage: &NymApiStorage, - mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, - interval_reward_params: RewardingParams, - current_interval: Interval, - rewarded_set: &CachedEpochRewardedSet, -) -> HashMap<NodeId, MixNodeBondAnnotated> { - let mut annotated = HashMap::new(); - for mixnode in mixnodes { - let stake_saturation = mixnode - .rewarding_details - .bond_saturation(&interval_reward_params); - - let uncapped_stake_saturation = mixnode - .rewarding_details - .uncapped_bond_saturation(&interval_reward_params); - - let rewarded_set_status = get_rewarded_set_status(rewarded_set, mixnode.mix_id()); - - // If the performance can't be obtained, because the nym-api was not started with - // the monitoring (and hence, storage), then reward estimates will be all zero - let performance = - get_mixnode_reliability_from_storage(storage, mixnode.mix_id(), current_interval) - .await - .map(Uptime::new) - .map(Into::into) - .unwrap_or_default(); - - let reward_estimate = compute_reward_estimate( - &mixnode, - performance, - rewarded_set_status, - interval_reward_params, - current_interval, - ); - - let node_performance = storage - .construct_mixnode_report(mixnode.mix_id()) - .await - .map(NodePerformance::from) - .ok() - .unwrap_or_default(); - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) - else { - continue; - }; - - let (estimated_operator_apy, estimated_delegators_apy) = - compute_apy_from_reward(&mixnode, reward_estimate, current_interval); - - annotated.insert( - mixnode.mix_id(), - MixNodeBondAnnotated { - // all legacy nodes are always blacklisted - blacklisted: true, - mixnode_details: mixnode, - stake_saturation, - uncapped_stake_saturation, - performance, - node_performance, - estimated_operator_apy, - estimated_delegators_apy, - ip_addresses, - }, - ); - } - annotated -} - -#[deprecated] -pub(crate) async fn annotate_legacy_gateways_with_details( - storage: &NymApiStorage, - gateway_bonds: Vec<LegacyGatewayBondWithId>, - current_interval: Interval, -) -> HashMap<NodeId, GatewayBondAnnotated> { - let mut annotated = HashMap::new(); - for gateway_bond in gateway_bonds { - let performance = - get_gateway_reliability_from_storage(storage, gateway_bond.node_id, current_interval) - .await - .map(Uptime::new) - .map(Into::into) - .unwrap_or_default(); - - let node_performance = storage - .construct_gateway_report(gateway_bond.node_id) - .await - .map(NodePerformance::from) - .ok() - .unwrap_or_default(); - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) - else { - continue; - }; - - annotated.insert( - gateway_bond.node_id, - GatewayBondAnnotated { - // all legacy nodes are always blacklisted - blacklisted: true, - gateway_bond, - self_described: None, - performance, - node_performance, - ip_addresses, - }, - ); - } - annotated -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn produce_node_annotations( - storage: &NymApiStorage, - config_score_data: &ConfigScoreData, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], - nym_nodes: &[NymNodeDetails], - rewarded_set: &CachedEpochRewardedSet, - current_interval: Interval, - described_nodes: &DescribedNodes, -) -> HashMap<NodeId, NodeAnnotation> { - let mut annotations = HashMap::new(); - - for legacy_mix in legacy_mixnodes { - let node_id = legacy_mix.mix_id(); - - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::LegacyMixnode, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_mix.mix_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for legacy_gateway in legacy_gateways { - let node_id = legacy_gateway.node_id; - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::LegacyGateway, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_gateway.node_id, - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for nym_node in nym_nodes { - let node_id = nym_node.node_id(); - let routing_score = get_routing_score( - storage, - node_id, - DescribedNodeType::NymNode, - current_interval, - ) - .await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - nym_node.node_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - annotations -} diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index 0666b84594..5232155625 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -2,15 +2,27 @@ // SPDX-License-Identifier: GPL-3.0-only use super::NodeStatusCache; +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; use crate::node_describe_cache::cache::DescribedNodes; -use crate::node_status_api::cache::node_sets::produce_node_annotations; +use crate::node_performance::provider::{NodePerformanceProvider, NodesRoutingScores}; +use crate::node_status_api::cache::config_score::calculate_config_score; +use crate::node_status_api::models::Uptime; use crate::support::caching::cache::SharedCache; +use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use crate::{ - node_status_api::cache::NodeStatusCacheError, nym_contract_cache::cache::NymContractCache, - storage::NymApiStorage, support::caching::CacheNotification, + mixnet_contract_cache::cache::MixnetContractCache, + node_status_api::cache::NodeStatusCacheError, support::caching::CacheNotification, }; use ::time::OffsetDateTime; +use cosmwasm_std::Decimal; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ + DetailedNodePerformance, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, + NodePerformance, +}; +use nym_mixnet_contract_common::{NodeId, NymNodeDetails, RewardingParams}; use nym_task::TaskClient; +use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; use std::time::Duration; use tokio::sync::watch; @@ -24,31 +36,32 @@ pub struct NodeStatusCacheRefresher { fallback_caching_interval: Duration, // Sources for when refreshing data - contract_cache: NymContractCache, + mixnet_contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, - contract_cache_listener: watch::Receiver<CacheNotification>, + mixnet_contract_cache_listener: watch::Receiver<CacheNotification>, describe_cache_listener: watch::Receiver<CacheNotification>, - storage: NymApiStorage, + + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, } impl NodeStatusCacheRefresher { pub(crate) fn new( cache: NodeStatusCache, fallback_caching_interval: Duration, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, contract_cache_listener: watch::Receiver<CacheNotification>, describe_cache_listener: watch::Receiver<CacheNotification>, - storage: NymApiStorage, + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, ) -> Self { Self { cache, fallback_caching_interval, - contract_cache, + mixnet_contract_cache: contract_cache, described_cache, - contract_cache_listener, + mixnet_contract_cache_listener: contract_cache_listener, describe_cache_listener, - storage, + performance_provider, } } @@ -63,7 +76,7 @@ impl NodeStatusCacheRefresher { trace!("NodeStatusCacheRefresher: Received shutdown"); } // Update node status cache when the contract cache / describe cache is updated - Ok(_) = self.contract_cache_listener.changed() => { + Ok(_) = self.mixnet_contract_cache_listener.changed() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), _ = shutdown.recv() => { @@ -95,7 +108,8 @@ impl NodeStatusCacheRefresher { } fn caches_available(&self) -> bool { - let contract_cache = *self.contract_cache_listener.borrow() != CacheNotification::Start; + let contract_cache = + *self.mixnet_contract_cache_listener.borrow() != CacheNotification::Start; let describe_cache = *self.describe_cache_listener.borrow() != CacheNotification::Start; let available = contract_cache && describe_cache; @@ -130,19 +144,201 @@ impl NodeStatusCacheRefresher { fallback_interval.reset(); } + #[allow(clippy::too_many_arguments)] + pub(crate) async fn produce_node_annotations( + &self, + config_score_data: &ConfigScoreData, + routing_scores: &NodesRoutingScores, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + rewarded_set: &CachedEpochRewardedSet, + described_nodes: &DescribedNodes, + ) -> HashMap<NodeId, NodeAnnotation> { + let mut annotations = HashMap::new(); + + for legacy_mix in legacy_mixnodes { + let node_id = legacy_mix.mix_id(); + let routing_score = routing_scores.get_or_log(node_id); + + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + legacy_mix.mix_id(), + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + for legacy_gateway in legacy_gateways { + let node_id = legacy_gateway.node_id; + let routing_score = routing_scores.get_or_log(node_id); + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + legacy_gateway.node_id, + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + for nym_node in nym_nodes { + let node_id = nym_node.node_id(); + let routing_score = routing_scores.get_or_log(node_id); + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + nym_node.node_id(), + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + annotations + } + + #[deprecated] + pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( + &self, + mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, + routing_scores: &NodesRoutingScores, + interval_reward_params: RewardingParams, + ) -> HashMap<NodeId, MixNodeBondAnnotated> { + let mut annotated = HashMap::new(); + for mixnode in mixnodes { + let stake_saturation = mixnode + .rewarding_details + .bond_saturation(&interval_reward_params); + + let uncapped_stake_saturation = mixnode + .rewarding_details + .uncapped_bond_saturation(&interval_reward_params); + + let score = routing_scores.get_or_log(mixnode.mix_id()); + let legacy_report = NodePerformance { + most_recent: score.legacy_performance(), + last_hour: score.legacy_performance(), + last_24h: score.legacy_performance(), + }; + + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) + else { + continue; + }; + + // legacy node will never get rewarded + let estimated_operator_apy = Decimal::zero(); + let estimated_delegators_apy = Decimal::zero(); + + annotated.insert( + mixnode.mix_id(), + MixNodeBondAnnotated { + // all legacy nodes are always blacklisted + blacklisted: true, + mixnode_details: mixnode, + stake_saturation, + uncapped_stake_saturation, + performance: score.legacy_performance(), + node_performance: legacy_report, + estimated_operator_apy, + estimated_delegators_apy, + ip_addresses, + }, + ); + } + annotated + } + + #[deprecated] + pub(crate) async fn annotate_legacy_gateways_with_details( + &self, + gateway_bonds: Vec<LegacyGatewayBondWithId>, + routing_scores: &NodesRoutingScores, + ) -> HashMap<NodeId, GatewayBondAnnotated> { + let mut annotated = HashMap::new(); + for gateway_bond in gateway_bonds { + let score = routing_scores.get_or_log(gateway_bond.node_id); + let legacy_report = NodePerformance { + most_recent: score.legacy_performance(), + last_hour: score.legacy_performance(), + last_24h: score.legacy_performance(), + }; + + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) + else { + continue; + }; + + annotated.insert( + gateway_bond.node_id, + GatewayBondAnnotated { + // all legacy nodes are always blacklisted + blacklisted: true, + gateway_bond, + self_described: None, + performance: score.legacy_performance(), + node_performance: legacy_report, + ip_addresses, + }, + ); + } + annotated + } + /// Refreshes the node status cache by fetching the latest data from the contract cache #[allow(deprecated)] async fn refresh(&self) -> Result<(), NodeStatusCacheError> { info!("Updating node status cache"); // Fetch contract cache data to work with - let mixnode_details = self.contract_cache.legacy_mixnodes_all().await; - let interval_reward_params = self.contract_cache.interval_reward_params().await?; - let current_interval = self.contract_cache.current_interval().await?; - let rewarded_set = self.contract_cache.rewarded_set_owned().await?; - let gateway_bonds = self.contract_cache.legacy_gateways_all().await; - let nym_nodes = self.contract_cache.nym_nodes().await; - let config_score_data = self.contract_cache.maybe_config_score_data().await?; + let mixnode_details = self.mixnet_contract_cache.legacy_mixnodes_all().await; + let interval_reward_params = self.mixnet_contract_cache.interval_reward_params().await?; + let current_interval = self.mixnet_contract_cache.current_interval().await?; + let rewarded_set = self.mixnet_contract_cache.rewarded_set_owned().await?; + let gateway_bonds = self.mixnet_contract_cache.legacy_gateways_all().await; + let nym_nodes = self.mixnet_contract_cache.nym_nodes().await; + let config_score_data = self.mixnet_contract_cache.maybe_config_score_data().await?; // Compute inclusion probabilities // (all legacy mixnodes have 0% chance of being selected) @@ -157,37 +353,48 @@ impl NodeStatusCacheRefresher { legacy_gateway_mapping.insert(gateway.identity().clone(), gateway.node_id); } - // Create annotated data - let node_annotations = produce_node_annotations( - &self.storage, - &config_score_data, - &mixnode_details, - &gateway_bonds, - &nym_nodes, - &rewarded_set, - current_interval, - &described, - ) - .await; + let all_ids = mixnode_details + .iter() + .map(|m| m.bond_information.mix_id) + .chain( + gateway_bonds + .iter() + .map(|g| g.node_id) + .chain(nym_nodes.iter().map(|n| n.bond_information.node_id)), + ) + .collect::<Vec<_>>(); - let mixnodes_annotated = - crate::node_status_api::cache::node_sets::annotate_legacy_mixnodes_nodes_with_details( - &self.storage, - mixnode_details, - interval_reward_params, - current_interval, + // note: any internal errors imply failures for that node in particular + let routing_scores = self + .performance_provider + .get_batch_node_scores(all_ids, current_interval.current_epoch_absolute_id()) + .await?; + + // Create annotated data + let node_annotations = self + .produce_node_annotations( + &config_score_data, + &routing_scores, + &mixnode_details, + &gateway_bonds, + &nym_nodes, &rewarded_set, + &described, ) .await; - let gateways_annotated = - crate::node_status_api::cache::node_sets::annotate_legacy_gateways_with_details( - &self.storage, - gateway_bonds, - current_interval, + let mixnodes_annotated = self + .annotate_legacy_mixnodes_nodes_with_details( + mixnode_details, + &routing_scores, + interval_reward_params, ) .await; + let gateways_annotated = self + .annotate_legacy_gateways_with_details(gateway_bonds, &routing_scores) + .await; + // Update the cache self.cache .update( diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 9e4fcba62f..91076e5963 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -1,12 +1,10 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use super::reward_estimate::compute_reward_estimate; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::storage::NymApiStorage; use crate::support::caching::Cache; -use crate::{NodeStatusCache, NymContractCache}; -use cosmwasm_std::Decimal; +use crate::{MixnetContractCache, NodeStatusCache}; use nym_api_requests::models::{ ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, @@ -14,6 +12,7 @@ use nym_api_requests::models::{ MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; +use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::NodeId; pub(crate) enum RewardedSetStatus { @@ -90,7 +89,7 @@ pub(crate) async fn _gateway_report( pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, identity: &str, ) -> AxumResult<GatewayUptimeHistoryResponse> { let history = storage @@ -144,7 +143,7 @@ pub(crate) async fn _mixnode_report( pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<MixnodeUptimeHistoryResponse> { let history = storage @@ -179,7 +178,7 @@ pub(crate) async fn _mixnode_core_status_count( } pub(crate) async fn _get_mixnode_status( - cache: &NymContractCache, + cache: &MixnetContractCache, mix_id: NodeId, ) -> MixnodeStatusResponse { MixnodeStatusResponse { @@ -189,14 +188,15 @@ pub(crate) async fn _get_mixnode_status( pub(crate) async fn _get_mixnode_reward_estimation( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<RewardEstimationResponse> { - let status = contract_cache.mixnode_status(mix_id).await; - let mixnode = status_cache + let _ = status_cache .mixnode_annotated(mix_id) .await .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + // legacy mixnode will never get any rewards + let reward_estimation = RewardEstimate::zero(); let reward_params = contract_cache.interval_reward_params().await?; let current_interval = contract_cache.current_interval().await?; @@ -205,14 +205,6 @@ pub(crate) async fn _get_mixnode_reward_estimation( // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) let as_at = contract_cache.cache_timestamp().await; - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); - Ok(RewardEstimationResponse { estimation: reward_estimation, reward_params, @@ -222,16 +214,18 @@ pub(crate) async fn _get_mixnode_reward_estimation( } pub(crate) async fn _compute_mixnode_reward_estimation( - user_reward_param: &ComputeRewardEstParam, + _: &ComputeRewardEstParam, status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<RewardEstimationResponse> { - let mut mixnode = status_cache + let _ = status_cache .mixnode_annotated(mix_id) .await .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + let reward_estimation = RewardEstimate::zero(); + let reward_params = contract_cache.interval_reward_params().await?; let current_interval = contract_cache.current_interval().await?; @@ -239,60 +233,6 @@ pub(crate) async fn _compute_mixnode_reward_estimation( // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) let as_at = contract_cache.cache_timestamp().await; - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => RewardedSetStatus::Active, - Some(false) => RewardedSetStatus::Standby, - None => { - let actual_status = contract_cache.mixnode_status(mix_id).await; - actual_status.into() - } - }; - - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); - } - - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost.clone(); - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(AxumErrorResponse::unprocessable_entity( - "Pledge plus delegation too large", - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - Ok(RewardEstimationResponse { estimation: reward_estimation, reward_params, @@ -303,7 +243,7 @@ pub(crate) async fn _compute_mixnode_reward_estimation( pub(crate) async fn _get_mixnode_stake_saturation( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<StakeSaturationResponse> { let mixnode = status_cache @@ -411,7 +351,7 @@ pub(crate) async fn _get_mixnodes_detailed_unfiltered( pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, ) -> Vec<MixNodeBondAnnotated> { let Some(rewarded_set) = contract_cache.rewarded_set().await else { return Vec::new(); @@ -429,7 +369,7 @@ pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( pub(crate) async fn _get_active_set_legacy_mixnodes_detailed( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, ) -> Vec<MixNodeBondAnnotated> { let Some(rewarded_set) = contract_cache.rewarded_set().await else { return Vec::new(); diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 0b2a0db478..1b3d97f839 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -3,11 +3,12 @@ use self::cache::refresher::NodeStatusCacheRefresher; use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::NodePerformanceProvider; use crate::support::caching::cache::SharedCache; use crate::support::config; use crate::{ - nym_contract_cache::cache::NymContractCache, - support::{self, storage}, + mixnet_contract_cache::cache::MixnetContractCache, + support::{self}, }; pub(crate) use cache::NodeStatusCache; use nym_task::TaskManager; @@ -18,7 +19,6 @@ pub(crate) mod cache; pub(crate) mod handlers; pub(crate) mod helpers; pub(crate) mod models; -pub(crate) mod reward_estimate; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -33,10 +33,10 @@ pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); #[allow(clippy::too_many_arguments)] pub(crate) fn start_cache_refresh( config: &config::NodeStatusAPI, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache_state: &MixnetContractCache, described_cache: &SharedCache<DescribedNodes>, node_status_cache_state: &NodeStatusCache, - storage: storage::NymApiStorage, + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, nym_contract_cache_listener: watch::Receiver<support::caching::CacheNotification>, described_cache_cache_listener: watch::Receiver<support::caching::CacheNotification>, shutdown: &TaskManager, @@ -48,7 +48,7 @@ pub(crate) fn start_cache_refresh( described_cache.clone(), nym_contract_cache_listener, described_cache_cache_listener, - storage, + performance_provider, ); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await }); diff --git a/nym-api/src/node_status_api/reward_estimate.rs b/nym-api/src/node_status_api/reward_estimate.rs deleted file mode 100644 index 6b1798227d..0000000000 --- a/nym-api/src/node_status_api/reward_estimate.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::helpers::RewardedSetStatus; -use cosmwasm_std::Decimal; -use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; -use nym_mixnet_contract_common::reward_params::{ - NodeRewardingParameters, Performance, RewardingParams, -}; -use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::Interval; - -fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { - if pledge_amount.is_zero() { - return Decimal::zero(); - } - let hundred = Decimal::from_ratio(100u32, 1u32); - - epochs_in_year * hundred * reward / pledge_amount -} - -pub fn compute_reward_estimate( - mixnode: &LegacyMixNodeDetailsWithLayer, - performance: Performance, - rewarded_set_status: RewardedSetStatus, - rewarding_params: RewardingParams, - interval: Interval, -) -> RewardEstimate { - if mixnode.is_unbonding() { - return Default::default(); - } - - if performance.is_zero() { - return Default::default(); - } - - let is_active = match rewarded_set_status { - RewardedSetStatus::Active => true, - RewardedSetStatus::Standby => false, - RewardedSetStatus::Inactive => return Default::default(), - }; - - let work_factor = if is_active { - rewarding_params.active_node_work() - } else { - rewarding_params.standby_node_work() - }; - - let node_reward_params = NodeRewardingParameters { - performance, - work_factor, - }; - let node_reward = mixnode - .rewarding_details - .node_reward(&rewarding_params, node_reward_params); - - let node_cost = mixnode - .rewarding_details - .cost_params - .epoch_operating_cost(interval.epochs_in_interval()) - * performance; - - let reward_distribution = mixnode.rewarding_details.determine_reward_split( - node_reward, - performance, - interval.epochs_in_interval(), - ); - - RewardEstimate { - total_node_reward: node_reward, - operator: reward_distribution.operator, - delegates: reward_distribution.delegates, - operating_cost: node_cost, - } -} - -pub fn compute_apy_from_reward( - mixnode: &LegacyMixNodeDetailsWithLayer, - reward_estimate: RewardEstimate, - interval: Interval, -) -> (Decimal, Decimal) { - let epochs_in_year = Decimal::from_ratio(interval.epoch_length_secs(), 3600u64 * 24 * 365); - - let operator = mixnode.rewarding_details.operator; - let total_delegations = mixnode.rewarding_details.delegates; - let estimated_operator_apy = compute_apy(epochs_in_year, reward_estimate.operator, operator); - let estimated_delegators_apy = - compute_apy(epochs_in_year, reward_estimate.delegates, total_delegations); - (estimated_operator_apy, estimated_delegators_apy) -} diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 2f782f6098..4923b70a9f 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -32,7 +32,44 @@ impl<T> SharedCache<T> { SharedCache::default() } - pub(crate) async fn try_update(&self, value: impl Into<T>, typ: &str) -> Result<(), T> { + pub(crate) fn new_with_value(value: T) -> Self { + SharedCache(Arc::new(RwLock::new(CachedItem { + inner: Some(Cache::new(value)), + }))) + } + + pub(crate) async fn try_update_value<S>( + &self, + update: S, + update_fn: impl Fn(&mut T, S), + typ: &str, + ) -> Result<(), S> + where + S: Into<T>, + { + let update_value = update; + let mut guard = match tokio::time::timeout(Duration::from_millis(200), self.0.write()).await + { + Ok(guard) => guard, + Err(_) => { + debug!("failed to obtain write permit for {typ} cache"); + return Err(update_value); + } + }; + + if let Some(ref mut existing) = guard.inner { + existing.update(update_value, update_fn); + } else { + guard.inner = Some(Cache::new(update_value.into())) + }; + Ok(()) + } + + pub(crate) async fn try_overwrite_old_value( + &self, + value: impl Into<T>, + typ: &str, + ) -> Result<(), T> { let value = value.into(); let mut guard = match tokio::time::timeout(Duration::from_millis(200), self.0.write()).await { @@ -107,6 +144,19 @@ impl<T> From<Cache<T>> for CachedItem<T> { } } +// specialised variant of `Cache` for holding maps of values that allow updates to individual entries + +/* + pub(crate) fn partial_update<F>(&mut self, partial_value: impl Into<S>, update_fn: F) + where + F: FnOnce(&mut T, S), + { + update_fn(&mut self.value, partial_value.into()); + self.as_at = OffsetDateTime::now_utc() + } + +*/ + // don't use this directly! // opt for SharedCache<T> instead pub struct Cache<T> { @@ -166,6 +216,11 @@ impl<T> Cache<T> { } } + pub(crate) fn update<S>(&mut self, update: S, update_fn: impl Fn(&mut T, S)) { + update_fn(&mut self.value, update); + self.as_at = OffsetDateTime::now_utc(); + } + // ugh. I hate to expose it, but it'd have broken pre-existing code pub(crate) fn unchecked_update(&mut self, value: impl Into<T>) { self.value = value.into(); diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index 05e6a3a3d4..b26a918f73 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::{watch, Notify}; use tokio::time::interval; -use tracing::{error, info, trace, warn}; +use tracing::{debug, error, info, trace, warn}; pub(crate) type CacheUpdateWatcher = watch::Receiver<CacheNotification>; @@ -28,13 +28,24 @@ impl Default for RefreshRequester { } } -pub struct CacheRefresher<T, E> { +/// Explanation on generics: +/// the internal SharedCache<T> can be updated in two ways +/// by default CacheItemProvider will just provide a T and the internal values will be swapped +/// however, an alternative is to make it provide another value of type S with an explicit update closure +/// this way the cache will be updated with a custom method mutating the existing value +/// the reason for this is to allow partial updates of maps, where we might not want to retrieve +/// the entire value, and we might want to just insert a new entry +pub struct CacheRefresher<T, E, S = T> { name: String, refreshing_interval: Duration, refresh_notification_sender: watch::Sender<CacheNotification>, + // it's not really THAT complex... it's just a boxed function + #[allow(clippy::type_complexity)] + update_fn: Option<Box<dyn Fn(&mut T, S) + Send + Sync>>, + // TODO: the Send + Sync bounds are only required for the `start` method. could we maybe make it less restrictive? - provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, shared_cache: SharedCache<T>, refresh_requester: RefreshRequester, } @@ -46,15 +57,16 @@ pub(crate) trait CacheItemProvider { async fn wait_until_ready(&self) {} - async fn try_refresh(&self) -> Result<Self::Item, Self::Error>; + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error>; } -impl<T, E> CacheRefresher<T, E> +impl<T, E, S> CacheRefresher<T, E, S> where E: std::error::Error, + S: Into<T>, { - pub(crate) fn new( - item_provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + pub(crate) fn new_boxed( + item_provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, refreshing_interval: Duration, ) -> Self { let (refresh_notification_sender, _) = watch::channel(CacheNotification::Start); @@ -63,14 +75,22 @@ where name: "GenericCacheRefresher".to_string(), refreshing_interval, refresh_notification_sender, + update_fn: None, provider: item_provider, shared_cache: SharedCache::new(), refresh_requester: Default::default(), } } + pub(crate) fn new<P>(item_provider: P, refreshing_interval: Duration) -> Self + where + P: CacheItemProvider<Error = E, Item = S> + Send + Sync + 'static, + { + Self::new_boxed(Box::new(item_provider), refreshing_interval) + } + pub(crate) fn new_with_initial_value( - item_provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + item_provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, refreshing_interval: Duration, shared_cache: SharedCache<T>, ) -> Self { @@ -80,12 +100,22 @@ where name: "GenericCacheRefresher".to_string(), refreshing_interval, refresh_notification_sender, + update_fn: None, provider: item_provider, shared_cache, refresh_requester: Default::default(), } } + #[must_use] + pub(crate) fn with_update_fn( + mut self, + update_fn: impl Fn(&mut T, S) + Send + Sync + 'static, + ) -> Self { + self.update_fn = Some(Box::new(update_fn)); + self + } + #[must_use] pub(crate) fn named(mut self, name: impl Into<String>) -> Self { self.name = name.into(); @@ -105,22 +135,39 @@ where self.shared_cache.clone() } - // TODO: in the future offer 2 options of refreshing cache. either provide `T` directly - // or via `FnMut(&mut T)` closure - async fn do_refresh_cache(&self) { - let mut updated_items = match self.provider.try_refresh().await { - Err(err) => { - error!("{}: failed to refresh the cache: {err}", self.name); - return; - } - Ok(items) => items, - }; - + async fn update_cache(&self, mut update: S, update_fn: impl Fn(&mut T, S)) { let mut failures = 0; + loop { match self .shared_cache - .try_update(updated_items, &self.name) + .try_update_value(update, &update_fn, &self.name) + .await + { + Ok(_) => break, + Err(returned) => { + failures += 1; + update = returned + } + }; + if failures % 10 == 0 { + warn!( + "failed to obtain write permit for {} cache {failures} times in a row!", + self.name + ); + } + + tokio::time::sleep(Duration::from_secs_f32(0.5)).await + } + } + + async fn overwrite_cache(&self, mut updated_items: T) { + let mut failures = 0; + + loop { + match self + .shared_cache + .try_overwrite_old_value(updated_items, &self.name) .await { Ok(_) => break, @@ -138,6 +185,26 @@ where tokio::time::sleep(Duration::from_secs_f32(0.5)).await } + } + + async fn do_refresh_cache(&mut self) { + let updated_items = match self.provider.try_refresh().await { + Err(err) => { + error!("{}: failed to refresh the cache: {err}", self.name); + return; + } + Ok(Some(items)) => items, + Ok(None) => { + debug!("no updates for {} cache this iteration", self.name); + return; + } + }; + + if let Some(update_fn) = self.update_fn.as_ref() { + self.update_cache(updated_items, update_fn).await; + } else { + self.overwrite_cache(updated_items.into()).await; + } if !self.refresh_notification_sender.is_closed() && self @@ -149,7 +216,7 @@ where } } - pub async fn refresh(&self, task_client: &mut TaskClient) { + pub async fn refresh(&mut self, task_client: &mut TaskClient) { info!("{}: refreshing cache state", self.name); tokio::select! { @@ -161,7 +228,7 @@ where } } - pub async fn run(&self, mut task_client: TaskClient) { + pub async fn run(&mut self, mut task_client: TaskClient) { self.provider.wait_until_ready().await; let mut refresh_interval = interval(self.refreshing_interval); @@ -183,10 +250,11 @@ where } } - pub fn start(self, task_client: TaskClient) + pub fn start(mut self, task_client: TaskClient) where T: Send + Sync + 'static, E: Send + Sync + 'static, + S: Send + Sync + 'static, { tokio::spawn(async move { self.run(task_client).await }); } @@ -195,6 +263,7 @@ where where T: Send + Sync + 'static, E: Send + Sync + 'static, + S: Send + Sync + 'static, { let receiver = self.update_watcher(); self.start(task_client); diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 741b2d3f90..bed8c6efaf 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::client::Client; use crate::ecash::comm::QueryCommunicationChannel; use crate::ecash::dkg::controller::keys::{ @@ -11,17 +10,21 @@ use crate::ecash::dkg::controller::DkgController; use crate::ecash::state::EcashState; use crate::epoch_operations::EpochAdvancer; use crate::key_rotation::KeyRotationController; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::contract_provider::ContractPerformanceProvider; +use crate::node_performance::provider::legacy_storage_provider::LegacyStoragePerformanceProvider; +use crate::node_performance::provider::NodePerformanceProvider; use crate::node_status_api::handlers::unstable; use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::{ApiStatusState, SignerState}; use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; use crate::support::config::{Config, DEFAULT_CHAIN_STATUS_CACHE_TTL}; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::http::state::AppState; use crate::support::http::{RouterBuilder, ShutdownHandles, TASK_MANAGER_TIMEOUT_S}; @@ -30,8 +33,8 @@ use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::m use crate::support::storage::NymApiStorage; use crate::unstable_routes::v1::account::cache::AddressInfoCache; use crate::{ - circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache, - node_status_api, nym_contract_cache, + ecash, epoch_operations, mixnet_contract_cache, network_monitor, node_describe_cache, + node_performance, node_status_api, }; use anyhow::{bail, Context}; use nym_config::defaults::NymNetworkDetails; @@ -146,10 +149,9 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); - let nym_contract_cache_state = NymContractCache::new(); + let mixnet_contract_cache_state = MixnetContractCache::new(); let node_status_cache_state = NodeStatusCache::new(); let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); let described_nodes_cache = SharedCache::<DescribedNodes>::new(); let node_info_cache = unstable::NodeInfoCache::default(); @@ -208,16 +210,14 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> config.address_cache.time_to_live, config.address_cache.capacity, ), - forced_refresh: ForcedRefresh::new( - config.topology_cacher.debug.node_describe_allow_illegal_ips, - ), - nym_contract_cache: nym_contract_cache_state.clone(), + forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips), + mixnet_contract_cache: mixnet_contract_cache_state.clone(), node_status_cache: node_status_cache_state.clone(), - circulating_supply_cache: circulating_supply_cache.clone(), storage: storage.clone(), described_nodes_cache: described_nodes_cache.clone(), - network_details, + network_details: network_details.clone(), node_info_cache, + contract_info_cache: ContractDetailsCache::new(config.contracts_info_cache.time_to_live), api_status: ApiStatusState::new(signer_information), ecash_state: Arc::new(ecash_state), }); @@ -227,8 +227,8 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); // let cache = refresher.get_shared_cache(); let describe_cache_refresher = node_describe_cache::provider::new_provider_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), + &config.describe_cache, + mixnet_contract_cache_state.clone(), described_nodes_cache.clone(), ) .named("node-self-described-data-refresher"); @@ -238,31 +238,54 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> let describe_cache_watcher = describe_cache_refresher .start_with_watcher(task_manager.subscribe_named("node-self-described-data-refresher")); + let performance_provider = if config.performance_provider.use_performance_contract_data { + if network_details + .network + .contracts + .performance_contract_address + .is_none() + { + bail!("can't use performance contract data without setting the address of the contract") + } + + let performance_contract_cache = node_performance::contract_cache::start_cache_refresher( + &config.performance_provider, + nyxd_client.clone(), + mixnet_contract_cache_state.clone(), + &task_manager, + ) + .await?; + let provider = ContractPerformanceProvider::new( + &config.performance_provider, + performance_contract_cache, + ); + Box::new(provider) as Box<dyn NodePerformanceProvider + Send + Sync> + } else { + Box::new(LegacyStoragePerformanceProvider::new( + storage.clone(), + mixnet_contract_cache_state.clone(), + )) + }; + // start all the caches first - let contract_cache_refresher = nym_contract_cache::build_refresher( - &config.node_status_api, - &nym_contract_cache_state.clone(), + let mixnet_contract_cache_refresher = mixnet_contract_cache::build_refresher( + &config.mixnet_contract_cache, + &mixnet_contract_cache_state.clone(), nyxd_client.clone(), ); let contract_cache_watcher = - contract_cache_refresher.start_with_watcher(task_manager.subscribe()); + mixnet_contract_cache_refresher.start_with_watcher(task_manager.subscribe()); node_status_api::start_cache_refresh( &config.node_status_api, - &nym_contract_cache_state, + &mixnet_contract_cache_state, &described_nodes_cache, &node_status_cache_state, - storage.clone(), + performance_provider, contract_cache_watcher.clone(), describe_cache_watcher, &task_manager, ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - &circulating_supply_cache, - &task_manager, - ); // start dkg task if config.ecash_signer.enabled { @@ -279,12 +302,15 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> )?; } + let has_performance_data = + config.network_monitor.enabled || config.performance_provider.use_performance_contract_data; + // and then only start the uptime updater (and the monitor itself, duh) // if the monitoring is enabled if config.network_monitor.enabled { network_monitor::start::<SphinxMessageReceiver>( config, - &nym_contract_cache_state, + &mixnet_contract_cache_state, described_nodes_cache.clone(), node_status_cache_state.clone(), &storage, @@ -294,19 +320,19 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> .await; HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); + } - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - EpochAdvancer::start( - nyxd_client, - &nym_contract_cache_state, - &node_status_cache_state, - described_nodes_cache.clone(), - &storage, - &task_manager, - ); - } + // start 'rewarding' if its enabled and there exists source for performance data + if config.rewarding.enabled && has_performance_data { + epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; + EpochAdvancer::start( + nyxd_client, + &mixnet_contract_cache_state, + &node_status_cache_state, + described_nodes_cache.clone(), + &storage, + &task_manager, + ); } // finally start a background task watching the contract changes and requesting @@ -314,7 +340,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles> KeyRotationController::new( describe_cache_refresh_requester, contract_cache_watcher, - nym_contract_cache_state, + mixnet_contract_cache_state, ) .start(task_manager.subscribe_named("KeyRotationController")); diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index cf2161eaa2..8be4ee0bc0 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -50,9 +50,11 @@ const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1; const DEFAULT_ROUTE_TEST_PACKETS: usize = 1000; const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3; -const DEFAULT_TOPOLOGY_CACHE_INTERVAL: Duration = Duration::from_secs(30); -const DEFAULT_NODE_STATUS_CACHE_INTERVAL: Duration = Duration::from_secs(120); -const DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL: Duration = Duration::from_secs(3600); +const DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(305); +const DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(150); +const DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL: Duration = Duration::from_secs(150); +const DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS: u32 = 12; +const DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS: usize = 25; pub(crate) const DEFAULT_ADDRESS_CACHE_TTL: Duration = Duration::from_secs(60 * 15); pub(crate) const DEFAULT_ADDRESS_CACHE_CAPACITY: u64 = 1000; @@ -63,6 +65,10 @@ pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50; // TODO: make it configurable pub(crate) const DEFAULT_CHAIN_STATUS_CACHE_TTL: Duration = Duration::from_secs(60); +// contract info is changed very infrequently (essentially once per release cycle) +// so this default is more than enough +pub(crate) const DEFAULT_CONTRACT_DETAILS_CACHE_TTL: Duration = Duration::from_secs(60 * 60); + const DEFAULT_MONITOR_THRESHOLD: u8 = 60; const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50; const DEFAULT_MIN_GATEWAY_RELIABILITY: u8 = 20; @@ -101,14 +107,22 @@ pub struct Config { pub base: Base, + #[serde(default)] + pub performance_provider: PerformanceProvider, + // TODO: perhaps introduce separate 'path finder' field for all the paths and directories like we have with other configs pub network_monitor: NetworkMonitor, + #[serde(default)] + pub mixnet_contract_cache: MixnetContractCache, + pub node_status_api: NodeStatusAPI, - pub topology_cacher: TopologyCacher, + #[serde(alias = "topology_cacher")] + pub describe_cache: DescribeCache, - pub circulating_supply_cacher: CirculatingSupplyCacher, + #[serde(default)] + pub contracts_info_cache: ContractsInfoCache, pub rewarding: Rewarding, @@ -130,10 +144,12 @@ impl Config { Config { save_path: None, base: Base::new_default(id.as_ref()), + performance_provider: Default::default(), network_monitor: NetworkMonitor::new_default(id.as_ref()), + mixnet_contract_cache: Default::default(), node_status_api: NodeStatusAPI::new_default(id.as_ref()), - topology_cacher: Default::default(), - circulating_supply_cacher: Default::default(), + describe_cache: Default::default(), + contracts_info_cache: Default::default(), rewarding: Default::default(), ecash_signer: EcashSigner::new_default(id.as_ref()), address_cache: Default::default(), @@ -184,7 +200,7 @@ impl Config { self.base.bind_address = http_bind_address } if args.allow_illegal_ips { - self.topology_cacher.debug.node_describe_allow_illegal_ips = true + self.describe_cache.debug.allow_illegal_ips = true } if let Some(address_cache_ttl) = args.address_cache_ttl { self.address_cache.time_to_live = address_cache_ttl; @@ -305,6 +321,98 @@ impl Base { } } +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct ContractsInfoCache { + pub time_to_live: Duration, +} + +impl Default for ContractsInfoCache { + fn default() -> Self { + ContractsInfoCache { + time_to_live: DEFAULT_CONTRACT_DETAILS_CACHE_TTL, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct MixnetContractCache { + #[serde(default)] + pub debug: MixnetContractCacheDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for MixnetContractCache { + fn default() -> Self { + MixnetContractCache { + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(default)] +pub struct MixnetContractCacheDebug { + #[serde(with = "humantime_serde")] + pub caching_interval: Duration, +} + +impl Default for MixnetContractCacheDebug { + fn default() -> Self { + MixnetContractCacheDebug { + caching_interval: DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PerformanceProvider { + /// Specifies whether this nym-api should attempt to retrieve node performance + /// information from the performance contract. + pub use_performance_contract_data: bool, + + pub debug: PerformanceProviderDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for PerformanceProvider { + fn default() -> Self { + PerformanceProvider { + // to be changed later + use_performance_contract_data: false, + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PerformanceProviderDebug { + /// Specifies interval of polling the performance contract. Note it is only applicable + /// if the contract data is being used. + /// Further note that if there have been no updates to the cache, the performance overhead is negligible + /// (i.e. there will be only a single query performed to check if anything has changed) + #[serde(with = "humantime_serde")] + pub contract_polling_interval: Duration, + + /// Specify the maximum number of epochs we can fallback to if given epoch's performance data + /// is not available in the contract + pub max_performance_fallback_epochs: u32, + + /// Specify the maximum number of epoch entries to be kept in the cache in case we needed non-current data + // (currently we need an equivalent of full day worth of data for legacy endpoints) + pub max_epoch_entries_to_retain: usize, +} + +#[allow(clippy::derivable_impls)] +impl Default for PerformanceProviderDebug { + fn default() -> Self { + PerformanceProviderDebug { + contract_polling_interval: DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL, + max_performance_fallback_epochs: DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS, + max_epoch_entries_to_retain: DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS, + } + } +} + #[derive(Debug, PartialEq, Eq)] pub struct AddressCacheConfig { pub time_to_live: Duration, @@ -447,76 +555,41 @@ pub struct NodeStatusAPIDebug { impl Default for NodeStatusAPIDebug { fn default() -> Self { NodeStatusAPIDebug { - caching_interval: DEFAULT_NODE_STATUS_CACHE_INTERVAL, + caching_interval: DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL, } } } #[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct TopologyCacher { +pub struct DescribeCache { // pub enabled: bool, // pub paths: TopologyCacherPathfinder, #[serde(default)] - pub debug: TopologyCacherDebug, + pub debug: DescribeCacheDebug, } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct TopologyCacherDebug { +pub struct DescribeCacheDebug { #[serde(with = "humantime_serde")] + #[serde(alias = "node_describe_caching_interval")] pub caching_interval: Duration, - #[serde(with = "humantime_serde")] - pub node_describe_caching_interval: Duration, + #[serde(alias = "node_describe_batch_size")] + pub batch_size: usize, - pub node_describe_batch_size: usize, - - pub node_describe_allow_illegal_ips: bool, + #[serde(alias = "node_describe_allow_illegal_ips")] + pub allow_illegal_ips: bool, } -impl Default for TopologyCacherDebug { +impl Default for DescribeCacheDebug { fn default() -> Self { - TopologyCacherDebug { - caching_interval: DEFAULT_TOPOLOGY_CACHE_INTERVAL, - node_describe_caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, - node_describe_batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, - node_describe_allow_illegal_ips: false, - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde(default)] -pub struct CirculatingSupplyCacher { - pub enabled: bool, - - // pub paths: CirculatingSupplyCacherPathfinder, - #[serde(default)] - pub debug: CirculatingSupplyCacherDebug, -} - -impl Default for CirculatingSupplyCacher { - fn default() -> Self { - CirculatingSupplyCacher { - enabled: true, - debug: CirculatingSupplyCacherDebug::default(), - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde(default)] -pub struct CirculatingSupplyCacherDebug { - #[serde(with = "humantime_serde")] - pub caching_interval: Duration, -} - -impl Default for CirculatingSupplyCacherDebug { - fn default() -> Self { - CirculatingSupplyCacherDebug { - caching_interval: DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL, + DescribeCacheDebug { + caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, + batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, + allow_illegal_ips: false, } } } diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 8925b3beee..b04956eed8 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -3,9 +3,9 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::ecash::api_routes::handlers::ecash_routes; +use crate::mixnet_contract_cache::handlers::nym_contract_cache_routes; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::status_routes; -use crate::nym_contract_cache::handlers::nym_contract_cache_routes; use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; use crate::status; diff --git a/nym-api/src/support/http/state/contract_details.rs b/nym-api/src/support/http/state/contract_details.rs new file mode 100644 index 0000000000..5380a92809 --- /dev/null +++ b/nym-api/src/support/http/state/contract_details.rs @@ -0,0 +1,182 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumErrorResponse; +use crate::support::nyxd::Client; +use nym_contracts_common::ContractBuildInformation; +use nym_validator_client::nyxd::contract_traits::{ + MixnetQueryClient, NymContractsProvider, VestingQueryClient, +}; +use nym_validator_client::nyxd::error::NyxdError; +use nym_validator_client::nyxd::AccountId; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +type ContractAddress = String; + +pub type CachedContractsInfo = HashMap<ContractAddress, CachedContractInfo>; + +#[derive(Clone)] +pub struct CachedContractInfo { + pub(crate) address: Option<AccountId>, + pub(crate) base: Option<cw2::ContractVersion>, + pub(crate) detailed: Option<ContractBuildInformation>, +} + +impl CachedContractInfo { + pub fn new( + address: Option<&AccountId>, + base: Option<cw2::ContractVersion>, + detailed: Option<ContractBuildInformation>, + ) -> Self { + Self { + address: address.cloned(), + base, + detailed, + } + } +} + +#[derive(Clone)] +pub(crate) struct ContractDetailsCache { + cache_ttl: Duration, + inner: Arc<RwLock<ContractDetailsCacheInner>>, +} + +impl ContractDetailsCache { + pub(crate) fn new(cache_ttl: Duration) -> Self { + ContractDetailsCache { + cache_ttl, + inner: Arc::new(RwLock::new(ContractDetailsCacheInner::new())), + } + } +} + +struct ContractDetailsCacheInner { + last_refreshed_at: OffsetDateTime, + cache_value: CachedContractsInfo, +} + +impl ContractDetailsCacheInner { + pub(crate) fn new() -> Self { + ContractDetailsCacheInner { + last_refreshed_at: OffsetDateTime::UNIX_EPOCH, + cache_value: Default::default(), + } + } + + fn is_valid(&self, ttl: Duration) -> bool { + if self.last_refreshed_at + ttl > OffsetDateTime::now_utc() { + return true; + } + false + } + + async fn retrieve_nym_contracts_info( + &self, + nyxd_client: &Client, + ) -> Result<CachedContractsInfo, NyxdError> { + use crate::query_guard; + + let mut updated = HashMap::new(); + + let client_guard = nyxd_client.read().await; + + let mixnet = query_guard!(client_guard, mixnet_contract_address()); + let vesting = query_guard!(client_guard, vesting_contract_address()); + let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); + let group = query_guard!(client_guard, group_contract_address()); + let multisig = query_guard!(client_guard, multisig_contract_address()); + let ecash = query_guard!(client_guard, ecash_contract_address()); + let performance = query_guard!(client_guard, performance_contract_address()); + + for (address, name) in [ + (mixnet, "nym-mixnet-contract"), + (vesting, "nym-vesting-contract"), + (coconut_dkg, "nym-coconut-dkg-contract"), + (group, "nym-cw4-group-contract"), + (multisig, "nym-cw3-multisig-contract"), + (ecash, "nym-ecash-contract"), + (performance, "nym-performance-contract"), + ] { + let (cw2, build_info) = if let Some(address) = address { + let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); + let mut build_info = query_guard!( + client_guard, + try_get_contract_build_information(address).await + ); + + // for backwards compatibility until we migrate the contracts + if build_info.is_none() { + match name { + "nym-mixnet-contract" => { + build_info = Some(query_guard!( + client_guard, + get_mixnet_contract_version().await + )?) + } + "nym-vesting-contract" => { + build_info = Some(query_guard!( + client_guard, + get_vesting_contract_version().await + )?) + } + _ => (), + } + } + + (cw2, build_info) + } else { + (None, None) + }; + + updated.insert( + name.to_string(), + CachedContractInfo::new(address, cw2, build_info), + ); + } + + Ok(updated) + } +} + +impl ContractDetailsCache { + pub(crate) async fn get_or_refresh( + &self, + client: &Client, + ) -> Result<CachedContractsInfo, AxumErrorResponse> { + if let Some(cached) = self.check_cache().await { + return Ok(cached); + } + + self.refresh(client).await + } + + async fn check_cache(&self) -> Option<CachedContractsInfo> { + let guard = self.inner.read().await; + if guard.is_valid(self.cache_ttl) { + return Some(guard.cache_value.clone()); + } + None + } + + async fn refresh(&self, client: &Client) -> Result<CachedContractsInfo, AxumErrorResponse> { + // 1. attempt to get write lock permit + let mut guard = self.inner.write().await; + + // 2. check if another task hasn't already updated the cache whilst we were waiting for the permit + if guard.is_valid(self.cache_ttl) { + return Ok(guard.cache_value.clone()); + } + + // 3. attempt to query the chain for the contracts data + let updated_values = guard.retrieve_nym_contracts_info(client).await?; + guard.last_refreshed_at = OffsetDateTime::now_utc(); + guard.cache_value = updated_values.clone(); + + Ok(updated_values) + } +} diff --git a/nym-api/src/support/http/state/mod.rs b/nym-api/src/support/http/state/mod.rs index b3bcf49216..ec0d2ca9bb 100644 --- a/nym-api/src/support/http/state/mod.rs +++ b/nym-api/src/support/http/state/mod.rs @@ -1,18 +1,18 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::models::AxumErrorResponse; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::caching::Cache; use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::nyxd::Client; use crate::support::storage; @@ -27,26 +27,58 @@ use std::sync::Arc; use tokio::sync::RwLockReadGuard; pub(crate) mod chain_status; +pub(crate) mod contract_details; pub(crate) mod force_refresh; #[derive(Clone)] pub(crate) struct AppState { // ideally this would have been made generic to make tests easier, // however, it'd be a way bigger change (I tried) + /// Instance of a client used for interacting with the nyx chain. pub(crate) nyxd_client: Client, + + /// Holds information about the latest chain block it has queried. + /// Note, it is not updated on every request. It follows the embedded ttl. pub(crate) chain_status_cache: ChainStatusCache, + /// Holds mapping between a nyx address and tokens/delegations it holds pub(crate) address_info_cache: AddressInfoCache, + + /// Holds information on when nym-nodes requested an explicit request of their self-described data. + /// It is used to prevent DoS by nodes constantly requesting the refresh. pub(crate) forced_refresh: ForcedRefresh, - pub(crate) nym_contract_cache: NymContractCache, + + /// Holds cached state of the Nym Mixnet contract, e.g. bonded nym-nodes, rewarded set, current interval. + pub(crate) mixnet_contract_cache: MixnetContractCache, + + /// Holds processed information on network nodes, i.e. performance, config scores, etc. + // TODO: also perhaps redundant? pub(crate) node_status_cache: NodeStatusCache, - pub(crate) circulating_supply_cache: CirculatingSupplyCache, + + /// Holds reference to the persistent storage of this nym-api. pub(crate) storage: storage::NymApiStorage, + + /// Holds information on the self-reported information of nodes, e.g. auxiliary keys they use, + /// ports they announce, etc. pub(crate) described_nodes_cache: SharedCache<DescribedNodes>, + + /// Information about the current network this nym-api is connected to, e.g. contract addresses, + /// endpoints, denominations. pub(crate) network_details: NetworkDetails, + + /// A simple in-memory cache of node information mapping their database id to their node-ids + /// and public keys. Useful (I guess?) for returning information about test routes. + // TODO: do we need it? pub(crate) node_info_cache: unstable::NodeInfoCache, + + /// Cache containing data (build info, versions, etc.) on all nym smart contracts on the network + pub(crate) contract_info_cache: ContractDetailsCache, + + /// Information about this nym-api, i.e. its public key, startup time, etc. pub(crate) api_status: ApiStatusState, + // todo: refactor it into inner: Arc<EcashStateInner> + /// Cache holding data required by the ecash credentials - static signatures, merkle trees, etc. pub(crate) ecash_state: Arc<EcashState>, } @@ -62,19 +94,21 @@ impl FromRef<AppState> for Arc<EcashState> { } } +impl FromRef<AppState> for MixnetContractCache { + fn from_ref(app_state: &AppState) -> Self { + app_state.mixnet_contract_cache.clone() + } +} + impl AppState { - pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { - &self.nym_contract_cache + pub(crate) fn nym_contract_cache(&self) -> &MixnetContractCache { + &self.mixnet_contract_cache } pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { &self.node_status_cache } - pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { - &self.circulating_supply_cache - } - pub(crate) fn network_details(&self) -> &NetworkDetails { &self.network_details } @@ -153,7 +187,7 @@ impl AppState { .address_info_cache .collect_balances( self.nyxd_client.clone(), - self.nym_contract_cache.clone(), + self.mixnet_contract_cache.clone(), self.network_details() .network .chain_details diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 41ef7d2bc4..6de33c4738 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -17,10 +17,10 @@ use nym_coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, PartialContractDealingData, State}; use nym_coconut_dkg_common::{ dealer::{DealerDetails, DealerDetailsResponse}, - types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId}, + types::{EncodedBTEPublicKeyWithProof, Epoch}, verification_key::{ContractVKShare, VerificationKeyShare}, }; -use nym_config::defaults::{ChainDetails, NymNetworkDetails}; +use nym_config::defaults::NymNetworkDetails; use nym_dkg::Threshold; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; @@ -35,14 +35,19 @@ use nym_mixnet_contract_common::{ }; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; -use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; +use nym_validator_client::nyxd::contract_traits::performance_query_client::{ + LastSubmission, NodePerformance, +}; +use nym_validator_client::nyxd::contract_traits::{ + PagedDkgQueryClient, PagedPerformanceQueryClient, PerformanceQueryClient, +}; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::Coin; use nym_validator_client::nyxd::{ contract_traits::{ DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, NymContractsProvider, - PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, + PagedMixnetQueryClient, PagedMultisigQueryClient, }, cosmwasm_client::types::ExecuteResult, BlockResponse, CosmWasmClient, Fee, TendermintRpcClient, @@ -54,7 +59,6 @@ use nym_validator_client::nyxd::{ use nym_validator_client::{ nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient, }; -use nym_vesting_contract_common::AccountVestingCoins; use serde::Deserialize; use std::sync::Arc; use tendermint::abci::response::Info; @@ -163,10 +167,6 @@ impl Client { } } - pub(crate) async fn chain_details(&self) -> ChainDetails { - nyxd_query!(self, current_chain_details().clone()) - } - pub(crate) async fn get_ecash_contract_address(&self) -> Result<AccountId, EcashError> { nyxd_query!( self, @@ -258,6 +258,12 @@ impl Client { nyxd_query!(self, get_current_interval_details().await) } + pub(crate) async fn get_mixnet_contract_state( + &self, + ) -> Result<nym_mixnet_contract_common::ContractState, NyxdError> { + nyxd_query!(self, get_mixnet_contract_state().await) + } + pub(crate) async fn get_current_epoch_status(&self) -> Result<EpochStatus, NyxdError> { nyxd_query!(self, get_current_epoch_status().await) } @@ -272,31 +278,6 @@ impl Client { nyxd_query!(self, get_rewarded_set().await) } - pub(crate) async fn get_current_vesting_account_storage_key(&self) -> Result<u32, NyxdError> { - let guard = self.inner.read().await; - - // the expect is fine as we always construct the client with the vesting contract explicitly set - let vesting_contract = query_guard!( - guard, - vesting_contract_address().expect("vesting contract address is not available") - ); - // TODO: I don't like the usage of the hardcoded value here - let res = query_guard!( - guard, - query_contract_raw(vesting_contract, b"key".to_vec()).await? - ); - if res.is_empty() { - return Ok(0); - } - - serde_json::from_slice(&res).map_err(NyxdError::from) - } - - pub(crate) async fn get_all_vesting_coins( - &self, - ) -> Result<Vec<AccountVestingCoins>, NyxdError> { - nyxd_query!(self, get_all_accounts_vesting_coins().await) - } pub(crate) async fn get_pending_events_count(&self) -> Result<u32, NyxdError> { let pending = nyxd_query!(self, get_number_of_pending_events().await?); Ok(pending.epoch_events + pending.interval_events) @@ -423,6 +404,19 @@ impl Client { ) -> Result<Option<Coin>, NyxdError> { nyxd_query!(self, get_balance(&address, denom.into()).await) } + + pub(crate) async fn get_last_performance_contract_submission( + &self, + ) -> Result<LastSubmission, NyxdError> { + nyxd_query!(self, get_last_submission().await) + } + + pub(crate) async fn get_full_epoch_performance( + &self, + epoch_id: nym_mixnet_contract_common::EpochId, + ) -> Result<Vec<NodePerformance>, NyxdError> { + nyxd_query!(self, get_all_epoch_performance(epoch_id).await) + } } #[async_trait] @@ -508,7 +502,7 @@ impl crate::ecash::client::Client for Client { async fn get_epoch_threshold( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> crate::ecash::error::Result<Option<Threshold>> { Ok(nyxd_query!(self, get_epoch_threshold(epoch_id).await?)) } @@ -522,7 +516,7 @@ impl crate::ecash::client::Client for Client { async fn get_registered_dealer_details( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> crate::ecash::error::Result<RegisteredDealerDetails> { let dealer = dealer @@ -537,7 +531,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealer_dealings_status( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> crate::ecash::error::Result<DealerDealingsStatusResponse> { Ok(nyxd_query!( @@ -548,7 +542,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_status( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, dealing_index: DealingIndex, ) -> crate::ecash::error::Result<DealingStatusResponse> { @@ -564,7 +558,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_metadata( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, dealing_index: DealingIndex, ) -> crate::ecash::error::Result<Option<DealingMetadata>> { @@ -578,7 +572,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_chunk( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, @@ -593,7 +587,7 @@ impl crate::ecash::client::Client for Client { async fn get_verification_key_share( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> Result<Option<ContractVKShare>, EcashError> { Ok(nyxd_query!(self, get_vk_share(epoch_id, dealer).await?).share) @@ -601,7 +595,7 @@ impl crate::ecash::client::Client for Client { async fn get_verification_key_shares( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> Result<Vec<ContractVKShare>, EcashError> { Ok(nyxd_query!( self, @@ -611,7 +605,7 @@ impl crate::ecash::client::Client for Client { async fn get_registered_ecash_clients( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> Result<Vec<EcashApiClient>, EcashError> { Ok(self .get_verification_key_shares(epoch_id) diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index b1f294d6d7..710481d41e 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -81,21 +81,6 @@ impl StorageManager { Ok(node_id) } - pub(super) async fn get_gateway_identity_key( - &self, - node_id: NodeId, - ) -> Result<Option<IdentityKey>, sqlx::Error> { - let identity_key = sqlx::query!( - "SELECT identity FROM gateway_details WHERE node_id = ?", - node_id - ) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.identity); - - Ok(identity_key) - } - /// Tries to obtain identity value of given mixnode given its mix_id /// /// # Arguments @@ -116,62 +101,6 @@ impl StorageManager { Ok(identity_key) } - /// Gets all reliability statuses for mixnode with particular identity that were inserted - /// into the database after the specified unix timestamp. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(super) async fn get_mixnode_statuses_since( - &self, - mix_id: NodeId, - timestamp: i64, - ) -> Result<Vec<NodeStatus>, sqlx::Error> { - sqlx::query_as!( - NodeStatus, - r#" - SELECT timestamp, reliability as "reliability: u8" - FROM mixnode_status - JOIN mixnode_details - ON mixnode_status.mixnode_details_id = mixnode_details.id - WHERE mixnode_details.mix_id=? AND mixnode_status.timestamp > ?; - "#, - mix_id, - timestamp, - ) - .fetch_all(&self.connection_pool) - .await - } - - /// Gets all reliability statuses for gateway with particular identity that were inserted - /// into the database after the specified unix timestamp. - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(super) async fn get_gateway_statuses_since( - &self, - node_id: NodeId, - timestamp: i64, - ) -> Result<Vec<NodeStatus>, sqlx::Error> { - sqlx::query_as!( - NodeStatus, - r#" - SELECT timestamp, reliability as "reliability: u8" - FROM gateway_status - JOIN gateway_details - ON gateway_status.gateway_details_id = gateway_details.id - WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?; - "#, - node_id, - timestamp, - ) - .fetch_all(&self.connection_pool) - .await - } - /// Gets the historical daily uptime associated with the particular mixnode /// /// # Arguments diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index d059ad304d..8bb14a697d 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -9,7 +9,7 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; -use crate::storage::models::{NodeStatus, TestingRoute}; +use crate::storage::models::TestingRoute; use crate::support::storage::models::{ GatewayDetails, HistoricalUptime, MixnodeDetails, MonitorRunReport, MonitorRunScore, TestedGatewayStatus, TestedMixnodeStatus, @@ -133,124 +133,6 @@ impl NymApiStorage { Ok(None) } - /// Gets all statuses for particular mixnode that were inserted - /// since the provided timestamp. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode to query. - /// * `since`: unix timestamp indicating the lower bound interval of the selection. - async fn get_mixnode_statuses( - &self, - mix_id: NodeId, - since: i64, - ) -> Result<Vec<NodeStatus>, NymApiStorageError> { - let statuses = self - .manager - .get_mixnode_statuses_since(mix_id, since) - .await?; - - Ok(statuses) - } - - /// Gets all statuses for particular gateway that were inserted - /// since the provided timestamp. - /// - /// # Arguments - /// - /// * `since`: unix timestamp indicating the lower bound interval of the selection. - async fn get_gateway_statuses( - &self, - node_id: NodeId, - since: i64, - ) -> Result<Vec<NodeStatus>, NymApiStorageError> { - let statuses = self - .manager - .get_gateway_statuses_since(node_id, since) - .await?; - - Ok(statuses) - } - - /// Tries to construct a status report for mixnode with the specified mix_id. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn construct_mixnode_report( - &self, - mix_id: NodeId, - ) -> Result<MixnodeStatusReport, NymApiStorageError> { - let now = OffsetDateTime::now_utc(); - let day_ago = (now - ONE_DAY).unix_timestamp(); - let hour_ago = (now - ONE_HOUR).unix_timestamp(); - - let statuses = self.get_mixnode_statuses(mix_id, day_ago).await?; - - // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report - if statuses.is_empty() { - return Err(NymApiStorageError::MixnodeReportNotFound { mix_id }); - } - - // determine the number of runs the mixnode should have been online for - let last_hour_runs_count = self - .get_monitor_runs_count(hour_ago, now.unix_timestamp()) - .await?; - let last_day_runs_count = self - .get_monitor_runs_count(day_ago, now.unix_timestamp()) - .await?; - - let Some(mixnode_identity) = self.manager.get_mixnode_identity_key(mix_id).await? else { - return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {mix_id} doesn't have an identity even though we have status information on it!") }); - }; - - Ok(MixnodeStatusReport::construct_from_last_day_reports( - now, - mix_id, - mixnode_identity, - statuses, - last_hour_runs_count, - last_day_runs_count, - )) - } - - pub(crate) async fn construct_gateway_report( - &self, - node_id: NodeId, - ) -> Result<GatewayStatusReport, NymApiStorageError> { - let now = OffsetDateTime::now_utc(); - let day_ago = (now - ONE_DAY).unix_timestamp(); - let hour_ago = (now - ONE_HOUR).unix_timestamp(); - - let statuses = self.get_gateway_statuses(node_id, day_ago).await?; - - // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report - if statuses.is_empty() { - return Err(NymApiStorageError::GatewayReportNotFound { node_id }); - } - - // determine the number of runs the gateway should have been online for - let last_hour_runs_count = self - .get_monitor_runs_count(hour_ago, now.unix_timestamp()) - .await?; - let last_day_runs_count = self - .get_monitor_runs_count(day_ago, now.unix_timestamp()) - .await?; - - let Some(gateway_identity) = self.manager.get_gateway_identity_key(node_id).await? else { - return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {node_id} doesn't have an identity even though we have status information on it!") }); - }; - - Ok(GatewayStatusReport::construct_from_last_day_reports( - now, - node_id, - gateway_identity, - statuses, - last_hour_runs_count, - last_day_runs_count, - )) - } - pub(crate) async fn get_mixnode_uptime_history( &self, mix_id: NodeId, diff --git a/nym-api/src/unstable_routes/v1/account/cache.rs b/nym-api/src/unstable_routes/v1/account/cache.rs index 75fe83a1fb..23623b2bf3 100644 --- a/nym-api/src/unstable_routes/v1/account/cache.rs +++ b/nym-api/src/unstable_routes/v1/account/cache.rs @@ -1,6 +1,8 @@ use crate::unstable_routes::v1::account::data_collector::AddressDataCollector; use crate::unstable_routes::v1::account::models::{NyxAccountDelegationDetails, NyxAccountDetails}; -use crate::{node_status_api::models::AxumResult, nym_contract_cache::cache::NymContractCache}; +use crate::{ + mixnet_contract_cache::cache::MixnetContractCache, node_status_api::models::AxumResult, +}; use moka::{future::Cache, Entry}; use nym_validator_client::nyxd::AccountId; use std::{sync::Arc, time::Duration}; @@ -48,7 +50,7 @@ impl AddressInfoCache { pub(crate) async fn collect_balances( &self, nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, base_denom: String, address: &str, account_id: AccountId, diff --git a/nym-api/src/unstable_routes/v1/account/data_collector.rs b/nym-api/src/unstable_routes/v1/account/data_collector.rs index 7d130e9f94..4d29f6dc29 100644 --- a/nym-api/src/unstable_routes/v1/account/data_collector.rs +++ b/nym-api/src/unstable_routes/v1/account/data_collector.rs @@ -3,8 +3,8 @@ use crate::unstable_routes::v1::account::models::NyxAccountDelegationRewardDetails; use crate::{ + mixnet_contract_cache::cache::MixnetContractCache, node_status_api::models::{AxumErrorResponse, AxumResult}, - nym_contract_cache::cache::NymContractCache, }; use cosmwasm_std::{Coin, Decimal}; use nym_mixnet_contract_common::NodeRewarding; @@ -14,7 +14,7 @@ use tracing::error; pub(crate) struct AddressDataCollector { nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, account_id: AccountId, total_value: u128, operator_rewards: u128, @@ -26,7 +26,7 @@ pub(crate) struct AddressDataCollector { impl AddressDataCollector { pub(crate) fn new( nyxd_client: crate::nyxd::Client, - nym_contract_cache: NymContractCache, + nym_contract_cache: MixnetContractCache, base_denom: String, account_id: AccountId, ) -> Self { diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 104e4c9863..916f30eb1f 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "3.0.0" +version = "3.1.1" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -28,7 +28,7 @@ moka = { workspace = true, features = ["future"] } # Nym API: revert after Cheddar is out nym-contracts-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-mixnet-contract-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } -nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } +nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar", features = ["openapi"]} nym-node-status-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-crypto = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-http-api-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } diff --git a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs index e9d76a1f75..f150dbc139 100644 --- a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs @@ -16,10 +16,6 @@ pub(crate) struct Cli { #[clap(long, env = "NETWORK_NAME")] pub(crate) network_name: String, - /// Explorer api url. - #[clap(short, long, env = "EXPLORER_API")] - pub(crate) explorer_api: String, - /// Nym api url. #[clap(short, long, env = "NYM_API")] pub(crate) nym_api: String, @@ -49,6 +45,10 @@ pub(crate) struct Cli { #[clap(long, env = "DATABASE_URL")] pub(crate) database_url: String, + #[clap(long, default_value = "5", env = "SQLX_BUSY_TIMEOUT_S")] + #[arg(value_parser = parse_duration)] + pub(crate) sqlx_busy_timeout_s: Duration, + #[clap( long, default_value = "300", diff --git a/nym-node-status-api/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/nym-node-status-api/src/db/mod.rs index cc3398964c..3f7d944906 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/mod.rs @@ -1,10 +1,11 @@ use anyhow::{anyhow, Result}; use sqlx::{ migrate::Migrator, + query, sqlite::{SqliteAutoVacuum, SqliteConnectOptions, SqliteSynchronous}, ConnectOptions, SqlitePool, }; -use std::str::FromStr; +use std::{str::FromStr, time::Duration}; pub(crate) mod models; pub(crate) mod queries; @@ -18,9 +19,10 @@ pub(crate) struct Storage { } impl Storage { - pub async fn init(connection_url: String) -> Result<Self> { + pub async fn init(connection_url: String, busy_timeout: Duration) -> Result<Self> { let connect_options = SqliteConnectOptions::from_str(&connection_url)? .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .busy_timeout(busy_timeout) .synchronous(SqliteSynchronous::Normal) .auto_vacuum(SqliteAutoVacuum::Incremental) .foreign_keys(true) @@ -33,6 +35,9 @@ impl Storage { MIGRATOR.run(&pool).await?; + // aftering setting pragma, check whether it was set successfully + Self::assert_busy_timeout(pool.clone(), busy_timeout.as_secs() as i64).await?; + Ok(Storage { pool }) } @@ -40,4 +45,27 @@ impl Storage { pub fn pool_owned(&self) -> DbPool { self.pool.clone() } + + async fn assert_busy_timeout(pool: DbPool, expected_busy_timeout_s: i64) -> Result<()> { + let mut conn = pool.acquire().await?; + // Sqlite stores this value as miliseconds + // https://www.sqlite.org/pragma.html#pragma_busy_timeout + let busy_timeout_db = query!("PRAGMA busy_timeout;") + .fetch_one(conn.as_mut()) + .await?; + + let actual_busy_timeout_ms = busy_timeout_db.timeout.unwrap_or(0); + tracing::info!("PRAGMA busy_timeout={}ms", actual_busy_timeout_ms); + let expected_busy_timeout_ms = expected_busy_timeout_s * 1000; + + if expected_busy_timeout_ms != actual_busy_timeout_ms { + anyhow::bail!( + "PRAGMA busy_timeout expected: {}ms, actual: {}ms", + expected_busy_timeout_ms, + actual_busy_timeout_ms + ); + } + + Ok(()) + } } diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs index 5bc06be2b2..bd4300275b 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod metrics; pub(crate) mod mixnodes; pub(crate) mod nym_nodes; pub(crate) mod services; +pub(crate) mod status; pub(crate) mod summary; pub(crate) mod testruns; @@ -39,7 +40,8 @@ impl RouterBuilder { .nest("/mixnodes", mixnodes::routes()) .nest("/services", services::routes()) .nest("/summary", summary::routes()) - .nest("/metrics", metrics::routes()), + .nest("/metrics", metrics::routes()) + .nest("/status", status::routes()), ) .nest( "/explorer/v3", diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/status.rs b/nym-node-status-api/nym-node-status-api/src/http/api/status.rs new file mode 100644 index 0000000000..306a7f9c2b --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/status.rs @@ -0,0 +1,46 @@ +use axum::{extract::State, Json, Router}; +use nym_validator_client::models::BinaryBuildInformationOwned; +use tracing::instrument; + +use crate::http::{ + error::HttpResult, + state::{AppState, HealthInfo}, +}; + +pub(crate) fn routes() -> Router<AppState> { + Router::new() + .route("/build_information", axum::routing::get(build_information)) + .route("/health", axum::routing::get(health)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/build_information", + context_path = "/v2/status", + responses( + (status = 200, body = BinaryBuildInformationOwned) + ) +)] +#[instrument(level = tracing::Level::INFO, skip_all)] +async fn build_information( + State(state): State<AppState>, +) -> HttpResult<Json<BinaryBuildInformationOwned>> { + let build_info = state.build_information().to_owned(); + + Ok(Json(build_info)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/health", + context_path = "/v2/status", + responses( + (status = 200, body = HealthInfo) + ) +)] +#[instrument(level = tracing::Level::INFO, skip_all)] +async fn health(State(state): State<AppState>) -> HttpResult<Json<HealthInfo>> { + Ok(Json(state.health())) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index 0a5ef4fca9..8d197f9a07 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -1,15 +1,20 @@ use cosmwasm_std::Decimal; use itertools::Itertools; use moka::{future::Cache, Entry}; +use nym_bin_common::bin_info_owned; use nym_contracts_common::NaiveFloat; use nym_crypto::asymmetric::ed25519::PublicKey; use nym_mixnet_contract_common::NodeId; use nym_validator_client::nym_api::SkimmedNode; use semver::Version; +use serde::Serialize; use std::{collections::HashMap, sync::Arc, time::Duration}; +use time::UtcDateTime; use tokio::sync::RwLock; use tracing::{error, instrument, warn}; +use utoipa::ToSchema; +use super::models::SessionStats; use crate::{ db::{queries, DbPool}, http::models::{ @@ -18,7 +23,7 @@ use crate::{ monitor::{DelegationsCache, NodeGeoCache}, }; -use super::models::SessionStats; +pub(crate) use nym_validator_client::models::BinaryBuildInformationOwned; #[derive(Debug, Clone)] pub(crate) struct AppState { @@ -28,6 +33,7 @@ pub(crate) struct AppState { agent_max_count: i64, node_geocache: NodeGeoCache, node_delegations: Arc<RwLock<DelegationsCache>>, + bin_info: BinaryInfo, } impl AppState { @@ -46,6 +52,7 @@ impl AppState { agent_max_count, node_geocache, node_delegations, + bin_info: BinaryInfo::new(), } } @@ -78,6 +85,15 @@ impl AppState { .await .delegations_owned(node_id) } + + pub(crate) fn health(&self) -> HealthInfo { + let uptime = (UtcDateTime::now() - self.bin_info.startup_time).whole_seconds(); + HealthInfo { uptime } + } + + pub(crate) fn build_information(&self) -> &BinaryBuildInformationOwned { + &self.bin_info.build_info + } } static GATEWAYS_LIST_KEY: &str = "gateways"; @@ -631,3 +647,23 @@ async fn aggregate_node_info_from_db( Ok(parsed_nym_nodes) } + +#[derive(Debug, Clone)] +pub(crate) struct BinaryInfo { + startup_time: UtcDateTime, + build_info: BinaryBuildInformationOwned, +} + +impl BinaryInfo { + fn new() -> Self { + Self { + startup_time: UtcDateTime::now(), + build_info: bin_info_owned!(), + } + } +} + +#[derive(Serialize, ToSchema)] +pub(crate) struct HealthInfo { + uptime: i64, +} diff --git a/nym-node-status-api/nym-node-status-api/src/main.rs b/nym-node-status-api/nym-node-status-api/src/main.rs index f0bd3b5b46..7db3015a7d 100644 --- a/nym-node-status-api/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/nym-node-status-api/src/main.rs @@ -31,7 +31,7 @@ async fn main() -> anyhow::Result<()> { let connection_url = args.database_url.clone(); tracing::debug!("Using config:\n{:#?}", args); - let storage = db::Storage::init(connection_url).await?; + let storage = db::Storage::init(connection_url, args.sqlx_busy_timeout_s).await?; let db_pool = storage.pool_owned(); // Start the node scraper diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 8b568150ed..703e47662b 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -78,6 +78,7 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> { } config }; + config.validate()?; if !config.modes.any_enabled() { warn!("this node is going to run without mixnode or gateway support! consider providing `mode` value"); diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 0bcd657c04..80836b3834 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -426,6 +426,13 @@ impl Config { pub fn validate(&self) -> Result<(), NymNodeError> { self.mixnet.validate()?; + // it's not allowed to run mixnode mode alongside entry mode + if self.modes.mixnode && self.modes.entry { + return Err(NymNodeError::config_validation_failure( + "illegal modes configuration - node cannot run as a mixnode and an entry gateway", + )); + } + Ok(()) } } diff --git a/nym-statistics-api/Cargo.toml b/nym-statistics-api/Cargo.toml index ae26c96a9e..4d37557118 100644 --- a/nym-statistics-api/Cargo.toml +++ b/nym-statistics-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-statistics-api" -version = "0.1.3" +version = "0.1.4" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-statistics-api/src/http/api/mod.rs b/nym-statistics-api/src/http/api/mod.rs index 9e3fc313d3..16e9763bd0 100644 --- a/nym-statistics-api/src/http/api/mod.rs +++ b/nym-statistics-api/src/http/api/mod.rs @@ -4,7 +4,7 @@ use nym_http_api_common::middleware::logging::log_request_info; use tokio::net::ToSocketAddrs; use tower_http::cors::CorsLayer; use utoipa::OpenApi; -use utoipa_swagger_ui::SwaggerUi; +use utoipa_swagger_ui::{Config, SwaggerUi}; use crate::http::{server::HttpServer, state::AppState}; @@ -19,11 +19,12 @@ impl RouterBuilder { let router = Router::new() .merge( SwaggerUi::new("/swagger") + .config(Config::default().supported_submit_methods(["get"])) .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), ) .route( "/", - axum::routing::get(|| async { Redirect::permanent("/swagger") }), + axum::routing::get(|| async { Redirect::permanent("/swagger") }), // SW let's redirect to a blogpost explaining the stats collection process once it exists ) .nest("/v1", Router::new().nest("/stats", stats::routes())); diff --git a/nym-statistics-api/src/logging.rs b/nym-statistics-api/src/logging.rs index 3c5a3834e0..39c2810226 100644 --- a/nym-statistics-api/src/logging.rs +++ b/nym-statistics-api/src/logging.rs @@ -30,6 +30,7 @@ pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { "axum", "reqwest", "hyper_util", + "nym_http_api_client", ]; for crate_name in warn_crates { filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))?); diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index aa6521d770..a53976a960 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4247,7 +4247,6 @@ dependencies = [ "schemars", "semver", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -4326,6 +4325,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars", + "serde", + "thiserror 2.0.12", +] + [[package]] name = "nym-serde-helpers" version = "0.1.0" @@ -4433,6 +4445,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-performance-contract-common", "nym-serde-helpers", "nym-vesting-contract-common", "prost", diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index c436c9f09e..f8e66ebe3e 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -24,6 +24,10 @@ pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh"; +// \/ TODO: this has to be updated once the contract is deployed +pub(crate) const PERFORMANCE_CONTRACT_ADDRESS: &str = ""; +// /\ TODO: this has to be updated once the contract is deployed + // -- Constructor functions -- pub(crate) fn validators() -> Vec<ValidatorDetails> { @@ -34,8 +38,6 @@ pub(crate) fn validators() -> Vec<ValidatorDetails> { )] } -pub(crate) const EXPLORER_API: &str = "https://sandbox-explorer.nymtech.net/api/"; - pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { nym_network_defaults::NymNetworkDetails { network_name: NETWORK_NAME.into(), @@ -48,12 +50,14 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), + performance_contract_address: parse_optional_str(PERFORMANCE_CONTRACT_ADDRESS), ecash_contract_address: parse_optional_str(ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, - explorer_api: parse_optional_str(EXPLORER_API), nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, } } diff --git a/nym-wallet/src/pages/node-settings/node-stats.tsx b/nym-wallet/src/pages/node-settings/node-stats.tsx index 0dfd9ae18b..04b36e7741 100644 --- a/nym-wallet/src/pages/node-settings/node-stats.tsx +++ b/nym-wallet/src/pages/node-settings/node-stats.tsx @@ -4,12 +4,12 @@ import { TauriLink as Link } from 'src/components/TauriLinkWrapper'; import { urls, AppContext } from '../../context/main'; -export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => { +export const NodeStats = ({ identityKey }: { identityKey?: string }) => { const { network } = useContext(AppContext); return ( <Stack spacing={2} sx={{ p: 4 }}> <Typography>All your node stats are available on the link below</Typography> - <Link href={`${urls(network).networkExplorer}/nodes/${mixnodeId}`} target="_blank" text="Network Explorer" /> + <Link href={`${urls(network).networkExplorer}/nodes/${identityKey}`} target="_blank" text="Network Explorer" /> </Stack> ); }; diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml new file mode 100644 index 0000000000..cef202e4df --- /dev/null +++ b/sqlx-pool-guard/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "sqlx-pool-guard" +version = "0.1.0" +edition = "2024" +license.workspace = true + +[lints] +workspace = true + +[dependencies] +tracing.workspace = true + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] +workspace = true +features = ["runtime-tokio-rustls", "sqlite"] + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +workspace = true +features = ["rt-multi-thread", "macros", "time", "fs"] + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +proc_pidinfo.workspace = true + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61", features = [ + "Win32", + "Win32_System", + "Win32_System_Memory", + "Win32_System_Threading", + "Win32_Storage_FileSystem", + "Wdk_System_SystemInformation", +] } + +[dev-dependencies] +tempfile.workspace = true +tracing-subscriber.workspace = true diff --git a/sqlx-pool-guard/src/apple.rs b/sqlx-pool-guard/src/apple.rs new file mode 100644 index 0000000000..a67ed1a447 --- /dev/null +++ b/sqlx-pool-guard/src/apple.rs @@ -0,0 +1,43 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use std::{io, path::Path}; + +use proc_pidinfo::{ + ProcFDInfo, ProcFDType, VnodeFdInfoWithPath, proc_pidfdinfo_self, proc_pidinfo_list_self, +}; + +/// Check if there are no open file descriptors for the given files. +/// +/// Uses `proc_pidinfo` (`sys/proc_info.h`) +/// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/ +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result<bool> { + let fd_list = proc_pidinfo_list_self::<ProcFDInfo>()?; + + for fd in fd_list + .iter() + .filter(|s| s.fd_type() == Ok(ProcFDType::VNODE)) + { + let Some(vnode) = proc_pidfdinfo_self::<VnodeFdInfoWithPath>(fd.proc_fd) + .inspect_err(|e| { + tracing::warn!("proc_pidfdinfo_self::<VnodeFdInfoWithPath>() failure: {e}"); + }) + .ok() + .flatten() + else { + continue; + }; + + if let Ok(true) = vnode + .path() + .map(|vnode_path| file_paths.contains(&vnode_path)) + .inspect_err(|e| { + tracing::warn!("vnode.path() failure: {e:?}"); + }) + { + return Ok(false); + } + } + + Ok(true) +} diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs new file mode 100644 index 0000000000..3dd15ee6c7 --- /dev/null +++ b/sqlx-pool-guard/src/lib.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + io, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + time::Duration, +}; + +#[cfg(windows)] +#[path = "windows.rs"] +mod imp; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[path = "apple.rs"] +mod imp; + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[path = "linux.rs"] +mod imp; + +/// Max number of retry attempts +const CHECK_FILES_CLOSED_MAX_ATTEMPTS: u8 = 20; + +/// Delay between file checks +const CHECK_FILES_CLOSED_RETRY_DELAY: Duration = Duration::from_millis(100); + +/// `sqlx::SqlitePool` wrapper providing a workaround for the [known bug](https://github.com/launchbadge/sqlx/issues/3217). +/// In principle after requesting to close the sqlite pool, the wrapper monitors open file descriptor and polls periodically until all database files are closed. +#[derive(Debug, Clone)] +pub struct SqlitePoolGuard { + /// Path to sqlite database file. + database_path: PathBuf, + + /// Inner connection pool. + connection_pool: sqlx::SqlitePool, +} + +impl SqlitePoolGuard { + /// Create new instance providing path to database and connection pool + pub fn new(database_path: PathBuf, connection_pool: sqlx::SqlitePool) -> Self { + Self { + database_path, + connection_pool, + } + } + + /// Returns database path + pub fn database_path(&self) -> &Path { + &self.database_path + } + + /// Close udnerlying sqlite pool and wait for files to be closed before returning. + pub async fn close(&self) { + // Avoid waiting for db files once the pool is marked closed to ensure that we don't wait on some other sqlite pool to close the database. + if !self.connection_pool.is_closed() { + tracing::info!("Closing sqlite pool: {}", self.database_path.display()); + self.close_pool_inner().await.ok(); + } + } + + async fn close_pool_inner(&self) -> std::io::Result<()> { + self.connection_pool.close().await; + + self.wait_for_db_files_close().await.inspect_err(|e| { + tracing::error!("Failed to wait for file to close: {e}"); + }) + } + + /// Returns all database files, including shm and wal files. + fn all_database_files(&self) -> Vec<PathBuf> { + let mut database_files = vec![]; + let canonical_path = self + .database_path + .canonicalize() + .inspect_err(|e| { + tracing::error!( + "Failed to canonicalize path: {}. Cause: {e}", + self.database_path.display() + ); + }) + .unwrap_or(self.database_path.clone()); + + if let Some(ext) = canonical_path.extension() { + for added_ext in ["-shm", "-wal"] { + let mut new_ext = ext.to_owned(); + new_ext.push(added_ext); + database_files.push(canonical_path.with_extension(new_ext)); + } + } + database_files.push(canonical_path); + database_files + } + + /// Wait for database files to be closed before returning. + async fn wait_for_db_files_close(&self) -> std::io::Result<()> { + let database_files = self.all_database_files(); + let paths: Vec<&Path> = database_files.iter().map(PathBuf::as_path).collect(); + + for _ in 0..CHECK_FILES_CLOSED_MAX_ATTEMPTS { + match imp::check_files_closed(&paths) + .await + .inspect_err(|e| tracing::error!("imp::check_files_closed() failure: {e}")) + { + Ok(false) | Err(_) => tokio::time::sleep(CHECK_FILES_CLOSED_RETRY_DELAY).await, + Ok(true) => return Ok(()), + } + } + + Err(io::Error::new( + io::ErrorKind::TimedOut, + "timed out waiting for sqlite files to be closed", + )) + } +} + +impl Deref for SqlitePoolGuard { + type Target = sqlx::SqlitePool; + + fn deref(&self) -> &Self::Target { + &self.connection_pool + } +} + +impl DerefMut for SqlitePoolGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.connection_pool + } +} +#[cfg(test)] +mod tests { + use sqlx::{ + ConnectOptions, Executor, + sqlite::{SqliteAutoVacuum, SqliteSynchronous}, + }; + + use super::*; + + #[tokio::test] + async fn test_wait_close() { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); + + let temp_dir = tempfile::tempdir().unwrap(); + let database_path = temp_dir.path().join("storage.db"); + + let opts = sqlx::sqlite::SqliteConnectOptions::new() + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .synchronous(SqliteSynchronous::Normal) + .auto_vacuum(SqliteAutoVacuum::Incremental) + .filename(database_path.clone()) + .create_if_missing(true) + .disable_statement_logging(); + let connection_pool = sqlx::SqlitePool::connect_with(opts).await.unwrap(); + + connection_pool + .execute("create table test (col int)") + .await + .unwrap(); + + let guard = SqlitePoolGuard::new(database_path.clone(), connection_pool); + assert!( + guard + .wait_for_db_files_close() + .await + .err() + .is_some_and(|e| e.kind() == io::ErrorKind::TimedOut) + ); + + assert!(guard.close_pool_inner().await.is_ok()); + tokio::fs::remove_file(database_path).await.unwrap(); + } +} diff --git a/sqlx-pool-guard/src/linux.rs b/sqlx-pool-guard/src/linux.rs new file mode 100644 index 0000000000..e0f26de9e5 --- /dev/null +++ b/sqlx-pool-guard/src/linux.rs @@ -0,0 +1,36 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use std::{io, path::Path}; + +static PROC_SELF_FD_DIR: &str = "/proc/self/fd/"; + +/// Check if there are no open file descriptors for the given files. +/// +/// Linux, Android: uses `/proc/self/fd/` to list open file descriptors +/// See: https://stackoverflow.com/a/59797198/351305 +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result<bool> { + let mut dir = tokio::fs::read_dir(PROC_SELF_FD_DIR).await?; + + while let Ok(Some(entry)) = dir.next_entry().await { + if entry + .file_type() + .await + .inspect_err(|e| tracing::warn!("entry.file_type() failure: {e}")) + .is_ok_and(|entry_type| entry_type.is_symlink()) + { + match tokio::fs::read_link(entry.path()).await { + Ok(resolved_path) => { + if file_paths.contains(&resolved_path.as_ref()) { + return Ok(false); + } + } + Err(e) => { + tracing::error!("Failed to read symlink: {e}"); + } + } + } + } + + Ok(true) +} diff --git a/sqlx-pool-guard/src/windows.rs b/sqlx-pool-guard/src/windows.rs new file mode 100644 index 0000000000..799eac2abe --- /dev/null +++ b/sqlx-pool-guard/src/windows.rs @@ -0,0 +1,188 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + ffi::{OsString, c_uchar, c_ulong, c_ushort, c_void}, + io, + os::windows::ffi::OsStringExt, + path::{Path, PathBuf}, +}; + +use windows::{ + Wdk::System::SystemInformation::{NtQuerySystemInformation, SYSTEM_INFORMATION_CLASS}, + Win32::{ + Foundation::{HANDLE, MAX_PATH, NTSTATUS, STATUS_INFO_LENGTH_MISMATCH}, + Storage::FileSystem::{ + FILE_NAME_NORMALIZED, FILE_TYPE_DISK, GetFileType, GetFinalPathNameByHandleW, + }, + System::{ + Memory::{ + GetProcessHeap, HEAP_FLAGS, HEAP_ZERO_MEMORY, HeapAlloc, HeapFree, HeapReAlloc, + }, + Threading::GetCurrentProcessId, + }, + }, +}; + +/// Private information class used to retrieve open file handles +const SYSTEM_HANDLE_INFORMATION_CLASS: SYSTEM_INFORMATION_CLASS = SYSTEM_INFORMATION_CLASS(0x10); + +/// Initial buffer size holding the handle info +/// The number is based on what I observe on a pretty standard Windows 11 +const SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE: usize = 2_500_000; + +/// Check if there are no open handles to the given files. +/// +/// Uses undocumented NT API to obtain open handles on the system. +/// See: https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/get-all-open-handles-and-kernel-object-address-from-userland +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result<bool> { + let current_pid = unsafe { GetCurrentProcessId() }; + + // Allocate info struct on heap with some initial value + let mut reserved_memory = SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE; + let mut handle_table_info = HeapGuard::<SystemHandleInformation>::new(reserved_memory)?; + + // Request system handle information + let mut status: NTSTATUS = NTSTATUS::default(); + let mut return_len = reserved_memory as u32; + for _ in 0..2 { + status = unsafe { + NtQuerySystemInformation( + SYSTEM_HANDLE_INFORMATION_CLASS, + handle_table_info.as_mut_ptr() as _, + return_len, + &mut return_len, + ) + }; + + // Buffer is too small, resize memory and retry again. + if status == STATUS_INFO_LENGTH_MISMATCH { + tracing::trace!("Buffer is too small ({reserved_memory}), resizing to {return_len}"); + reserved_memory = return_len as usize; + handle_table_info.reallocate(reserved_memory)?; + } else { + break; + } + } + status.ok()?; + + // Convert returned data into slice + let num_handles = unsafe { (*handle_table_info.inner).number_of_handles }; + let proc_entries = unsafe { + std::slice::from_raw_parts( + (*handle_table_info.as_mut_ptr()).handles.as_ptr(), + num_handles as usize, + ) + }; + + // Iterate over open file handle entries + for entry in proc_entries { + if entry.unique_process_id == current_pid { + let file_handle = HANDLE(entry.handle_value as _); + + // Filter everything except disk files + if unsafe { GetFileType(file_handle) } == FILE_TYPE_DISK { + // Obtain canonical path for file handle + let mut file_handle_path = vec![0u16; MAX_PATH as usize]; + let num_chars_without_nul = unsafe { + GetFinalPathNameByHandleW( + file_handle, + &mut file_handle_path, + FILE_NAME_NORMALIZED, + ) as usize + }; + + if num_chars_without_nul > 0 { + let path_str = OsString::from_wide(&file_handle_path[0..num_chars_without_nul]); + let file_handle_pathbuf = PathBuf::from(path_str); + if file_paths.contains(&file_handle_pathbuf.as_path()) { + return Ok(false); + } + } + } + } + } + + Ok(true) +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct SystemHandleInformation { + pub number_of_handles: c_ulong, + pub handles: [SystemHandleTableEntryInfo; 1], +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct SystemHandleTableEntryInfo { + pub unique_process_id: c_ulong, + pub object_type_index: c_uchar, + pub handle_attributes: c_uchar, + pub handle_value: c_ushort, + pub object: *mut c_void, + pub granted_access: c_ulong, +} + +/// Managed heap memory +struct HeapGuard<T> { + inner: *mut T, + process_heap: HANDLE, +} + +impl<T> HeapGuard<T> { + /// Allocate new memory using `HealAlloc` + fn new(length: usize) -> io::Result<Self> { + let process_heap = unsafe { GetProcessHeap()? }; + let inner: *mut T = unsafe { HeapAlloc(process_heap, HEAP_ZERO_MEMORY, length) as _ }; + + if inner.is_null() { + Err(io::Error::other("Failed to allocate memory")) + } else { + Ok(Self { + inner, + process_heap, + }) + } + } + + /// Reallocate existing chunk of memory + /// + /// On success: the internal memory pointer is replaced. + /// On failure: the internal memory pointer remains the same and still valid. + fn reallocate(&mut self, new_length: usize) -> io::Result<()> { + let new_ptr: *mut T = unsafe { + HeapReAlloc( + self.process_heap, + HEAP_ZERO_MEMORY, + Some(self.inner as _), + new_length, + ) as _ + }; + + if new_ptr.is_null() { + Err(io::Error::other("Failed to reallocate memory")) + } else { + self.inner = new_ptr; + Ok(()) + } + } + + fn as_mut_ptr(&self) -> *mut T { + self.inner + } +} + +impl<T> Drop for HeapGuard<T> { + fn drop(&mut self) { + #[allow(clippy::expect_used)] + unsafe { + HeapFree( + self.process_heap, + HEAP_FLAGS(0), + Some(self.inner as *mut c_void), + ) + } + .expect("HeapFree failure"); + } +} diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/testnet-manager/Cargo.toml index 4da0379b11..e1a32ec685 100644 --- a/tools/internal/testnet-manager/Cargo.toml +++ b/tools/internal/testnet-manager/Cargo.toml @@ -46,6 +46,7 @@ nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/g nym-ecash-contract-common = { path = "../../../common/cosmwasm-smart-contracts/ecash-contract" } nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" } nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" } +nym-performance-contract-common = { path = "../../../common/cosmwasm-smart-contracts/nym-performance-contract" } nym-pemstore = { path = "../../../common/pemstore" } diff --git a/tools/internal/testnet-manager/migrations/02_performance_contract.sql b/tools/internal/testnet-manager/migrations/02_performance_contract.sql new file mode 100644 index 0000000000..ad8d53b2c5 --- /dev/null +++ b/tools/internal/testnet-manager/migrations/02_performance_contract.sql @@ -0,0 +1,53 @@ +/* + * Copyright 2025 - Nym Technologies SA <contact@nymtech.net> + * SPDX-License-Identifier: GPL-3.0-only + */ + + +CREATE TABLE network_old +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract (id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract (id), + + rewarder_address TEXT NOT NULL REFERENCES account (address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account (address) +); + +INSERT INTO network_old +SELECT * +from network; + +DROP TABLE network; + +CREATE TABLE network +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract (id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract (id), + performance_contract_id INTEGER NOT NULL REFERENCES contract (id), + + rewarder_address TEXT NOT NULL REFERENCES account (address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account (address) +); + +CREATE TABLE authorised_network_monitor +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + network_id INTEGER NOT NULL REFERENCES network (id), + address TEXT NOT NULL REFERENCES account (address) +); diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs index 8295550dfc..6328dc0b10 100644 --- a/tools/internal/testnet-manager/src/manager/contract.rs +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -18,6 +18,7 @@ pub(crate) struct LoadedNymContracts { pub(crate) cw3_multisig: LoadedContract, pub(crate) cw4_group: LoadedContract, pub(crate) dkg: LoadedContract, + pub(crate) performance: LoadedContract, } impl From<NymContracts> for LoadedNymContracts { @@ -29,6 +30,7 @@ impl From<NymContracts> for LoadedNymContracts { cw3_multisig: value.cw3_multisig.into(), cw4_group: value.cw4_group.into(), dkg: value.dkg.into(), + performance: value.performance.into(), } } } @@ -41,6 +43,7 @@ pub(crate) struct NymContracts { pub(crate) cw3_multisig: Contract, pub(crate) cw4_group: Contract, pub(crate) dkg: Contract, + pub(crate) performance: Contract, } impl NymContracts { @@ -52,6 +55,7 @@ impl NymContracts { &self.cw3_multisig, &self.cw4_group, &self.dkg, + &self.performance, ] } @@ -63,11 +67,12 @@ impl NymContracts { &mut self.cw3_multisig, &mut self.cw4_group, &mut self.dkg, + &mut self.performance, ] } pub(crate) fn count(&self) -> usize { - 6 + 7 } pub(crate) fn discover_paths<P: AsRef<Path>>( @@ -100,6 +105,9 @@ impl NymContracts { if name.contains("dkg") { self.dkg.wasm_path = Some(entry.path()) } + if name.contains("performance") { + self.performance.wasm_path = Some(entry.path()) + } } } @@ -122,6 +130,7 @@ impl Default for NymContracts { cw4_group: Contract::new("cw4_group"), cw3_multisig: Contract::new("cw3_multisig"), dkg: Contract::new("dkg"), + performance: Contract::new("performance"), } } } diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs index 6c65eda06c..39e04f10cf 100644 --- a/tools/internal/testnet-manager/src/manager/network.rs +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -65,6 +65,7 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { let contracts = nym_config::defaults::NymContracts { mixnet_contract_address: Some(value.contracts.mixnet.address.to_string()), vesting_contract_address: Some(value.contracts.vesting.address.to_string()), + performance_contract_address: Some(value.contracts.performance.address.to_string()), ecash_contract_address: Some(value.contracts.ecash.address.to_string()), group_contract_address: Some(value.contracts.cw4_group.address.to_string()), multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()), @@ -81,8 +82,9 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { api_url: None, }], contracts, - explorer_api: None, nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, } } } @@ -133,6 +135,7 @@ impl LoadedNetwork { pub struct SpecialAddresses { pub ecash_holding_account: Account, pub mixnet_rewarder: Account, + pub network_monitors: Vec<Account>, } impl Default for SpecialAddresses { @@ -140,6 +143,8 @@ impl Default for SpecialAddresses { SpecialAddresses { ecash_holding_account: Account::new(), mixnet_rewarder: Account::new(), + // by default use one address; to be adjusted in the future + network_monitors: vec![Account::new()], } } } diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index 8e978e4fc6..44c5a97335 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -38,8 +38,9 @@ impl InitCtx { network_name: "foomp".to_string(), // does this matter? endpoints: vec![], contracts: Default::default(), - explorer_api: None, nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, }; Ok(Config::try_from_nym_network_details(&network_details)?) } @@ -260,6 +261,22 @@ impl NetworkManager { }) } + fn performance_init_message( + &self, + ctx: &InitCtx, + ) -> Result<nym_performance_contract_common::msg::InstantiateMsg, NetworkManagerError> { + Ok(nym_performance_contract_common::msg::InstantiateMsg { + mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(), + authorised_network_monitors: ctx + .network + .auxiliary_addresses + .network_monitors + .iter() + .map(|acc| acc.address.to_string()) + .collect(), + }) + } + fn find_contracts<P: AsRef<Path>>( &self, ctx: &mut InitCtx, @@ -295,6 +312,10 @@ impl NetworkManager { "\tdiscovered dkg contract at '{}'", ctx.network.contracts.dkg.wasm_path()?.display() )); + ctx.println(format!( + "\tdiscovered performance contract at '{}'", + ctx.network.contracts.performance.wasm_path()?.display() + )); ctx.println("\t✅ found all the contracts!"); @@ -391,6 +412,11 @@ impl NetworkManager { ctx.admin.mix_coins(10_000000), )); + // and to any network monitors + for network_monitor in &ctx.network.auxiliary_addresses.network_monitors { + receivers.push((network_monitor.address(), ctx.admin.mix_coins(10_000000))) + } + ctx.set_pb_message("attempting to send admin tokens..."); let send_future = @@ -552,6 +578,28 @@ impl NetworkManager { )); ctx.network.contracts.ecash.init_info = Some(res.into()); + // performance (semi-temp) + ctx.set_pb_prefix(format!("[7/{total}]")); + let name = &ctx.network.contracts.performance.name; + let code_id = ctx.network.contracts.performance.upload_info()?.code_id; + let admin = ctx.network.contracts.performance.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.performance_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.performance.init_info = Some(res.into()); + ctx.println("\t✅ instantiated all the contracts!"); Ok(()) diff --git a/tools/internal/testnet-manager/src/manager/storage/manager.rs b/tools/internal/testnet-manager/src/manager/storage/manager.rs index 24132db687..1bd3713d3b 100644 --- a/tools/internal/testnet-manager/src/manager/storage/manager.rs +++ b/tools/internal/testnet-manager/src/manager/storage/manager.rs @@ -1,6 +1,8 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::manager::storage::models::{RawAccount, RawContract, RawNetwork}; +use crate::manager::storage::models::{ + RawAccount, RawAuthorisedNetworkMonitor, RawContract, RawNetwork, +}; use time::OffsetDateTime; #[derive(Clone)] @@ -85,6 +87,7 @@ impl StorageManager { cw3_id: i64, cw4_id: i64, dkg_id: i64, + performance_id: i64, rewarder_address: &str, ecash_holding_address: &str, ) -> Result<i64, sqlx::Error> { @@ -99,10 +102,11 @@ impl StorageManager { cw3_multisig_contract_id, cw4_group_contract_id, dkg_contract_id, + performance_contract_id, rewarder_address, ecash_holding_account_address ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "#, name, created_at, @@ -112,6 +116,7 @@ impl StorageManager { cw3_id, cw4_id, dkg_id, + performance_id, rewarder_address, ecash_holding_address, ) @@ -159,19 +164,46 @@ impl StorageManager { .await } + pub(crate) async fn save_authorised_network_monitor( + &self, + network_id: i64, + address: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO authorised_network_monitor (network_id, address) VALUES (?, ?)", + network_id, + address, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn load_authorised_network_monitors( + &self, + network_id: i64, + ) -> Result<Vec<RawAuthorisedNetworkMonitor>, sqlx::Error> { + sqlx::query_as("SELECT * FROM authorised_network_monitor WHERE network_id = ?") + .bind(network_id) + .fetch_all(&self.connection_pool) + .await + } + pub(crate) async fn save_account( &self, address: &str, mnemonic: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( + ) -> Result<i64, sqlx::Error> { + let account_id = sqlx::query!( "INSERT INTO account (address, mnemonic) VALUES (?, ?)", address, mnemonic ) .execute(&self.connection_pool) - .await?; - Ok(()) + .await? + .last_insert_rowid(); + Ok(account_id) } pub(crate) async fn load_account(&self, address: &str) -> Result<RawAccount, sqlx::Error> { diff --git a/tools/internal/testnet-manager/src/manager/storage/mod.rs b/tools/internal/testnet-manager/src/manager/storage/mod.rs index 7eb3df7cb1..21c85df32e 100644 --- a/tools/internal/testnet-manager/src/manager/storage/mod.rs +++ b/tools/internal/testnet-manager/src/manager/storage/mod.rs @@ -1,6 +1,6 @@ -use std::fs; // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only + use crate::{ error::NetworkManagerError, manager::{ @@ -14,6 +14,7 @@ use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; +use std::fs; use std::path::Path; use tracing::{error, info}; use url::Url; @@ -159,7 +160,7 @@ impl NetworkManagerStorage { .await?) } - async fn persist_account(&self, account: &Account) -> Result<(), NetworkManagerError> { + async fn persist_account(&self, account: &Account) -> Result<i64, NetworkManagerError> { let as_str = Zeroizing::new(account.mnemonic.to_string()); Ok(self .manager @@ -191,6 +192,18 @@ impl NetworkManagerStorage { Ok(()) } + async fn persist_authorised_network_monitor( + &self, + network_id: i64, + account: &Account, + ) -> Result<(), NetworkManagerError> { + self.persist_account(account).await?; + self.manager + .save_authorised_network_monitor(network_id, account.address.as_ref()) + .await?; + Ok(()) + } + pub(crate) async fn persist_network( &self, network: &Network, @@ -206,6 +219,8 @@ impl NetworkManagerStorage { self.persist_account(network.contracts.cw4_group.admin()?) .await?; self.persist_account(network.contracts.dkg.admin()?).await?; + self.persist_account(network.contracts.performance.admin()?) + .await?; self.persist_account(&network.auxiliary_addresses.mixnet_rewarder) .await?; @@ -220,6 +235,9 @@ impl NetworkManagerStorage { .await?; let cw4_group_id = self.persist_contract(&network.contracts.cw4_group).await?; let dkg_id = self.persist_contract(&network.contracts.dkg).await?; + let performance_id = self + .persist_contract(&network.contracts.performance) + .await?; let network_id = self .manager @@ -232,6 +250,7 @@ impl NetworkManagerStorage { cw3_multisig_id, cw4_group_id, dkg_id, + performance_id, network.auxiliary_addresses.mixnet_rewarder.address.as_ref(), network .auxiliary_addresses @@ -242,6 +261,10 @@ impl NetworkManagerStorage { .await?; self.manager.save_latest_network_id(network_id).await?; + for nm in &network.auxiliary_addresses.network_monitors { + self.persist_authorised_network_monitor(network_id, nm) + .await? + } Ok(()) } @@ -256,6 +279,20 @@ impl NetworkManagerStorage { .await? .ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?; + let authorised = self + .manager + .load_authorised_network_monitors(base_network.id) + .await?; + let mut network_monitors = Vec::with_capacity(authorised.len()); + for authorised in authorised { + network_monitors.push( + self.manager + .load_account(&authorised.address) + .await? + .try_into()?, + ) + } + Ok(LoadedNetwork { id: base_network.id, name: base_network.name, @@ -292,6 +329,11 @@ impl NetworkManagerStorage { .load_contract(base_network.dkg_contract_id) .await? .try_into()?, + performance: self + .manager + .load_contract(base_network.performance_contract_id) + .await? + .try_into()?, }, auxiliary_addresses: SpecialAddresses { ecash_holding_account: self @@ -304,6 +346,7 @@ impl NetworkManagerStorage { .load_account(&base_network.rewarder_address) .await? .try_into()?, + network_monitors, }, }) } diff --git a/tools/internal/testnet-manager/src/manager/storage/models.rs b/tools/internal/testnet-manager/src/manager/storage/models.rs index c224fc36d5..9df7ebb481 100644 --- a/tools/internal/testnet-manager/src/manager/storage/models.rs +++ b/tools/internal/testnet-manager/src/manager/storage/models.rs @@ -6,6 +6,14 @@ use crate::manager::contract::{Account, LoadedContract}; use sqlx::FromRow; use time::OffsetDateTime; +#[allow(dead_code)] +#[derive(FromRow)] +pub(crate) struct RawAuthorisedNetworkMonitor { + pub(crate) id: i64, + pub(crate) network_id: i64, + pub(crate) address: String, +} + #[derive(FromRow)] pub(crate) struct RawAccount { pub(crate) address: String, @@ -70,6 +78,7 @@ pub(crate) struct RawNetwork { pub(crate) cw3_multisig_contract_id: i64, pub(crate) cw4_group_contract_id: i64, pub(crate) dkg_contract_id: i64, + pub(crate) performance_contract_id: i64, pub(crate) rewarder_address: String, pub(crate) ecash_holding_account_address: String,