Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bf36f1367 | |||
| 5da1d89501 | |||
| d7090ecb5a | |||
| 1a1a2a0355 | |||
| 8bcc931d9b | |||
| 955ef7b871 | |||
| 9ee7ad4fa8 | |||
| 79aa1febbe | |||
| 32c4974c44 | |||
| 5dadd73dfd | |||
| 6ad5badef4 | |||
| 480ad18b2e | |||
| cdb21f418b | |||
| bd4c18c723 | |||
| e7716ae852 | |||
| b19528d47e | |||
| ef88ce9252 | |||
| 488f1c7742 | |||
| 63c698d903 | |||
| 7e1c3b4501 | |||
| e1d7772a78 | |||
| 9db589e0b3 | |||
| 57c8d69785 | |||
| 4a9cc5b757 | |||
| 06aac9393b | |||
| f4c5b1d67f | |||
| 2e0aba6100 | |||
| 8c6549c5dd | |||
| 7b431ebfa8 | |||
| 3152fd6887 | |||
| e03e7f186a | |||
| ec056a548f | |||
| 24aa075a07 | |||
| 8488c4cb0f | |||
| 20c96b940f | |||
| 7aad7daa5e |
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an enhancement to the product
|
||||
title: "[Feature Request]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is...
|
||||
|
||||
**Is your request a feature not related to an existing problem? A new feature.**
|
||||
For example.
|
||||
- Given I am using the nym wallet
|
||||
- When I transfer nym tokens across the network
|
||||
- Then I want to have an url link in the wallet which navigates outside the application to the nym-explorer
|
||||
|
||||
**Where does the feature fit in the Nym real estate?**
|
||||
- Application / UI
|
||||
|
||||
**What is this solving?**
|
||||
How will this improve the product...
|
||||
|
||||
**Is this an update to packages or libraries?**
|
||||
If so, please list them. If not, please ignore this section.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Report
|
||||
about: To help identify and reproduce issues
|
||||
title: "[Issue]"
|
||||
labels: bug, bug-needs-triage, qa
|
||||
assignees: tommyv1987
|
||||
|
||||
---
|
||||
|
||||
**Describe the issue**
|
||||
A clear and concise description of what the issue is...
|
||||
|
||||
**Expected behaviour**
|
||||
A clear and concise description of what you expected to happen...
|
||||
|
||||
**Stack Traces**
|
||||
If there are stack traces or logs, please provide them here...
|
||||
|
||||
**Steps to Reproduce**
|
||||
Steps to reproduce the behaviour, if you're familiar with BDD syntax, please write it in this style:
|
||||
- Given I was doing X
|
||||
- And I installed Y
|
||||
- When I actioned Y
|
||||
- Then I expect Z
|
||||
|
||||
*An example:*
|
||||
- Given I was setting up a mix-node following the instructions in the docs
|
||||
- And I successfully bonded my node via the the wallet
|
||||
- When I went to start my mixnode
|
||||
- Then I was presented with an error
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem...
|
||||
|
||||
**Which area of Nym were you using?**
|
||||
- UI: [e.g. Websites - network-explorer, nym-website]
|
||||
- Application: [e.g Gateway, Client, Wallet]
|
||||
- OS: [e.g. Ubuntu 20.x, MacOs Big Sur, Windows 10]
|
||||
- Browser: [e.g Chrome (if applicable)]
|
||||
- Version: [e.g. nym binary(0.11.0), browser(94.0)]
|
||||
|
||||
**Additional context**
|
||||
Please provide any other information
|
||||
@@ -1,136 +0,0 @@
|
||||
name: Continuous integration
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.os == 'windows-latest' }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust: [stable, beta, nightly]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --all
|
||||
|
||||
- name: Run all tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.rust != 'nightly' }}
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
||||
# COCONUT stuff
|
||||
- name: Reclaim some disk space (because Windows is being annoying)
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
with:
|
||||
command: clean
|
||||
|
||||
# BUILD
|
||||
- name: Build gateway with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --bin nym-gateway --features=coconut
|
||||
|
||||
- name: Build native client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --bin nym-client --features=coconut
|
||||
|
||||
- name: Build socks5 client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --bin nym-socks5-client --features=coconut
|
||||
|
||||
- name: Build validator-api with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --bin nym-validator-api --features=coconut
|
||||
|
||||
# TEST
|
||||
- name: Test gateway with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --bin nym-gateway --features=coconut
|
||||
|
||||
- name: Test native client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --bin nym-client --features=coconut
|
||||
|
||||
- name: Test socks5 client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --bin nym-socks5-client --features=coconut
|
||||
|
||||
- name: Test validator-api with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --bin nym-validator-api --features=coconut
|
||||
|
||||
# CLIPPY
|
||||
|
||||
- name: Run clippy on gateway with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --bin nym-gateway --features=coconut -- -D warnings
|
||||
|
||||
- name: Run clippy on native client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --bin nym-client --features=coconut -- -D warnings
|
||||
|
||||
- name: Run clippy on socks5 client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --bin nym-socks5-client --features=coconut -- -D warnings
|
||||
|
||||
- name: Run clippy on validator-api with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --bin nym-validator-api --features=coconut -- -D warnings
|
||||
@@ -1,14 +0,0 @@
|
||||
name: Clippy check
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
clippy_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- run: rustup component add clippy
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Mixnet Contract
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
mixnet-contract:
|
||||
# since it's going to be compiled into wasm, there's absolutely
|
||||
# no point in running CI on different OS-es
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust: [ stable, beta, nightly ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path contracts/mixnet/Cargo.toml --target wasm32-unknown-unknown
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path contracts/mixnet/Cargo.toml
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path contracts/mixnet/Cargo.toml -- --check
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.rust != 'nightly' }}
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path contracts/mixnet/Cargo.toml -- -D warnings
|
||||
@@ -1,56 +0,0 @@
|
||||
name: CI for Network Explorer
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'explorer/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: explorer
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- run: npm install
|
||||
continue-on-error: true
|
||||
- run: npm run test
|
||||
continue-on-error: true
|
||||
- run: npm run build
|
||||
continue-on-error: true
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "explorer/dist/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Keybase - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files/messages
|
||||
- name: Keybase - Send Notification
|
||||
env:
|
||||
NYM_PROJECT_NAME: "Network Explorer"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
|
||||
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
|
||||
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
|
||||
KEYBASE_NYM_CHANNEL: "ci-network-explorer"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/messages/entry_point_notifications.sh
|
||||
@@ -1,72 +0,0 @@
|
||||
name: Webdriverio tests for nym wallet
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tauri-wallet/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: tauri-wallet
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: wallet tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Tauri dependencies
|
||||
run: >
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y
|
||||
libgtk-3-dev
|
||||
libgtksourceview-3.0-dev
|
||||
webkit2gtk-4.0
|
||||
libappindicator3-dev
|
||||
webkit2gtk-driver
|
||||
xvfb
|
||||
|
||||
- name: Install minimal stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
|
||||
- name: Node v16
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Install yarn for building application
|
||||
run: yarn install
|
||||
|
||||
- name: Build application
|
||||
run: yarn run webpack:build & yarn run tauri:build
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
working-directory: tauri-wallet/webdriver
|
||||
|
||||
- name: Remove existing user datafile
|
||||
uses: JesseTG/rm@v1.0.2
|
||||
with:
|
||||
path: tauri-wallet/webdriver/common/data/user-data.json
|
||||
|
||||
- name: Create user data json file
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "user-data.json"
|
||||
json: ${{ secrets.WALLET_USERDATA }}
|
||||
dir: 'tauri-wallet/webdriver/common/data/'
|
||||
|
||||
- name: Install tauri-driver
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: tauri-driver
|
||||
|
||||
- name: Launch tests
|
||||
run: xvfb-run yarn test:newuser
|
||||
working-directory: tauri-wallet/webdriver
|
||||
@@ -1,5 +1,5 @@
|
||||
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
|
||||
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.GITHUB_REF_SLUG }}.{{ env.NYM_CI_WWW_BASE }}/
|
||||
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}/
|
||||
> ✅ **SUCCESS**
|
||||
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
|
||||
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
name: Publish Nym Wallet
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- tauri-wallet-*
|
||||
branches:
|
||||
- feature/gh-action-build-tauri-wallet
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: tauri-wallet
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, macos-10.15, macos-11, windows-latest]
|
||||
outputs:
|
||||
PACKAGE_VERSION: ${{ steps.package-node-version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Tauri dependencies
|
||||
if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
run: >
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y
|
||||
libgtk-3-dev
|
||||
libgtksourceview-3.0-dev
|
||||
webkit2gtk-4.0
|
||||
libappindicator3-dev
|
||||
webkit2gtk-driver
|
||||
xvfb
|
||||
|
||||
- name: Install minimal stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
|
||||
- name: Node v16
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Read node from package.json
|
||||
uses: culshaw/read-package-node-version-actions@v1
|
||||
with:
|
||||
path: "${{github.workspace}}/tauri-wallet"
|
||||
id: package-node-version
|
||||
|
||||
- name: Install yarn for building application
|
||||
run: yarn install
|
||||
|
||||
- name: Build application (bundle)
|
||||
run: yarn run webpack:build
|
||||
|
||||
- name: Build application (tauri)
|
||||
run: yarn run tauri:build
|
||||
|
||||
# - name: Fake build
|
||||
# run: |
|
||||
# mkdir release-assets
|
||||
# cp package.json release-assets/${{ matrix.os }}-${{ steps.package-node-version.outputs.version }}.zip
|
||||
|
||||
- name: Compress (MacOS / Linux)
|
||||
if: ${{ matrix.os != 'windows-latest' }}
|
||||
working-directory: tauri-wallet
|
||||
env:
|
||||
TARGET_FILE: ${{ matrix.os }}_v${{ steps.package-node-version.outputs.version }}.tar.gz
|
||||
run: |
|
||||
mkdir release-assets
|
||||
pushd target/release/bundle
|
||||
tar -cvzf ../../../release-assets/$TARGET_FILE .
|
||||
|
||||
- name: Compress (Windows)
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
uses: thedoctor0/zip-release@master
|
||||
with:
|
||||
type: 'zip'
|
||||
filename: '${{github.workspace}}/tauri-wallet/release-assets/${{ runner.os }}_v${{ steps.package-node-version.outputs.version }}.zip'
|
||||
path: "${{github.workspace}}/tauri-wallet/target/release/bundle"
|
||||
|
||||
- name: Upload release assets
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-assets-${{ matrix.os }}
|
||||
path: "${{github.workspace}}/tauri-wallet/release-assets/*"
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ needs.build.outputs.PACKAGE_VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download assets
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
path: download-assets
|
||||
- name: Display downloaded assets
|
||||
run: ls -R download-assets
|
||||
- name: Re-arrange downloaded assets
|
||||
run: |
|
||||
mkdir release-assets
|
||||
find download-assets -mindepth 2 -type f -exec mv -t ${{github.workspace}}/release-assets -i '{}' +
|
||||
rm -rf download-assets
|
||||
- name: Hash files
|
||||
id: file_hashes
|
||||
# Put the multi-line string in an env var and create a new action env var by wrapping the multiline value
|
||||
run: |
|
||||
cd release-assets
|
||||
ASSET_HASHES=$(find . -type f -exec sha256sum {} \;)
|
||||
echo "ASSET_HASHES<<EOF" >> $GITHUB_ENV
|
||||
echo "$ASSET_HASHES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Display file hashes
|
||||
run: echo $ASSET_HASHES
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
# The pipe syntax needs to stay to escape the wildcard correctly
|
||||
artifacts: |
|
||||
release-assets/*
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: "nym-wallet-v${{env.PACKAGE_VERSION}}"
|
||||
commit: feature/gh-action-build-tauri-wallet
|
||||
draft: true
|
||||
name: "Nym Wallet v${{ env.PACKAGE_VERSION }}"
|
||||
body: |
|
||||
This is a build of the Nym Wallet from commit `${{ github.sha }}` [(browse commit)](https://github.com/nymtech/nym/tree/${{ github.sha }}).
|
||||
|
||||
Installers for each operating system have the following SHA256 hashes:
|
||||
|
||||
```
|
||||
${{ env.ASSET_HASHES }}
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Generate TS types
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
tauri-wallet-types:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: sudo apt-get update && sudo apt-get install -y libpango1.0-dev libatk1.0-dev libgdk-pixbuf2.0-dev libsoup2.4-dev librust-gdk-dev libwebkit2gtk-4.0-dev
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Generate TS
|
||||
run: cd tauri-wallet/src-tauri && cargo test
|
||||
- uses: EndBug/add-and-commit@v7.2.1 # https://github.com/marketplace/actions/add-commit
|
||||
with:
|
||||
add: '["tauri-wallet"]'
|
||||
message: '[ci skip] Generate TS types'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Wasm Client
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
wasm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown --features=coconut
|
||||
|
||||
# for some reason this does not seem to work correctly, leave it for later, building is good enough for now
|
||||
# - uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: test
|
||||
# args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path clients/webassembly/Cargo.toml -- --check
|
||||
|
||||
# for some reason this does not seem to work correctly, leave it for later, building is good enough for now
|
||||
# - uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: clippy
|
||||
# args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown -- -D warnings
|
||||
@@ -11,7 +11,6 @@ target
|
||||
/.vscode/settings.json
|
||||
validator/.vscode
|
||||
sample-configs/validator-config.toml
|
||||
.vscode
|
||||
scripts/deploy_qa.sh
|
||||
scripts/run_gate.sh
|
||||
scripts/run_mix.sh
|
||||
|
||||
Generated
+87
-105
@@ -12,15 +12,6 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
@@ -66,15 +57,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anomaly"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "550632e31568ae1a5f47998c3aa48563030fc49b9ec91913ca337cf64fbc5ccb"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.11.0"
|
||||
@@ -246,21 +228,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"miniz_oxide 0.4.4",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base-x"
|
||||
version = "0.2.8"
|
||||
@@ -680,7 +647,7 @@ dependencies = [
|
||||
"ff",
|
||||
"getrandom 0.2.3",
|
||||
"group",
|
||||
"itertools 0.10.1",
|
||||
"itertools",
|
||||
"rand 0.8.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -871,9 +838,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmos-sdk-proto"
|
||||
version = "0.6.3"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7115eae4b8518967b8e737a3ef76025f2d007de7906d88c18f00b8bbfc70f51e"
|
||||
checksum = "edb5204c6ddc4352c74297638b5561f2929d6334866c156e5f3c75e1e1a1436a"
|
||||
dependencies = [
|
||||
"prost",
|
||||
"prost-types",
|
||||
@@ -882,9 +849,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmrs"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "609eba7eee6d9929927cbdf15f9fe96d250c316c5e1b983c4a47a10f60da5ea7"
|
||||
checksum = "d31147fe89e547e74e2692e4bec35387e1ea406fe8cfb14c0ea177b58fd2a8a9"
|
||||
dependencies = [
|
||||
"bip32",
|
||||
"cosmos-sdk-proto",
|
||||
@@ -895,6 +862,8 @@ dependencies = [
|
||||
"prost",
|
||||
"prost-types",
|
||||
"rand_core 0.6.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"subtle-encoding",
|
||||
"tendermint",
|
||||
"tendermint-rpc",
|
||||
@@ -1476,7 +1445,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"signature",
|
||||
]
|
||||
|
||||
@@ -1490,7 +1458,6 @@ dependencies = [
|
||||
"ed25519",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"sha2",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -1687,6 +1654,16 @@ dependencies = [
|
||||
"miniz_oxide 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flex-error"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b"
|
||||
dependencies = [
|
||||
"eyre",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluvio-wasm-timer"
|
||||
version = "0.2.5"
|
||||
@@ -2069,12 +2046,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.14.6"
|
||||
@@ -2551,6 +2522,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"webpki",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2711,15 +2683,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.1"
|
||||
@@ -3638,15 +3601,6 @@ dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "okapi"
|
||||
version = "0.6.0-alpha-1"
|
||||
@@ -3790,6 +3744,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.8.0"
|
||||
@@ -3822,6 +3782,33 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peg"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a"
|
||||
dependencies = [
|
||||
"peg-macros",
|
||||
"peg-runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peg-macros"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c"
|
||||
dependencies = [
|
||||
"peg-runtime",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peg-runtime"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "0.8.3"
|
||||
@@ -4171,9 +4158,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e6984d2f1a23009bd270b8bb56d0926810a3d483f59c987d77969e9d8e840b2"
|
||||
checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
@@ -4181,12 +4168,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "169a15f3008ecb5160cba7d37bcd690a7601b6d30cfb87a117d45e59d52af5d4"
|
||||
checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.9.0",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -4194,9 +4181,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b518d7cdd93dab1d1122cf07fa9a60771836c668dde9d9e2a139f957f0d9f1bb"
|
||||
checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost",
|
||||
@@ -4809,12 +4796,6 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.2.3"
|
||||
@@ -5376,7 +5357,7 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4"
|
||||
dependencies = [
|
||||
"itertools 0.10.1",
|
||||
"itertools",
|
||||
"nom",
|
||||
"unicode_categories",
|
||||
]
|
||||
@@ -5862,7 +5843,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
"itertools 0.10.1",
|
||||
"itertools",
|
||||
"pkg-config",
|
||||
"strum 0.21.0",
|
||||
"strum_macros 0.21.1",
|
||||
@@ -6077,16 +6058,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tendermint"
|
||||
version = "0.21.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0752808f59b612916614e92e43be64e9ba566b762a266bcfcf7dce1b324e9319"
|
||||
checksum = "50d1cdb0236becb17ab35a2ed1566503e412fd910944dc940239857bb7663652"
|
||||
dependencies = [
|
||||
"anomaly",
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"ed25519",
|
||||
"ed25519-dalek",
|
||||
"flex-error",
|
||||
"futures",
|
||||
"k256",
|
||||
"num-traits",
|
||||
@@ -6103,21 +6084,32 @@ dependencies = [
|
||||
"subtle 2.4.1",
|
||||
"subtle-encoding",
|
||||
"tendermint-proto",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-proto"
|
||||
version = "0.21.0"
|
||||
name = "tendermint-config"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfd0371c6b0c7fc4f6b053b8a1bc868a2a47c49a1408c4cd6f595d9f3bd3c238"
|
||||
checksum = "2a1f94250d30e3011130a09756b05985d8dbfbd562cf261b5a17e36d89a37992"
|
||||
dependencies = [
|
||||
"flex-error",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tendermint",
|
||||
"toml",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-proto"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff16a7b42bdbcf31c8cd10a4cffc7631f2a301360ba3a3f71dde48eabfa5bced"
|
||||
dependencies = [
|
||||
"anomaly",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"flex-error",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"prost",
|
||||
@@ -6125,30 +6117,32 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"subtle-encoding",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tendermint-rpc"
|
||||
version = "0.21.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fa8d172e3129802d28de980531993dd25cc01841c5bc8fe4a87c7750a7f222"
|
||||
checksum = "c7842dcd5edb60b077572aa92ff8b29fc810b9b463310f9810a2607474130db4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"flex-error",
|
||||
"futures",
|
||||
"getrandom 0.1.16",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-proxy",
|
||||
"hyper-rustls",
|
||||
"peg",
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"subtle-encoding",
|
||||
"tendermint",
|
||||
"tendermint-config",
|
||||
"tendermint-proto",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@@ -6430,21 +6424,9 @@ checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98863d0dd09fa59a1b79c6750ad80dbda6b75f4e71c437a6a1a8cb91a8bcbd77"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.20"
|
||||
@@ -6665,7 +6647,7 @@ dependencies = [
|
||||
"cosmrs",
|
||||
"cosmwasm-std",
|
||||
"flate2",
|
||||
"itertools 0.10.1",
|
||||
"itertools",
|
||||
"log",
|
||||
"mixnet-contract",
|
||||
"network-defaults",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
opt-level = "s"
|
||||
overflow-checks = true
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
@@ -257,7 +257,7 @@ impl TopologyRefresher {
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
let gateways = match self.validator_client.get_cached_active_gateways().await {
|
||||
let gateways = match self.validator_client.get_cached_gateways().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network gateways - {}", err);
|
||||
return None;
|
||||
|
||||
@@ -3,6 +3,7 @@ name = "nym-client"
|
||||
version = "0.11.0"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2018"
|
||||
rust-version = "1.56"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ name = "nym-socks5-client"
|
||||
version = "0.11.0"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
rust-version = "1.56"
|
||||
|
||||
[lib]
|
||||
name = "nym_socks5"
|
||||
|
||||
@@ -7,6 +7,7 @@ keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/nymtech/nym"
|
||||
description = "A webassembly client which can be used to interact with the the Nym privacy platform. Wasm is used for Sphinx packet generation."
|
||||
rust-version = "1.56"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
@@ -279,7 +279,7 @@ impl NymClient {
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
let gateways = match validator_client.get_cached_active_gateways().await {
|
||||
let gateways = match validator_client.get_cached_gateways().await {
|
||||
Err(err) => panic!("{}", err),
|
||||
Ok(gateways) => gateways,
|
||||
};
|
||||
|
||||
@@ -139,6 +139,11 @@ impl GatewayClient {
|
||||
self.gateway_identity
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
pub fn remaining_bandwidth(&self) -> i64 {
|
||||
self.bandwidth_remaining
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
|
||||
match std::mem::replace(&mut self.connection, SocketState::NotConnected) {
|
||||
|
||||
@@ -173,9 +173,9 @@ impl PartiallyDelegated {
|
||||
// this call failing is incredibly unlikely, but not impossible.
|
||||
// basically the gateway connection must have failed after executing previous line but
|
||||
// before starting execution of this one.
|
||||
if notify.send(()).is_err() {
|
||||
return Err(GatewayClientError::ConnectionAbruptlyClosed);
|
||||
}
|
||||
notify
|
||||
.send(())
|
||||
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?;
|
||||
|
||||
let stream_results: Result<_, GatewayClientError> = stream_receiver
|
||||
.await
|
||||
|
||||
@@ -3,6 +3,7 @@ name = "validator-client"
|
||||
version = "0.1.0"
|
||||
authors = ["Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2018"
|
||||
rust-version = "1.56"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -25,8 +26,8 @@ network-defaults = { path = "../../network-defaults" }
|
||||
async-trait = { version = "0.1.51", optional = true }
|
||||
bip39 = { version = "1", features = ["rand"], optional = true }
|
||||
config = { path = "../../config", optional = true }
|
||||
cosmrs = { version = "0.1", features = ["rpc", "bip32", "cosmwasm"], optional = true }
|
||||
prost = { version = "0.7", default-features = false, optional = true }
|
||||
cosmrs = { version = "0.3", features = ["rpc", "bip32", "cosmwasm"], optional = true }
|
||||
prost = { version = "0.9", default-features = false, optional = true }
|
||||
flate2 = { version = "1.0.20", optional = true }
|
||||
sha2 = { version = "0.9.5", optional = true }
|
||||
itertools = { version = "0.10", optional = true }
|
||||
|
||||
@@ -24,7 +24,6 @@ pub struct Config {
|
||||
mixnode_page_limit: Option<u32>,
|
||||
gateway_page_limit: Option<u32>,
|
||||
mixnode_delegations_page_limit: Option<u32>,
|
||||
gateway_delegations_page_limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "nymd-client")]
|
||||
@@ -41,7 +40,6 @@ impl Config {
|
||||
mixnode_page_limit: None,
|
||||
gateway_page_limit: None,
|
||||
mixnode_delegations_page_limit: None,
|
||||
gateway_delegations_page_limit: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +57,6 @@ impl Config {
|
||||
self.mixnode_delegations_page_limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_gateway_delegations_page_limit(mut self, limit: Option<u32>) -> Config {
|
||||
self.gateway_delegations_page_limit = limit;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nymd-client")]
|
||||
@@ -74,7 +67,6 @@ pub struct Client<C> {
|
||||
mixnode_page_limit: Option<u32>,
|
||||
gateway_page_limit: Option<u32>,
|
||||
mixnode_delegations_page_limit: Option<u32>,
|
||||
gateway_delegations_page_limit: Option<u32>,
|
||||
|
||||
// ideally they would have been read-only, but unfortunately rust doesn't have such features
|
||||
pub validator_api: validator_api::Client,
|
||||
@@ -100,7 +92,6 @@ impl Client<SigningNymdClient> {
|
||||
mixnode_page_limit: config.mixnode_page_limit,
|
||||
gateway_page_limit: config.gateway_page_limit,
|
||||
mixnode_delegations_page_limit: config.mixnode_delegations_page_limit,
|
||||
gateway_delegations_page_limit: config.gateway_delegations_page_limit,
|
||||
validator_api: validator_api_client,
|
||||
nymd: nymd_client,
|
||||
})
|
||||
@@ -136,7 +127,6 @@ impl Client<QueryNymdClient> {
|
||||
mixnode_page_limit: config.mixnode_page_limit,
|
||||
gateway_page_limit: config.gateway_page_limit,
|
||||
mixnode_delegations_page_limit: config.mixnode_delegations_page_limit,
|
||||
gateway_delegations_page_limit: config.gateway_delegations_page_limit,
|
||||
validator_api: validator_api_client,
|
||||
nymd: nymd_client,
|
||||
})
|
||||
@@ -339,116 +329,6 @@ impl<C> Client<C> {
|
||||
Ok(delegations)
|
||||
}
|
||||
|
||||
pub async fn get_all_nymd_single_gateway_delegations(
|
||||
&self,
|
||||
identity: mixnet_contract::IdentityKey,
|
||||
) -> Result<Vec<mixnet_contract::Delegation>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let mut delegations = Vec::new();
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let mut paged_response = self
|
||||
.nymd
|
||||
.get_gateway_delegations(
|
||||
identity.clone(),
|
||||
start_after.take(),
|
||||
self.gateway_delegations_page_limit,
|
||||
)
|
||||
.await?;
|
||||
delegations.append(&mut paged_response.delegations);
|
||||
|
||||
if let Some(start_after_res) = paged_response.start_next_after {
|
||||
start_after = Some(start_after_res)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(delegations)
|
||||
}
|
||||
|
||||
pub async fn get_all_nymd_gateway_delegations(
|
||||
&self,
|
||||
) -> Result<Vec<mixnet_contract::UnpackedDelegation<RawDelegationData>>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let mut delegations = Vec::new();
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let mut paged_response = self
|
||||
.nymd
|
||||
.get_all_gateway_delegations(
|
||||
start_after.take(),
|
||||
self.gateway_delegations_page_limit,
|
||||
)
|
||||
.await?;
|
||||
delegations.append(&mut paged_response.delegations);
|
||||
|
||||
if let Some(start_after_res) = paged_response.start_next_after {
|
||||
start_after = Some(start_after_res)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(delegations)
|
||||
}
|
||||
|
||||
pub async fn get_all_nymd_reverse_gateway_delegations(
|
||||
&self,
|
||||
delegation_owner: &cosmrs::AccountId,
|
||||
) -> Result<Vec<mixnet_contract::IdentityKey>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let mut delegations = Vec::new();
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let mut paged_response = self
|
||||
.nymd
|
||||
.get_reverse_gateway_delegations_paged(
|
||||
mixnet_contract::Addr::unchecked(delegation_owner.as_ref()),
|
||||
start_after.take(),
|
||||
self.mixnode_delegations_page_limit,
|
||||
)
|
||||
.await?;
|
||||
delegations.append(&mut paged_response.delegated_nodes);
|
||||
|
||||
if let Some(start_after_res) = paged_response.start_next_after {
|
||||
start_after = Some(start_after_res)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(delegations)
|
||||
}
|
||||
|
||||
pub async fn get_all_nymd_gateway_delegations_of_owner(
|
||||
&self,
|
||||
delegation_owner: &cosmrs::AccountId,
|
||||
) -> Result<Vec<mixnet_contract::Delegation>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let mut delegations = Vec::new();
|
||||
for node_identity in self
|
||||
.get_all_nymd_reverse_gateway_delegations(delegation_owner)
|
||||
.await?
|
||||
{
|
||||
let delegation = self
|
||||
.nymd
|
||||
.get_gateway_delegation(node_identity, delegation_owner)
|
||||
.await?;
|
||||
delegations.push(delegation);
|
||||
}
|
||||
|
||||
Ok(delegations)
|
||||
}
|
||||
|
||||
pub async fn blind_sign(
|
||||
&self,
|
||||
request_body: &BlindSignRequestBody,
|
||||
@@ -488,12 +368,6 @@ impl ApiClient {
|
||||
Ok(self.validator_api.get_active_mixnodes().await?)
|
||||
}
|
||||
|
||||
pub async fn get_cached_active_gateways(
|
||||
&self,
|
||||
) -> Result<Vec<GatewayBond>, ValidatorClientError> {
|
||||
Ok(self.validator_api.get_active_gateways().await?)
|
||||
}
|
||||
|
||||
pub async fn get_cached_mixnodes(&self) -> Result<Vec<MixNodeBond>, ValidatorClientError> {
|
||||
Ok(self.validator_api.get_mixnodes().await?)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use cosmrs::rpc::query::Query;
|
||||
use cosmrs::rpc::{self, HttpClient, Order};
|
||||
use cosmrs::tendermint::abci::Transaction;
|
||||
use cosmrs::tendermint::{abci, block, chain};
|
||||
use cosmrs::{AccountId, Coin, Denom};
|
||||
use cosmrs::{tx, AccountId, Coin, Denom};
|
||||
use prost::Message;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
@@ -153,12 +153,9 @@ pub trait CosmWasmClient: rpc::Client {
|
||||
.map_err(|_| NymdError::SerializationError("Coins".to_owned()))
|
||||
}
|
||||
|
||||
// disabled until https://github.com/tendermint/tendermint/issues/6802
|
||||
// and consequently https://github.com/informalsystems/tendermint-rs/issues/942 is resolved
|
||||
//
|
||||
// async fn get_tx(&self, id: tx::Hash) -> Result<TxResponse, NymdError> {
|
||||
// Ok(self.tx(id, false).await?)
|
||||
// }
|
||||
async fn get_tx(&self, id: tx::Hash) -> Result<TxResponse, NymdError> {
|
||||
Ok(self.tx(id, false).await?)
|
||||
}
|
||||
|
||||
async fn search_tx(&self, query: Query) -> Result<Vec<TxResponse>, NymdError> {
|
||||
// according to https://docs.tendermint.com/master/rpc/#/Info/tx_search
|
||||
|
||||
@@ -13,8 +13,8 @@ use cosmrs::distribution::MsgWithdrawDelegatorReward;
|
||||
use cosmrs::rpc::endpoint::broadcast;
|
||||
use cosmrs::rpc::{Error as TendermintRpcError, HttpClient, HttpClientUrl, SimpleRequest};
|
||||
use cosmrs::staking::{MsgDelegate, MsgUndelegate};
|
||||
use cosmrs::tx::{Fee, Msg, MsgType, SignDoc, SignerInfo};
|
||||
use cosmrs::{cosmwasm, rpc, tx, AccountId, Coin};
|
||||
use cosmrs::tx::{Fee, Msg, SignDoc, SignerInfo};
|
||||
use cosmrs::{cosmwasm, rpc, tx, AccountId, Any, Coin};
|
||||
use log::debug;
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
@@ -52,7 +52,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.unwrap_or_default(),
|
||||
instantiate_permission: Default::default(),
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgStoreCode".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
@@ -114,7 +114,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
init_msg: serde_json::to_vec(msg)?,
|
||||
funds: options.map(|options| options.funds).unwrap_or_default(),
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgInstantiateContract".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
@@ -154,7 +154,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
new_admin: new_admin.clone(),
|
||||
contract: contract_address.clone(),
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgUpdateAdmin".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
@@ -179,7 +179,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
sender: sender_address.clone(),
|
||||
contract: contract_address.clone(),
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgClearAdmin".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
@@ -211,7 +211,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
code_id,
|
||||
migrate_msg: serde_json::to_vec(msg)?,
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgMigrateContract".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
@@ -243,7 +243,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
msg: serde_json::to_vec(msg)?,
|
||||
funds,
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
@@ -278,7 +278,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
msg: serde_json::to_vec(&msg)?,
|
||||
funds,
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned()))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
@@ -312,7 +312,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
to_address: recipient_address.clone(),
|
||||
amount,
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?;
|
||||
|
||||
self.sign_and_broadcast_commit(sender_address, vec![send_msg], fee, memo)
|
||||
@@ -332,7 +332,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
validator_address: validator_address.to_owned(),
|
||||
amount,
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgDelegate".to_owned()))?;
|
||||
|
||||
self.sign_and_broadcast_commit(delegator_address, vec![delegate_msg], fee, memo)
|
||||
@@ -352,7 +352,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
validator_address: validator_address.to_owned(),
|
||||
amount: Some(amount),
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgUndelegate".to_owned()))?;
|
||||
|
||||
self.sign_and_broadcast_commit(delegator_address, vec![undelegate_msg], fee, memo)
|
||||
@@ -370,7 +370,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
delegator_address: delegator_address.to_owned(),
|
||||
validator_address: validator_address.to_owned(),
|
||||
}
|
||||
.to_msg()
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgWithdrawDelegatorReward".to_owned()))?;
|
||||
|
||||
self.sign_and_broadcast_commit(delegator_address, vec![withdraw_msg], fee, memo)
|
||||
@@ -381,7 +381,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
async fn sign_and_broadcast_async(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
messages: Vec<Msg>,
|
||||
messages: Vec<Any>,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_async::Response, NymdError> {
|
||||
@@ -397,7 +397,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
async fn sign_and_broadcast_sync(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
messages: Vec<Msg>,
|
||||
messages: Vec<Any>,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_sync::Response, NymdError> {
|
||||
@@ -413,7 +413,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
async fn sign_and_broadcast_commit(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
messages: Vec<Msg>,
|
||||
messages: Vec<Any>,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_commit::Response, NymdError> {
|
||||
@@ -428,7 +428,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
fn sign_direct(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
messages: Vec<Msg>,
|
||||
messages: Vec<Any>,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
signer_data: SignerData,
|
||||
@@ -466,7 +466,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
async fn sign(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
messages: Vec<Msg>,
|
||||
messages: Vec<Any>,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<tx::Raw, NymdError> {
|
||||
@@ -506,7 +506,7 @@ impl Client {
|
||||
|
||||
#[async_trait]
|
||||
impl rpc::Client for Client {
|
||||
async fn perform<R>(&self, request: R) -> rpc::Result<R::Response>
|
||||
async fn perform<R>(&self, request: R) -> Result<R::Response, rpc::Error>
|
||||
where
|
||||
R: SimpleRequest,
|
||||
{
|
||||
|
||||
@@ -7,7 +7,10 @@ use cosmrs::{bip32, tx, AccountId};
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use cosmrs::rpc::error::{Code, Error as TendermintRpcError};
|
||||
pub use cosmrs::rpc::error::{
|
||||
Error as TendermintRpcError, ErrorDetail as TendermintRpcErrorDetail,
|
||||
};
|
||||
pub use cosmrs::rpc::response_error::{Code, ResponseError};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NymdError {
|
||||
@@ -102,17 +105,21 @@ pub enum NymdError {
|
||||
}
|
||||
|
||||
impl NymdError {
|
||||
pub fn is_tendermint_timeout(&self) -> bool {
|
||||
pub fn is_tendermint_response_timeout(&self) -> bool {
|
||||
match &self {
|
||||
NymdError::TendermintError(tm_err) => {
|
||||
if tm_err.code() == Code::InternalError {
|
||||
NymdError::TendermintError(TendermintRpcError(
|
||||
TendermintRpcErrorDetail::Response(err),
|
||||
_,
|
||||
)) => {
|
||||
let response = &err.source;
|
||||
if response.code() == Code::InternalError {
|
||||
// 0.34 (and earlier) versions of tendermint seemed to be using phrase "timed out waiting ..."
|
||||
// (https://github.com/tendermint/tendermint/blob/v0.34.13/rpc/core/mempool.go#L124)
|
||||
// while 0.35+ has "timeout waiting for ..."
|
||||
// https://github.com/tendermint/tendermint/blob/v0.35.0-rc3/internal/rpc/core/mempool.go#L99
|
||||
// note that as of the time of writing this comment (08.10.2021), the most recent version
|
||||
// of cosmos-sdk (v0.44.1) uses tendermint 0.34.13
|
||||
if let Some(data) = tm_err.data() {
|
||||
if let Some(data) = response.data() {
|
||||
data.contains("timed out") || data.contains("timeout")
|
||||
} else {
|
||||
false
|
||||
@@ -125,14 +132,18 @@ impl NymdError {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tendermint_duplicate(&self) -> bool {
|
||||
pub fn is_tendermint_response_duplicate(&self) -> bool {
|
||||
match &self {
|
||||
NymdError::TendermintError(tm_err) => {
|
||||
if tm_err.code() == Code::InternalError {
|
||||
NymdError::TendermintError(TendermintRpcError(
|
||||
TendermintRpcErrorDetail::Response(err),
|
||||
_,
|
||||
)) => {
|
||||
let response = &err.source;
|
||||
if response.code() == Code::InternalError {
|
||||
// this particular error message seems to be unchanged between 0.34 and newer versions
|
||||
// https://github.com/tendermint/tendermint/blob/v0.34.13/mempool/errors.go#L10
|
||||
// https://github.com/tendermint/tendermint/blob/v0.35.0-rc3/types/mempool.go#L10
|
||||
if let Some(data) = tm_err.data() {
|
||||
if let Some(data) = response.data() {
|
||||
data.contains("tx already exists in cache")
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -15,8 +15,7 @@ use cosmwasm_std::Coin;
|
||||
use mixnet_contract::{
|
||||
Addr, Delegation, ExecuteMsg, Gateway, GatewayOwnershipResponse, IdentityKey,
|
||||
LayerDistribution, MixNode, MixOwnershipResponse, PagedAllDelegationsResponse,
|
||||
PagedGatewayDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse,
|
||||
PagedMixnodeResponse, PagedReverseGatewayDelegationsResponse,
|
||||
PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse,
|
||||
PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData, StateParams,
|
||||
};
|
||||
use serde::Serialize;
|
||||
@@ -354,82 +353,6 @@ impl<C> NymdClient<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Gets list of all delegations towards particular gateway on particular page.
|
||||
pub async fn get_gateway_delegations(
|
||||
&self,
|
||||
gateway_identity: IdentityKey,
|
||||
start_after: Option<Addr>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedGatewayDelegationsResponse, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let request = QueryMsg::GetGatewayDelegations {
|
||||
gateway_identity,
|
||||
start_after,
|
||||
limit: page_limit,
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.contract_address()?, &request)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Gets list of all gateway delegations on particular page.
|
||||
pub async fn get_all_gateway_delegations(
|
||||
&self,
|
||||
start_after: Option<Vec<u8>>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedAllDelegationsResponse<RawDelegationData>, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let request = QueryMsg::GetAllGatewayDelegations {
|
||||
start_after,
|
||||
limit: page_limit,
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.contract_address()?, &request)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Gets list of all the gateways on which a particular address delegated.
|
||||
pub async fn get_reverse_gateway_delegations_paged(
|
||||
&self,
|
||||
delegation_owner: Addr,
|
||||
start_after: Option<IdentityKey>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedReverseGatewayDelegationsResponse, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let request = QueryMsg::GetReverseGatewayDelegations {
|
||||
delegation_owner,
|
||||
start_after,
|
||||
limit: page_limit,
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.contract_address()?, &request)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Checks value of delegation of given client towards particular gateway.
|
||||
pub async fn get_gateway_delegation(
|
||||
&self,
|
||||
gateway_identity: IdentityKey,
|
||||
delegator: &AccountId,
|
||||
) -> Result<Delegation, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let request = QueryMsg::GetGatewayDelegation {
|
||||
gateway_identity,
|
||||
address: Addr::unchecked(delegator.as_ref()),
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.contract_address()?, &request)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send funds from one address to another
|
||||
pub async fn send(
|
||||
&self,
|
||||
@@ -698,57 +621,6 @@ impl<C> NymdClient<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Delegates specified amount of stake to particular gateway.
|
||||
pub async fn delegate_to_gateway(
|
||||
&self,
|
||||
gateway_identity: &str,
|
||||
amount: &Coin,
|
||||
) -> Result<ExecuteResult, NymdError>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
{
|
||||
let fee = self.get_fee(Operation::DelegateToGateway);
|
||||
|
||||
let req = ExecuteMsg::DelegateToGateway {
|
||||
gateway_identity: gateway_identity.to_string(),
|
||||
};
|
||||
self.client
|
||||
.execute(
|
||||
self.address(),
|
||||
self.contract_address()?,
|
||||
&req,
|
||||
fee,
|
||||
"Delegating to gateway from rust!",
|
||||
vec![cosmwasm_coin_ptr_to_cosmos_coin(amount)],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Removes stake delegation from a particular gateway.
|
||||
pub async fn remove_gateway_delegation(
|
||||
&self,
|
||||
gateway_identity: &str,
|
||||
) -> Result<ExecuteResult, NymdError>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
{
|
||||
let fee = self.get_fee(Operation::UndelegateFromGateway);
|
||||
|
||||
let req = ExecuteMsg::UndelegateFromGateway {
|
||||
gateway_identity: gateway_identity.to_string(),
|
||||
};
|
||||
self.client
|
||||
.execute(
|
||||
self.address(),
|
||||
self.contract_address()?,
|
||||
&req,
|
||||
fee,
|
||||
"Removing gateway delegation from rust!",
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_state_params(
|
||||
&self,
|
||||
new_params: StateParams,
|
||||
|
||||
@@ -73,11 +73,6 @@ impl Client {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_active_gateways(&self) -> Result<Vec<GatewayBond>, ValidatorAPIError> {
|
||||
self.query_validator_api(&[routes::API_VERSION, routes::GATEWAYS, routes::ACTIVE])
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn blind_sign(
|
||||
&self,
|
||||
request_body: &BlindSignRequestBody,
|
||||
|
||||
@@ -5,18 +5,13 @@ use digest::{BlockInput, FixedOutput, Reset, Update};
|
||||
use generic_array::ArrayLength;
|
||||
use hkdf::Hkdf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HkdfError {
|
||||
InvalidOkmLength,
|
||||
}
|
||||
|
||||
/// Perform HKDF `extract` then `expand` as a single step.
|
||||
pub fn extract_then_expand<D>(
|
||||
salt: Option<&[u8]>,
|
||||
ikm: &[u8],
|
||||
info: Option<&[u8]>,
|
||||
okm_length: usize,
|
||||
) -> Result<Vec<u8>, HkdfError>
|
||||
) -> Result<Vec<u8>, hkdf::InvalidLength>
|
||||
where
|
||||
D: Update + BlockInput + FixedOutput + Reset + Default + Clone,
|
||||
D::BlockSize: ArrayLength<u8>,
|
||||
@@ -27,9 +22,7 @@ where
|
||||
|
||||
let hkdf = Hkdf::<D>::new(salt, ikm);
|
||||
let mut okm = vec![0u8; okm_length];
|
||||
if hkdf.expand(info.unwrap_or_else(|| &[]), &mut okm).is_err() {
|
||||
return Err(HkdfError::InvalidOkmLength);
|
||||
}
|
||||
hkdf.expand(info.unwrap_or_else(|| &[]), &mut okm)?;
|
||||
|
||||
Ok(okm)
|
||||
}
|
||||
|
||||
@@ -120,48 +120,6 @@ impl PagedReverseMixDelegationsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct PagedGatewayDelegationsResponse {
|
||||
pub node_identity: IdentityKey,
|
||||
pub delegations: Vec<Delegation>,
|
||||
pub start_next_after: Option<Addr>,
|
||||
}
|
||||
|
||||
impl PagedGatewayDelegationsResponse {
|
||||
pub fn new(
|
||||
node_identity: IdentityKey,
|
||||
delegations: Vec<Delegation>,
|
||||
start_next_after: Option<Addr>,
|
||||
) -> Self {
|
||||
PagedGatewayDelegationsResponse {
|
||||
node_identity,
|
||||
delegations,
|
||||
start_next_after,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct PagedReverseGatewayDelegationsResponse {
|
||||
pub delegation_owner: Addr,
|
||||
pub delegated_nodes: Vec<IdentityKey>,
|
||||
pub start_next_after: Option<IdentityKey>,
|
||||
}
|
||||
|
||||
impl PagedReverseGatewayDelegationsResponse {
|
||||
pub fn new(
|
||||
delegation_owner: Addr,
|
||||
delegated_nodes: Vec<IdentityKey>,
|
||||
start_next_after: Option<IdentityKey>,
|
||||
) -> Self {
|
||||
PagedReverseGatewayDelegationsResponse {
|
||||
delegation_owner,
|
||||
delegated_nodes,
|
||||
start_next_after,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct PagedAllDelegationsResponse<T> {
|
||||
pub delegations: Vec<UnpackedDelegation<T>>,
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
#![allow(clippy::field_reassign_with_default)]
|
||||
|
||||
use crate::{IdentityKey, SphinxKey};
|
||||
use cosmwasm_std::{coin, Addr, Coin};
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::current_block_height;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)]
|
||||
pub struct Gateway {
|
||||
pub host: String,
|
||||
@@ -26,9 +24,7 @@ pub struct Gateway {
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct GatewayBond {
|
||||
pub bond_amount: Coin,
|
||||
pub total_delegation: Coin,
|
||||
pub owner: Addr,
|
||||
#[serde(default = "current_block_height")]
|
||||
pub block_height: u64,
|
||||
pub gateway: Gateway,
|
||||
}
|
||||
@@ -36,7 +32,6 @@ pub struct GatewayBond {
|
||||
impl GatewayBond {
|
||||
pub fn new(bond_amount: Coin, owner: Addr, block_height: u64, gateway: Gateway) -> Self {
|
||||
GatewayBond {
|
||||
total_delegation: coin(0, &bond_amount.denom),
|
||||
bond_amount,
|
||||
owner,
|
||||
block_height,
|
||||
@@ -64,27 +59,11 @@ impl GatewayBond {
|
||||
impl PartialOrd for GatewayBond {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
// first remove invalid cases
|
||||
if self.bond_amount.denom != self.total_delegation.denom {
|
||||
return None;
|
||||
}
|
||||
|
||||
if other.bond_amount.denom != other.total_delegation.denom {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.bond_amount.denom != other.bond_amount.denom {
|
||||
return None;
|
||||
}
|
||||
|
||||
// try to order by total bond + delegation
|
||||
let total_cmp = (self.bond_amount.amount + self.total_delegation.amount)
|
||||
.partial_cmp(&(self.bond_amount.amount + self.total_delegation.amount))?;
|
||||
|
||||
if total_cmp != Ordering::Equal {
|
||||
return Some(total_cmp);
|
||||
}
|
||||
|
||||
// then if those are equal, prefer higher bond over delegation
|
||||
// try to order by total bond
|
||||
let bond_cmp = self
|
||||
.bond_amount
|
||||
.amount
|
||||
@@ -93,15 +72,6 @@ impl PartialOrd for GatewayBond {
|
||||
return Some(bond_cmp);
|
||||
}
|
||||
|
||||
// then look at delegation (I'm not sure we can get here, but better safe than sorry)
|
||||
let delegation_cmp = self
|
||||
.total_delegation
|
||||
.amount
|
||||
.partial_cmp(&other.total_delegation.amount)?;
|
||||
if delegation_cmp != Ordering::Equal {
|
||||
return Some(delegation_cmp);
|
||||
}
|
||||
|
||||
// then check block height
|
||||
let height_cmp = self.block_height.partial_cmp(&other.block_height)?;
|
||||
if height_cmp != Ordering::Equal {
|
||||
@@ -175,20 +145,19 @@ mod tests {
|
||||
#[test]
|
||||
fn gateway_bond_partial_ord() {
|
||||
let _150foos = Coin::new(150, "foo");
|
||||
let _140foos = Coin::new(140, "foo");
|
||||
let _50foos = Coin::new(50, "foo");
|
||||
let _0foos = Coin::new(0, "foo");
|
||||
|
||||
let gate1 = GatewayBond {
|
||||
bond_amount: _150foos.clone(),
|
||||
total_delegation: _50foos.clone(),
|
||||
owner: Addr::unchecked("foo1"),
|
||||
block_height: 100,
|
||||
gateway: gateway_fixture(),
|
||||
};
|
||||
|
||||
let gate2 = GatewayBond {
|
||||
bond_amount: _150foos.clone(),
|
||||
total_delegation: _50foos.clone(),
|
||||
bond_amount: _150foos,
|
||||
owner: Addr::unchecked("foo2"),
|
||||
block_height: 120,
|
||||
gateway: gateway_fixture(),
|
||||
@@ -196,15 +165,13 @@ mod tests {
|
||||
|
||||
let gate3 = GatewayBond {
|
||||
bond_amount: _50foos,
|
||||
total_delegation: _150foos.clone(),
|
||||
owner: Addr::unchecked("foo3"),
|
||||
block_height: 120,
|
||||
gateway: gateway_fixture(),
|
||||
};
|
||||
|
||||
let gate4 = GatewayBond {
|
||||
bond_amount: _150foos.clone(),
|
||||
total_delegation: _0foos.clone(),
|
||||
bond_amount: _140foos,
|
||||
owner: Addr::unchecked("foo4"),
|
||||
block_height: 120,
|
||||
gateway: gateway_fixture(),
|
||||
@@ -212,21 +179,19 @@ mod tests {
|
||||
|
||||
let gate5 = GatewayBond {
|
||||
bond_amount: _0foos,
|
||||
total_delegation: _150foos,
|
||||
owner: Addr::unchecked("foo5"),
|
||||
block_height: 120,
|
||||
gateway: gateway_fixture(),
|
||||
};
|
||||
|
||||
// summary:
|
||||
// gate1: 150bond + 50delegation, foo1, 100
|
||||
// gate2: 150bond + 50delegation, foo2, 120
|
||||
// gate3: 50bond + 150delegation, foo3, 120
|
||||
// gate4: 150bond + 0delegation, foo4, 120
|
||||
// gate5: 0bond + 150delegation, foo5, 120
|
||||
// gate1: 150bond, foo1, 100
|
||||
// gate2: 150bond, foo2, 120
|
||||
// gate3: 50bond, foo3, 120
|
||||
// gate4: 140bond, foo4, 120
|
||||
// gate5: 0bond, foo5, 120
|
||||
|
||||
// highest total bond+delegation is used
|
||||
// then bond followed by delegation
|
||||
// highest total bond is used
|
||||
// finally just the rest of the fields
|
||||
|
||||
// gate1 has higher total than gate4 or gate5
|
||||
|
||||
@@ -9,18 +9,10 @@ mod types;
|
||||
|
||||
pub use cosmwasm_std::{Addr, Coin};
|
||||
pub use delegation::{
|
||||
Delegation, PagedAllDelegationsResponse, PagedGatewayDelegationsResponse,
|
||||
PagedMixDelegationsResponse, PagedReverseGatewayDelegationsResponse,
|
||||
Delegation, PagedAllDelegationsResponse, PagedMixDelegationsResponse,
|
||||
PagedReverseMixDelegationsResponse, RawDelegationData, UnpackedDelegation,
|
||||
};
|
||||
pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse};
|
||||
pub use mixnode::{Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse};
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::{IdentityKey, IdentityKeyRef, LayerDistribution, SphinxKey, StateParams};
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
pub static CURRENT_BLOCK_HEIGHT: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub fn current_block_height() -> u64 {
|
||||
CURRENT_BLOCK_HEIGHT.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::current_block_height;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema, TS)]
|
||||
pub struct MixNode {
|
||||
pub host: String,
|
||||
@@ -25,7 +23,17 @@ pub struct MixNode {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize_repr, PartialEq, PartialOrd, Deserialize_repr, JsonSchema,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize_repr,
|
||||
Deserialize_repr,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum Layer {
|
||||
@@ -41,7 +49,6 @@ pub struct MixNodeBond {
|
||||
pub total_delegation: Coin,
|
||||
pub owner: Addr,
|
||||
pub layer: Layer,
|
||||
#[serde(default = "current_block_height")]
|
||||
pub block_height: u64,
|
||||
pub mix_node: MixNode,
|
||||
}
|
||||
|
||||
@@ -31,25 +31,11 @@ pub enum ExecuteMsg {
|
||||
mix_identity: IdentityKey,
|
||||
},
|
||||
|
||||
DelegateToGateway {
|
||||
gateway_identity: IdentityKey,
|
||||
},
|
||||
|
||||
UndelegateFromGateway {
|
||||
gateway_identity: IdentityKey,
|
||||
},
|
||||
|
||||
RewardMixnode {
|
||||
identity: IdentityKey,
|
||||
// percentage value in range 0-100
|
||||
uptime: u32,
|
||||
},
|
||||
|
||||
RewardGateway {
|
||||
identity: IdentityKey,
|
||||
// percentage value in range 0-100
|
||||
uptime: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
@@ -88,24 +74,6 @@ pub enum QueryMsg {
|
||||
mix_identity: IdentityKey,
|
||||
address: Addr,
|
||||
},
|
||||
GetGatewayDelegations {
|
||||
gateway_identity: IdentityKey,
|
||||
start_after: Option<Addr>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
GetAllGatewayDelegations {
|
||||
start_after: Option<Vec<u8>>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
GetReverseGatewayDelegations {
|
||||
delegation_owner: Addr,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
GetGatewayDelegation {
|
||||
gateway_identity: IdentityKey,
|
||||
address: Addr,
|
||||
},
|
||||
LayerDistribution {},
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,10 @@ pub struct StateParams {
|
||||
|
||||
pub minimum_mixnode_bond: Uint128, // minimum amount a mixnode must bond to get into the system
|
||||
pub minimum_gateway_bond: Uint128, // minimum amount a gateway must bond to get into the system
|
||||
|
||||
pub mixnode_bond_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
|
||||
pub gateway_bond_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
|
||||
pub mixnode_delegation_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
|
||||
pub gateway_delegation_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
|
||||
pub mixnode_active_set_size: u32,
|
||||
pub gateway_active_set_size: u32,
|
||||
}
|
||||
|
||||
impl Display for StateParams {
|
||||
@@ -51,30 +49,15 @@ impl Display for StateParams {
|
||||
"mixnode bond reward rate: {}; ",
|
||||
self.mixnode_bond_reward_rate
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
"gateway bond reward rate: {}; ",
|
||||
self.gateway_bond_reward_rate
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
"mixnode delegation reward rate: {}; ",
|
||||
self.mixnode_delegation_reward_rate
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
"gateway delegation reward rate: {}; ",
|
||||
self.gateway_delegation_reward_rate
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
"mixnode active set size: {}",
|
||||
self.mixnode_active_set_size
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
"gateway active set size: {} ]",
|
||||
self.gateway_active_set_size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,11 +156,9 @@ impl MessageReceiver {
|
||||
};
|
||||
|
||||
// Finally, remove the zero padding from the message
|
||||
if Self::remove_padding(&mut message).is_err() {
|
||||
return Err(MessageRecoveryError::MalformedReconstructedMessage(
|
||||
used_sets,
|
||||
));
|
||||
};
|
||||
Self::remove_padding(&mut message).map_err(|_| {
|
||||
MessageRecoveryError::MalformedReconstructedMessage(used_sets.clone())
|
||||
})?;
|
||||
|
||||
Ok(Some((
|
||||
ReconstructedMessage {
|
||||
@@ -270,7 +268,6 @@ mod message_receiver {
|
||||
vec![gateway::Node {
|
||||
owner: "foomp4".to_string(),
|
||||
stake: 123,
|
||||
delegation: 456,
|
||||
location: "unknown".to_string(),
|
||||
host: "1.2.3.4".parse().unwrap(),
|
||||
mix_host: "1.2.3.4:1789".parse().unwrap(),
|
||||
|
||||
@@ -72,7 +72,6 @@ pub struct Node {
|
||||
// somebody correct me if I'm wrong, but we should only ever have a single denom of currency
|
||||
// on the network at a type, right?
|
||||
pub stake: u128,
|
||||
pub delegation: u128,
|
||||
pub location: String,
|
||||
pub host: NetworkAddress,
|
||||
// we're keeping this as separate resolved field since we do not want to be resolving the potential
|
||||
@@ -127,7 +126,6 @@ impl<'a> TryFrom<&'a GatewayBond> for Node {
|
||||
Ok(Node {
|
||||
owner: bond.owner.as_str().to_owned(),
|
||||
stake: bond.bond_amount.amount.into(),
|
||||
delegation: bond.total_delegation.amount.into(),
|
||||
location: bond.gateway.location.clone(),
|
||||
host,
|
||||
mix_host,
|
||||
|
||||
@@ -22,18 +22,13 @@ pub const INITIAL_MIXNODE_BOND: Uint128 = Uint128(100_000000);
|
||||
|
||||
// percentage annual increase. Given starting value of x, we expect to have 1.1x at the end of the year
|
||||
pub const INITIAL_MIXNODE_BOND_REWARD_RATE: u64 = 110;
|
||||
pub const INITIAL_GATEWAY_BOND_REWARD_RATE: u64 = 110;
|
||||
pub const INITIAL_MIXNODE_DELEGATION_REWARD_RATE: u64 = 110;
|
||||
pub const INITIAL_GATEWAY_DELEGATION_REWARD_RATE: u64 = 110;
|
||||
|
||||
pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100;
|
||||
pub const INITIAL_GATEWAY_ACTIVE_SET_SIZE: u32 = 20;
|
||||
|
||||
fn default_initial_state(owner: Addr) -> State {
|
||||
let mixnode_bond_reward_rate = Decimal::percent(INITIAL_MIXNODE_BOND_REWARD_RATE);
|
||||
let gateway_bond_reward_rate = Decimal::percent(INITIAL_GATEWAY_BOND_REWARD_RATE);
|
||||
let mixnode_delegation_reward_rate = Decimal::percent(INITIAL_MIXNODE_DELEGATION_REWARD_RATE);
|
||||
let gateway_delegation_reward_rate = Decimal::percent(INITIAL_GATEWAY_DELEGATION_REWARD_RATE);
|
||||
|
||||
State {
|
||||
owner,
|
||||
@@ -43,28 +38,17 @@ fn default_initial_state(owner: Addr) -> State {
|
||||
minimum_mixnode_bond: INITIAL_MIXNODE_BOND,
|
||||
minimum_gateway_bond: INITIAL_GATEWAY_BOND,
|
||||
mixnode_bond_reward_rate,
|
||||
gateway_bond_reward_rate,
|
||||
mixnode_delegation_reward_rate,
|
||||
gateway_delegation_reward_rate,
|
||||
mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE,
|
||||
gateway_active_set_size: INITIAL_GATEWAY_ACTIVE_SET_SIZE,
|
||||
},
|
||||
mixnode_epoch_bond_reward: calculate_epoch_reward_rate(
|
||||
INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
mixnode_bond_reward_rate,
|
||||
),
|
||||
gateway_epoch_bond_reward: calculate_epoch_reward_rate(
|
||||
INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
gateway_bond_reward_rate,
|
||||
),
|
||||
mixnode_epoch_delegation_reward: calculate_epoch_reward_rate(
|
||||
INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
mixnode_delegation_reward_rate,
|
||||
),
|
||||
gateway_epoch_delegation_reward: calculate_epoch_reward_rate(
|
||||
INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
gateway_delegation_reward_rate,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,21 +94,12 @@ pub fn execute(
|
||||
ExecuteMsg::RewardMixnode { identity, uptime } => {
|
||||
transactions::try_reward_mixnode(deps, env, info, identity, uptime)
|
||||
}
|
||||
ExecuteMsg::RewardGateway { identity, uptime } => {
|
||||
transactions::try_reward_gateway(deps, env, info, identity, uptime)
|
||||
}
|
||||
ExecuteMsg::DelegateToMixnode { mix_identity } => {
|
||||
transactions::try_delegate_to_mixnode(deps, env, info, mix_identity)
|
||||
}
|
||||
ExecuteMsg::UndelegateFromMixnode { mix_identity } => {
|
||||
transactions::try_remove_delegation_from_mixnode(deps, info, mix_identity)
|
||||
}
|
||||
ExecuteMsg::DelegateToGateway { gateway_identity } => {
|
||||
transactions::try_delegate_to_gateway(deps, env, info, gateway_identity)
|
||||
}
|
||||
ExecuteMsg::UndelegateFromGateway { gateway_identity } => {
|
||||
transactions::try_remove_delegation_from_gateway(deps, info, gateway_identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,37 +151,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, Cont
|
||||
mix_identity,
|
||||
address,
|
||||
)?),
|
||||
QueryMsg::GetGatewayDelegations {
|
||||
gateway_identity,
|
||||
start_after,
|
||||
limit,
|
||||
} => to_binary(&queries::query_gateway_delegations_paged(
|
||||
deps,
|
||||
gateway_identity,
|
||||
start_after,
|
||||
limit,
|
||||
)?),
|
||||
QueryMsg::GetAllGatewayDelegations { start_after, limit } => to_binary(
|
||||
&queries::query_all_gateway_delegations_paged(deps, start_after, limit)?,
|
||||
),
|
||||
QueryMsg::GetReverseGatewayDelegations {
|
||||
delegation_owner,
|
||||
start_after,
|
||||
limit,
|
||||
} => to_binary(&queries::query_reverse_gateway_delegations_paged(
|
||||
deps,
|
||||
delegation_owner,
|
||||
start_after,
|
||||
limit,
|
||||
)?),
|
||||
QueryMsg::GetGatewayDelegation {
|
||||
gateway_identity,
|
||||
address,
|
||||
} => to_binary(&queries::query_gateway_delegation(
|
||||
deps,
|
||||
gateway_identity,
|
||||
address,
|
||||
)?),
|
||||
};
|
||||
|
||||
Ok(query_res?)
|
||||
|
||||
@@ -48,15 +48,9 @@ pub enum ContractError {
|
||||
#[error("The bond reward rate for mixnode was set to be lower than 1")]
|
||||
DecreasingMixnodeBondReward,
|
||||
|
||||
#[error("The bond reward rate for gateway was set to be lower than 1")]
|
||||
DecreasingGatewayBondReward,
|
||||
|
||||
#[error("The delegation reward rate for mixnode was set to be lower than 1")]
|
||||
DecreasingMixnodeDelegationReward,
|
||||
|
||||
#[error("The delegation reward rate for gateway was set to be lower than 1")]
|
||||
DecreasingGatewayDelegationReward,
|
||||
|
||||
#[error("The node had uptime larger than 100%")]
|
||||
UnexpectedUptime,
|
||||
|
||||
@@ -83,10 +77,4 @@ pub enum ContractError {
|
||||
identity: IdentityKey,
|
||||
address: Addr,
|
||||
},
|
||||
|
||||
#[error("Could not find any delegation information associated with gateway {identity} for {address}")]
|
||||
NoGatewayDelegationFound {
|
||||
identity: IdentityKey,
|
||||
address: Addr,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -209,10 +209,7 @@ impl<'a, T: Clone + Serialize + DeserializeOwned> Iterator for Delegations<'a, T
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::queries::tests::store_n_mix_delegations;
|
||||
use crate::storage::{
|
||||
all_gateway_delegations_read, all_mix_delegations_read, gateway_delegations,
|
||||
mix_delegations,
|
||||
};
|
||||
use crate::storage::{all_mix_delegations_read, mix_delegations};
|
||||
use crate::support::tests::helpers;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use mixnet_contract::RawDelegationData;
|
||||
@@ -389,66 +386,4 @@ mod tests {
|
||||
UnpackedDelegation::new(delegation_owner2, node_identity2, raw_delegation.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_gateway_delegations() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar1");
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
let raw_delegation = RawDelegationData::new(1000u128.into(), 42);
|
||||
let mut start_after = None;
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity1)
|
||||
.save(delegation_owner1.as_bytes(), &raw_delegation)
|
||||
.unwrap();
|
||||
|
||||
let bucket = all_gateway_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let response =
|
||||
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
|
||||
start_after = response.start_next_after;
|
||||
let delegations = response.delegations;
|
||||
assert_eq!(delegations.len(), 1);
|
||||
assert_eq!(
|
||||
delegations[0],
|
||||
UnpackedDelegation::new(
|
||||
delegation_owner1.clone(),
|
||||
node_identity1.clone(),
|
||||
raw_delegation.clone()
|
||||
)
|
||||
);
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity2)
|
||||
.save(delegation_owner2.as_bytes(), &raw_delegation)
|
||||
.unwrap();
|
||||
|
||||
let bucket = all_gateway_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let response =
|
||||
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
|
||||
start_after = response.start_next_after;
|
||||
let delegations = response.delegations;
|
||||
assert_eq!(delegations.len(), 2);
|
||||
assert_eq!(
|
||||
delegations[1],
|
||||
UnpackedDelegation::new(
|
||||
delegation_owner2.clone(),
|
||||
node_identity2.clone(),
|
||||
raw_delegation.clone()
|
||||
)
|
||||
);
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity1)
|
||||
.remove(delegation_owner1.as_bytes());
|
||||
|
||||
let bucket = all_gateway_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let response =
|
||||
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
|
||||
let delegations = response.delegations;
|
||||
assert_eq!(delegations.len(), 1);
|
||||
assert_eq!(
|
||||
delegations[0],
|
||||
UnpackedDelegation::new(delegation_owner2, node_identity2, raw_delegation.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,17 @@
|
||||
use crate::error::ContractError;
|
||||
use crate::helpers::get_all_delegations_paged;
|
||||
use crate::storage::{
|
||||
all_gateway_delegations_read, all_mix_delegations_read, gateway_delegations_read,
|
||||
gateways_owners_read, gateways_read, mix_delegations_read, mixnodes_owners_read, mixnodes_read,
|
||||
read_layer_distribution, read_state_params, reverse_gateway_delegations_read,
|
||||
all_mix_delegations_read, gateways_owners_read, gateways_read, mix_delegations_read,
|
||||
mixnodes_owners_read, mixnodes_read, read_layer_distribution, read_state_params,
|
||||
reverse_mix_delegations_read,
|
||||
};
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{coin, Addr, Deps, Order, StdResult};
|
||||
use mixnet_contract::{
|
||||
Delegation, GatewayBond, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNodeBond,
|
||||
MixOwnershipResponse, PagedAllDelegationsResponse, PagedGatewayDelegationsResponse,
|
||||
PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse,
|
||||
PagedReverseGatewayDelegationsResponse, PagedReverseMixDelegationsResponse, RawDelegationData,
|
||||
StateParams,
|
||||
MixOwnershipResponse, PagedAllDelegationsResponse, PagedGatewayResponse,
|
||||
PagedMixDelegationsResponse, PagedMixnodeResponse, PagedReverseMixDelegationsResponse,
|
||||
RawDelegationData, StateParams,
|
||||
};
|
||||
|
||||
const BOND_PAGE_MAX_LIMIT: u32 = 100;
|
||||
@@ -211,114 +209,11 @@ pub(crate) fn query_mixnode_delegation(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn query_gateway_delegations_paged(
|
||||
deps: Deps,
|
||||
gateway_identity: IdentityKey,
|
||||
start_after: Option<Addr>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedGatewayDelegationsResponse> {
|
||||
let limit = limit
|
||||
.unwrap_or(DELEGATION_PAGE_DEFAULT_LIMIT)
|
||||
.min(DELEGATION_PAGE_MAX_LIMIT) as usize;
|
||||
let start = calculate_start_value(start_after);
|
||||
|
||||
let delegations = gateway_delegations_read(deps.storage, &gateway_identity)
|
||||
.range(start.as_deref(), None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|res| {
|
||||
res.map(|entry| {
|
||||
Delegation::new(
|
||||
Addr::unchecked(String::from_utf8(entry.0).expect(
|
||||
"Non-UTF8 address used as key in bucket. The storage is corrupted!",
|
||||
)),
|
||||
coin(entry.1.amount.u128(), DENOM),
|
||||
entry.1.block_height,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<Vec<Delegation>>>()?;
|
||||
|
||||
let start_next_after = delegations.last().map(|delegation| delegation.owner());
|
||||
|
||||
Ok(PagedGatewayDelegationsResponse::new(
|
||||
gateway_identity,
|
||||
delegations,
|
||||
start_next_after,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn query_all_gateway_delegations_paged(
|
||||
deps: Deps,
|
||||
start_after: Option<Vec<u8>>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedAllDelegationsResponse<RawDelegationData>> {
|
||||
let limit = limit
|
||||
.unwrap_or(DELEGATION_PAGE_DEFAULT_LIMIT)
|
||||
.min(DELEGATION_PAGE_MAX_LIMIT) as usize;
|
||||
|
||||
let bucket = all_gateway_delegations_read::<RawDelegationData>(deps.storage);
|
||||
let start = start_after.map(|mut v| {
|
||||
v.push(0);
|
||||
v
|
||||
});
|
||||
get_all_delegations_paged::<RawDelegationData>(&bucket, &start, limit)
|
||||
}
|
||||
|
||||
pub(crate) fn query_reverse_gateway_delegations_paged(
|
||||
deps: Deps,
|
||||
delegation_owner: Addr,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedReverseGatewayDelegationsResponse> {
|
||||
let limit = limit
|
||||
.unwrap_or(DELEGATION_PAGE_DEFAULT_LIMIT)
|
||||
.min(DELEGATION_PAGE_MAX_LIMIT) as usize;
|
||||
let start = calculate_start_value(start_after);
|
||||
|
||||
let delegations = reverse_gateway_delegations_read(deps.storage, &delegation_owner)
|
||||
.range(start.as_deref(), None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|res| {
|
||||
res.map(|entry| {
|
||||
String::from_utf8(entry.0)
|
||||
.expect("Non-UTF8 address used as key in bucket. The storage is corrupted!")
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<Vec<IdentityKey>>>()?;
|
||||
|
||||
let start_next_after = delegations.last().cloned();
|
||||
|
||||
Ok(PagedReverseGatewayDelegationsResponse::new(
|
||||
delegation_owner,
|
||||
delegations,
|
||||
start_next_after,
|
||||
))
|
||||
}
|
||||
|
||||
// queries for delegation value of given address for particular node
|
||||
pub(crate) fn query_gateway_delegation(
|
||||
deps: Deps,
|
||||
gateway_identity: IdentityKey,
|
||||
address: Addr,
|
||||
) -> Result<Delegation, ContractError> {
|
||||
match gateway_delegations_read(deps.storage, &gateway_identity).may_load(address.as_bytes())? {
|
||||
Some(delegation_value) => Ok(Delegation::new(
|
||||
address,
|
||||
coin(delegation_value.amount.u128(), DENOM),
|
||||
delegation_value.block_height,
|
||||
)),
|
||||
None => Err(ContractError::NoGatewayDelegationFound {
|
||||
identity: gateway_identity,
|
||||
address,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::state::State;
|
||||
use crate::storage::{config, gateway_delegations, gateways, mix_delegations, mixnodes};
|
||||
use crate::storage::{config, gateways, mix_delegations, mixnodes};
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::{
|
||||
good_gateway_bond, good_mixnode_bond, raw_delegation_fixture,
|
||||
@@ -673,16 +568,11 @@ pub(crate) mod tests {
|
||||
minimum_mixnode_bond: 123u128.into(),
|
||||
minimum_gateway_bond: 456u128.into(),
|
||||
mixnode_bond_reward_rate: "1.23".parse().unwrap(),
|
||||
gateway_bond_reward_rate: "4.56".parse().unwrap(),
|
||||
mixnode_delegation_reward_rate: "7.89".parse().unwrap(),
|
||||
gateway_delegation_reward_rate: "0.12".parse().unwrap(),
|
||||
mixnode_active_set_size: 1000,
|
||||
gateway_active_set_size: 20,
|
||||
},
|
||||
mixnode_epoch_bond_reward: "1.23".parse().unwrap(),
|
||||
gateway_epoch_bond_reward: "4.56".parse().unwrap(),
|
||||
mixnode_epoch_delegation_reward: "7.89".parse().unwrap(),
|
||||
gateway_epoch_delegation_reward: "0.12".parse().unwrap(),
|
||||
};
|
||||
|
||||
config(deps.as_mut().storage).save(&dummy_state).unwrap();
|
||||
@@ -1203,524 +1093,4 @@ pub(crate) mod tests {
|
||||
assert_eq!(2, page2.delegated_nodes.len());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_n_gateway_delegations(
|
||||
n: u32,
|
||||
storage: &mut dyn Storage,
|
||||
node_identity: &IdentityKey,
|
||||
) {
|
||||
for i in 0..n {
|
||||
let address = format!("address{}", i);
|
||||
gateway_delegations(storage, node_identity)
|
||||
.save(address.as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod querying_for_gateway_delegations_paged {
|
||||
use super::*;
|
||||
use crate::storage::gateway_delegations;
|
||||
|
||||
#[test]
|
||||
fn retrieval_obeys_limits() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let limit = 2;
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_gateway_delegations(100, &mut deps.storage, &node_identity);
|
||||
|
||||
let page1 = query_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity,
|
||||
None,
|
||||
Option::from(limit),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(limit, page1.delegations.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_default_limit() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_gateway_delegations(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 =
|
||||
query_gateway_delegations_paged(deps.as_ref(), node_identity, None, None).unwrap();
|
||||
assert_eq!(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT,
|
||||
page1.delegations.len() as u32
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_max_limit() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_gateway_delegations(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000 * DELEGATION_PAGE_DEFAULT_LIMIT;
|
||||
let page1 = query_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity,
|
||||
None,
|
||||
Option::from(crazy_limit),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = DELEGATION_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.delegations.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save("1".as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
let per_page = 2;
|
||||
let page1 = query_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.delegations.len());
|
||||
|
||||
// save another
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save("2".as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 = query_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegations.len());
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save("3".as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 = query_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegations.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after = Addr::unchecked("2");
|
||||
let page2 = query_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity.clone(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.delegations.len());
|
||||
|
||||
// save another one
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save("4".as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
let start_after = Addr::unchecked("2");
|
||||
let page2 = query_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity,
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.delegations.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_deletion_query_returns_current_delegation_value() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
let delegation_owner = Addr::unchecked("bar");
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegation_owner.as_bytes(),
|
||||
&&RawDelegationData::new(42u128.into(), 12_345),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Ok(Delegation::new(
|
||||
delegation_owner.clone(),
|
||||
coin(42, DENOM),
|
||||
12_345
|
||||
)),
|
||||
query_gateway_delegation(deps.as_ref(), node_identity, delegation_owner)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_deletion_query_returns_error_if_delegation_doesnt_exist() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar");
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
|
||||
assert_eq!(
|
||||
Err(ContractError::NoGatewayDelegationFound {
|
||||
identity: node_identity1.clone(),
|
||||
address: delegation_owner1.clone(),
|
||||
}),
|
||||
query_gateway_delegation(
|
||||
deps.as_ref(),
|
||||
node_identity1.clone(),
|
||||
delegation_owner1.clone()
|
||||
)
|
||||
);
|
||||
|
||||
// add delegation from a different address
|
||||
gateway_delegations(&mut deps.storage, &node_identity1)
|
||||
.save(delegation_owner2.as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Err(ContractError::NoGatewayDelegationFound {
|
||||
identity: node_identity1.clone(),
|
||||
address: delegation_owner1.clone(),
|
||||
}),
|
||||
query_gateway_delegation(
|
||||
deps.as_ref(),
|
||||
node_identity1.clone(),
|
||||
delegation_owner1.clone()
|
||||
)
|
||||
);
|
||||
|
||||
// add delegation for a different node
|
||||
gateway_delegations(&mut deps.storage, &node_identity2)
|
||||
.save(delegation_owner1.as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Err(ContractError::NoGatewayDelegationFound {
|
||||
identity: node_identity1.clone(),
|
||||
address: delegation_owner1.clone()
|
||||
}),
|
||||
query_gateway_delegation(deps.as_ref(), node_identity1, delegation_owner1)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod querying_for_all_gateway_delegations_paged {
|
||||
use super::*;
|
||||
use crate::helpers::identity_and_owner_to_bytes;
|
||||
use crate::storage::gateway_delegations;
|
||||
|
||||
#[test]
|
||||
fn retrieval_obeys_limits() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let limit = 2;
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_gateway_delegations(100, &mut deps.storage, &node_identity);
|
||||
|
||||
let page1 =
|
||||
query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(limit))
|
||||
.unwrap();
|
||||
assert_eq!(limit, page1.delegations.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_default_limit() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_gateway_delegations(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 = query_all_gateway_delegations_paged(deps.as_ref(), None, None).unwrap();
|
||||
assert_eq!(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT,
|
||||
page1.delegations.len() as u32
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_max_limit() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_gateway_delegations(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000 * DELEGATION_PAGE_DEFAULT_LIMIT;
|
||||
let page1 =
|
||||
query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(crazy_limit))
|
||||
.unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = DELEGATION_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.delegations.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save("1".as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
let per_page = 2;
|
||||
let page1 =
|
||||
query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(per_page))
|
||||
.unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.delegations.len());
|
||||
|
||||
// save another
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save("2".as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 =
|
||||
query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(per_page))
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegations.len());
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save("3".as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 =
|
||||
query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(per_page))
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegations.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after = identity_and_owner_to_bytes(&node_identity, &Addr::unchecked("2"));
|
||||
let page2 = query_all_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after.clone()),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.delegations.len());
|
||||
|
||||
// save another one
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save("4".as_bytes(), &raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
let page2 = query_all_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.delegations.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod querying_for_reverse_gateway_delegations_paged {
|
||||
use super::*;
|
||||
use crate::storage::reverse_gateway_delegations;
|
||||
|
||||
fn store_n_reverse_delegations(n: u32, storage: &mut dyn Storage, delegation_owner: &Addr) {
|
||||
for i in 0..n {
|
||||
let node_identity = format!("node{}", i);
|
||||
reverse_gateway_delegations(storage, delegation_owner)
|
||||
.save(node_identity.as_bytes(), &())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_obeys_limits() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let limit = 2;
|
||||
let delegation_owner = Addr::unchecked("foo");
|
||||
store_n_reverse_delegations(100, &mut deps.storage, &delegation_owner);
|
||||
|
||||
let page1 = query_reverse_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner,
|
||||
None,
|
||||
Option::from(limit),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(limit, page1.delegated_nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_default_limit() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let delegation_owner = Addr::unchecked("foo");
|
||||
store_n_reverse_delegations(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&delegation_owner,
|
||||
);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 = query_reverse_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT,
|
||||
page1.delegated_nodes.len() as u32
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_max_limit() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let delegation_owner = Addr::unchecked("foo");
|
||||
store_n_reverse_delegations(
|
||||
DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&delegation_owner,
|
||||
);
|
||||
|
||||
// query with a crazy high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000 * DELEGATION_PAGE_DEFAULT_LIMIT;
|
||||
let page1 = query_reverse_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner,
|
||||
None,
|
||||
Option::from(crazy_limit),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = DELEGATION_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.delegated_nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let delegation_owner = Addr::unchecked("bar");
|
||||
|
||||
reverse_gateway_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save("1".as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
let per_page = 2;
|
||||
let page1 = query_reverse_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.delegated_nodes.len());
|
||||
|
||||
// save another
|
||||
reverse_gateway_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save("2".as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 = query_reverse_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegated_nodes.len());
|
||||
|
||||
reverse_gateway_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save("3".as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 = query_reverse_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegated_nodes.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after: IdentityKey = String::from("2");
|
||||
let page2 = query_reverse_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner.clone(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.delegated_nodes.len());
|
||||
|
||||
// save another one
|
||||
reverse_gateway_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save("4".as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
let start_after = String::from("2");
|
||||
let page2 = query_reverse_gateway_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner,
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.delegated_nodes.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,5 @@ pub struct State {
|
||||
|
||||
// helper values to avoid having to recalculate them on every single payment operation
|
||||
pub mixnode_epoch_bond_reward: Decimal, // reward per epoch expressed as a decimal like 0.05
|
||||
pub gateway_epoch_bond_reward: Decimal, // reward per epoch expressed as a decimal like 0.05
|
||||
pub mixnode_epoch_delegation_reward: Decimal, // reward per epoch expressed as a decimal like 0.05
|
||||
pub gateway_epoch_delegation_reward: Decimal, // reward per epoch expressed as a decimal like 0.05
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ const PREFIX_GATEWAYS: &[u8] = b"gt";
|
||||
const PREFIX_GATEWAYS_OWNERS: &[u8] = b"go";
|
||||
|
||||
const PREFIX_MIX_DELEGATION: &[u8] = b"md";
|
||||
const PREFIX_GATEWAY_DELEGATION: &[u8] = b"gd";
|
||||
const PREFIX_REVERSE_MIX_DELEGATION: &[u8] = b"dm";
|
||||
const PREFIX_REVERSE_GATEWAY_DELEGATION: &[u8] = b"dg";
|
||||
|
||||
// Contract-level stuff
|
||||
|
||||
@@ -63,14 +61,6 @@ pub(crate) fn read_mixnode_epoch_bond_reward_rate(storage: &dyn Storage) -> Deci
|
||||
.mixnode_epoch_bond_reward
|
||||
}
|
||||
|
||||
pub(crate) fn read_gateway_epoch_bond_reward_rate(storage: &dyn Storage) -> Decimal {
|
||||
// same justification as in `read_state_params` for the unwrap
|
||||
config_read(storage)
|
||||
.load()
|
||||
.unwrap()
|
||||
.gateway_epoch_bond_reward
|
||||
}
|
||||
|
||||
pub(crate) fn read_mixnode_epoch_delegation_reward_rate(storage: &dyn Storage) -> Decimal {
|
||||
// same justification as in `read_state_params` for the unwrap
|
||||
config_read(storage)
|
||||
@@ -79,14 +69,6 @@ pub(crate) fn read_mixnode_epoch_delegation_reward_rate(storage: &dyn Storage) -
|
||||
.mixnode_epoch_delegation_reward
|
||||
}
|
||||
|
||||
pub(crate) fn read_gateway_epoch_delegation_reward_rate(storage: &dyn Storage) -> Decimal {
|
||||
// same justification as in `read_state_params` for the unwrap
|
||||
config_read(storage)
|
||||
.load()
|
||||
.unwrap()
|
||||
.gateway_epoch_delegation_reward
|
||||
}
|
||||
|
||||
pub fn layer_distribution(storage: &mut dyn Storage) -> Singleton<LayerDistribution> {
|
||||
singleton(storage, LAYER_DISTRIBUTION_KEY)
|
||||
}
|
||||
@@ -214,56 +196,6 @@ pub(crate) fn increase_mix_delegated_stakes(
|
||||
Ok(total_rewarded)
|
||||
}
|
||||
|
||||
pub(crate) fn increase_gateway_delegated_stakes(
|
||||
storage: &mut dyn Storage,
|
||||
gateway_identity: IdentityKeyRef,
|
||||
scaled_reward_rate: Decimal,
|
||||
reward_blockstamp: u64,
|
||||
) -> StdResult<Uint128> {
|
||||
let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize;
|
||||
|
||||
let mut total_rewarded = Uint128::zero();
|
||||
let mut chunk_start: Option<Vec<_>> = None;
|
||||
loop {
|
||||
// get `chunk_size` of delegations
|
||||
let delegations_chunk = gateway_delegations_read(storage, gateway_identity)
|
||||
.range(chunk_start.as_deref(), None, Order::Ascending)
|
||||
.take(chunk_size)
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
if delegations_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// append 0 byte to the last value to start with whatever is the next suceeding key
|
||||
chunk_start = Some(
|
||||
delegations_chunk
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(0u8))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// and for each of them increase the stake proportionally to the reward
|
||||
// if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created
|
||||
// since they delegated
|
||||
for (delegator_address, mut delegation) in delegations_chunk.into_iter() {
|
||||
if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= reward_blockstamp {
|
||||
let reward = delegation.amount * scaled_reward_rate;
|
||||
delegation.amount += reward;
|
||||
total_rewarded += reward;
|
||||
gateway_delegations(storage, gateway_identity)
|
||||
.save(&delegator_address, &delegation)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_rewarded)
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_mixnode_bond(
|
||||
@@ -338,53 +270,6 @@ pub fn reverse_mix_delegations_read<'a>(
|
||||
ReadonlyBucket::multilevel(storage, &[PREFIX_REVERSE_MIX_DELEGATION, owner.as_bytes()])
|
||||
}
|
||||
|
||||
pub fn all_gateway_delegations_read<T>(storage: &dyn Storage) -> ReadonlyBucket<T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
bucket_read(storage, PREFIX_GATEWAY_DELEGATION)
|
||||
}
|
||||
|
||||
pub fn gateway_delegations<'a>(
|
||||
storage: &'a mut dyn Storage,
|
||||
gateway_identity: IdentityKeyRef,
|
||||
) -> Bucket<'a, RawDelegationData> {
|
||||
Bucket::multilevel(
|
||||
storage,
|
||||
&[PREFIX_GATEWAY_DELEGATION, gateway_identity.as_bytes()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn gateway_delegations_read<'a>(
|
||||
storage: &'a dyn Storage,
|
||||
gateway_identity: IdentityKeyRef,
|
||||
) -> ReadonlyBucket<'a, RawDelegationData> {
|
||||
ReadonlyBucket::multilevel(
|
||||
storage,
|
||||
&[PREFIX_GATEWAY_DELEGATION, gateway_identity.as_bytes()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn reverse_gateway_delegations<'a>(
|
||||
storage: &'a mut dyn Storage,
|
||||
owner: &Addr,
|
||||
) -> Bucket<'a, ()> {
|
||||
Bucket::multilevel(
|
||||
storage,
|
||||
&[PREFIX_REVERSE_GATEWAY_DELEGATION, owner.as_bytes()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn reverse_gateway_delegations_read<'a>(
|
||||
storage: &'a dyn Storage,
|
||||
owner: &Addr,
|
||||
) -> ReadonlyBucket<'a, ()> {
|
||||
ReadonlyBucket::multilevel(
|
||||
storage,
|
||||
&[PREFIX_REVERSE_GATEWAY_DELEGATION, owner.as_bytes()],
|
||||
)
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_gateway_bond(
|
||||
@@ -396,17 +281,6 @@ pub(crate) fn read_gateway_bond(
|
||||
Ok(node.bond_amount.amount)
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_gateway_delegation(
|
||||
storage: &dyn Storage,
|
||||
identity: &[u8],
|
||||
) -> StdResult<cosmwasm_std::Uint128> {
|
||||
let bucket = gateways_read(storage);
|
||||
let node = bucket.load(identity)?;
|
||||
Ok(node.total_delegation.amount)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -499,7 +373,6 @@ mod tests {
|
||||
|
||||
let gateway_bond = GatewayBond {
|
||||
bond_amount: coin(bond_value, DENOM),
|
||||
total_delegation: coin(0, DENOM),
|
||||
owner: node_owner.clone(),
|
||||
block_height: 12_345,
|
||||
gateway: Gateway {
|
||||
@@ -551,39 +424,6 @@ mod tests {
|
||||
assert_eq!(raw_delegation2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_gateway_delegations_read_retrieval() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar1");
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
let raw_delegation1 = RawDelegationData::new(1u128.into(), 1000);
|
||||
let raw_delegation2 = RawDelegationData::new(2u128.into(), 2000);
|
||||
|
||||
gateway_delegations(&mut deps.storage, &node_identity1)
|
||||
.save(delegation_owner1.as_bytes(), &raw_delegation1)
|
||||
.unwrap();
|
||||
gateway_delegations(&mut deps.storage, &node_identity2)
|
||||
.save(delegation_owner2.as_bytes(), &raw_delegation2)
|
||||
.unwrap();
|
||||
|
||||
let res1 = all_gateway_delegations_read::<RawDelegationData>(&deps.storage)
|
||||
.load(&*identity_and_owner_to_bytes(
|
||||
&node_identity1,
|
||||
&delegation_owner1,
|
||||
))
|
||||
.unwrap();
|
||||
let res2 = all_gateway_delegations_read::<RawDelegationData>(&deps.storage)
|
||||
.load(&*identity_and_owner_to_bytes(
|
||||
&node_identity2,
|
||||
&delegation_owner2,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(raw_delegation1, res1);
|
||||
assert_eq!(raw_delegation2, res2);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod increasing_mix_delegated_stakes {
|
||||
use super::*;
|
||||
@@ -858,279 +698,4 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod increasing_gateway_delegated_stakes {
|
||||
use super::*;
|
||||
use crate::queries::query_gateway_delegations_paged;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
|
||||
#[test]
|
||||
fn when_there_are_no_delegations() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let total_increase = increase_gateway_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
42,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there was no increase
|
||||
assert!(total_increase.is_zero());
|
||||
|
||||
// there are no 'new' delegations magically added
|
||||
assert!(
|
||||
query_gateway_delegations_paged(deps.as_ref(), node_identity, None, None)
|
||||
.unwrap()
|
||||
.delegations
|
||||
.is_empty()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_is_a_single_delegation() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let delegator_address = Addr::unchecked("bob");
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let total_increase = increase_gateway_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Uint128(1), total_increase);
|
||||
|
||||
// amount is incremented, block height remains the same
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1001u128.into(), 42),
|
||||
gateway_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_is_a_single_delegation_depending_on_blockstamp() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let delegator_address = Addr::unchecked("bob");
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let total_increase = increase_gateway_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING - 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there was no increase
|
||||
assert!(total_increase.is_zero());
|
||||
|
||||
// amount is not incremented
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
gateway_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let total_increase = increase_gateway_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there is an increase now, that the lock period has passed
|
||||
assert_eq!(Uint128(1), total_increase);
|
||||
|
||||
// amount is incremented
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1001u128.into(), delegation_blockstamp),
|
||||
gateway_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_are_multiple_delegations() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
for i in 0..100 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let total_increase = increase_gateway_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Uint128(100), total_increase);
|
||||
|
||||
for i in 0..100 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
assert_eq!(
|
||||
raw_delegation_fixture(1001),
|
||||
gateway_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_are_more_delegations_than_page_size() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
gateway_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let total_increase = increase_gateway_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(queries::DELEGATION_PAGE_MAX_LIMIT as u128 * 10),
|
||||
total_increase
|
||||
);
|
||||
|
||||
for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
assert_eq!(
|
||||
raw_delegation_fixture(1001),
|
||||
gateway_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reverse_gateway_delegations {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers;
|
||||
|
||||
#[test]
|
||||
fn reverse_gateway_delegation_exists() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
let delegation_owner = Addr::unchecked("bar");
|
||||
|
||||
reverse_gateway_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save(node_identity.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_gateway_delegations_read(deps.as_ref().storage, &delegation_owner)
|
||||
.may_load(node_identity.as_bytes())
|
||||
.unwrap()
|
||||
.is_some(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_gateway_delegation_returns_none_if_delegation_doesnt_exist() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar");
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
|
||||
assert!(
|
||||
reverse_gateway_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// add delegation for a different node
|
||||
reverse_gateway_delegations(&mut deps.storage, &delegation_owner1)
|
||||
.save(node_identity2.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_gateway_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// add delegation from a different owner
|
||||
reverse_gateway_delegations(&mut deps.storage, &delegation_owner2)
|
||||
.save(node_identity1.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_gateway_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/env", "@babel/react"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"plugins": ["react", "react-hooks", "jsx-a11y", "prettier", "jest"],
|
||||
"extends": ["plugin:react/recommended", "airbnb", "prettier", "plugin:jest/recommended", "plugin:jest/style"],
|
||||
"rules": {
|
||||
"jest/prefer-strict-equal": "error",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"prettier/prettier": "error",
|
||||
"import/prefer-default-export": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-filename-extension": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"**/*.test.[jt]s",
|
||||
"**/*.spec.[jt]s",
|
||||
"**/*.test.[jt]sx",
|
||||
"**/*.spec.[jt]sx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"ts": "never",
|
||||
"tsx": "never",
|
||||
"js": "never",
|
||||
"jsx": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.+(ts|tsx)",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint/eslint-plugin"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"no-use-before-define": [0],
|
||||
"@typescript-eslint/no-use-before-define": [1],
|
||||
"import/no-unresolved": 0,
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.tsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"quotes": "off",
|
||||
"@typescript-eslint/quotes": [
|
||||
2,
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }]
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"root-import": {
|
||||
"rootPathPrefix": "@",
|
||||
"rootPathSuffix": "src",
|
||||
"extensions": [".js", ".ts", ".tsx", ".jsx", ".mdx"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
.DS_Store
|
||||
detailAPI.md
|
||||
@@ -0,0 +1 @@
|
||||
14
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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`
|
||||
|
||||
From the `explorer` directory of the `nym` monorepo, run:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
|
||||
You can then open a browser to http://localhost:3000 and start development.
|
||||
|
||||
## Development
|
||||
|
||||
Documentation for developers [can be found here](./docs).
|
||||
|
||||
## Deployment
|
||||
|
||||
Build the UI with:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run 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.
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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
|
||||
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
Generated
+30149
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"name": "@nym/network-explorer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@cosmjs/math": "^0.26.2",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@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.2",
|
||||
"@nymproject/nym-validator-client": "^0.18.0",
|
||||
"d3-scale": "^4.0.0",
|
||||
"date-fns": "^2.24.0",
|
||||
"i18n-iso-countries": "^6.8.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-google-charts": "^3.0.15",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-tooltip": "^4.2.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-rc.3",
|
||||
"@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": "^17.0.34",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/react-simple-maps": "^1.0.6",
|
||||
"@types/react-tooltip": "^4.2.4",
|
||||
"@types/topojson-client": "^3.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.31.0",
|
||||
"@typescript-eslint/parser": "4.31.0",
|
||||
"babel-plugin-root-import": "^6.6.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^6.2.0",
|
||||
"css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-airbnb": "18.2.1",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-import-resolver-root-import": "1.0.4",
|
||||
"eslint-plugin-import": "2.24.2",
|
||||
"eslint-plugin-jest": "^24.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"eslint-plugin-react": "7.25.1",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"imports-loader": "^3.0.0",
|
||||
"jest": "^27.1.0",
|
||||
"loader-utils": "^2.0.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"prettier": "2.3.2",
|
||||
"react-refresh": "^0.10.0",
|
||||
"react-refresh-typescript": "^2.0.2",
|
||||
"style-loader": "^3.2.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "^4.4.2",
|
||||
"webpack": "^5.52.0",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.1.0",
|
||||
"webpack-favicons": "^1.0.7",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"yaml-loader": "^0.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --progress --port 3000",
|
||||
"build": "webpack build --progress --config webpack.prod.js",
|
||||
"build:serve": "npx serve dist",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"tsc": "tsc",
|
||||
"tsc:watch": "tsc --watch",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { Nav } from './components/Nav';
|
||||
import { Routes } from './routes/index';
|
||||
|
||||
export const App: React.FC = () => (
|
||||
<Nav>
|
||||
<Routes />
|
||||
</Nav>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
// master APIs
|
||||
export const MASTER_URL = 'https://testnet-milhon-explorer.nymtech.net';
|
||||
export const MASTER_VALIDATOR_URL =
|
||||
'https://testnet-milhon-validator1.nymtech.net';
|
||||
export const BIG_DIPPER = 'https://testnet-milhon-blocks.nymtech.net';
|
||||
|
||||
// specific API routes
|
||||
export const MIXNODE_PING = `${MASTER_URL}/api/ping`;
|
||||
export const MIXNODES_API = `${MASTER_URL}/api/mix-node`;
|
||||
export const GATEWAYS_API = `${MASTER_VALIDATOR_URL}/api/v1/gateways`;
|
||||
export const VALIDATORS_API = `${MASTER_VALIDATOR_URL}/validators`;
|
||||
export const BLOCK_API = `${MASTER_VALIDATOR_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${MASTER_URL}/api/countries`;
|
||||
export const UPTIME_STORY_API = `${MASTER_VALIDATOR_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
|
||||
// errors
|
||||
export const MIXNODE_API_ERROR =
|
||||
"We're having trouble finding that record, please try again or Contact Us.";
|
||||
|
||||
// socials
|
||||
export const TELEGRAM_LINK = 'https://t.me/nymchan';
|
||||
export const TWITTER_LINK = 'https://twitter.com/nymproject';
|
||||
export const GITHUB_LINK = 'https://github.com/nymtech';
|
||||
export const NYM_WEBSITE = 'https://nymtech.net';
|
||||
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
GATEWAYS_API,
|
||||
MIXNODES_API,
|
||||
VALIDATORS_API,
|
||||
BLOCK_API,
|
||||
COUNTRY_DATA_API,
|
||||
MIXNODE_PING,
|
||||
UPTIME_STORY_API,
|
||||
} from './constants';
|
||||
|
||||
import {
|
||||
MixNodeResponse,
|
||||
GatewayResponse,
|
||||
ValidatorsResponse,
|
||||
CountryDataResponse,
|
||||
MixNodeResponseItem,
|
||||
DelegationsResponse,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
UptimeStoryResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
|
||||
function getFromCache(key: string) {
|
||||
const ts = Number(localStorage.getItem('ts'));
|
||||
const hasExpired = Date.now() - ts > 200000;
|
||||
const curr = localStorage.getItem(key);
|
||||
if (curr && !hasExpired) {
|
||||
return JSON.parse(curr);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function storeInCache(key: string, data: any) {
|
||||
localStorage.setItem(key, data);
|
||||
}
|
||||
export class Api {
|
||||
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));
|
||||
storeInCache('ts', Date.now());
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodeByID = async (
|
||||
id: string,
|
||||
): Promise<MixNodeResponseItem | undefined> => {
|
||||
const allMixnodes: MixNodeResponse = await Api.fetchMixnodes();
|
||||
const matchedByID = allMixnodes.filter(
|
||||
(eachRecord) => eachRecord.mix_node.identity_key === id,
|
||||
);
|
||||
return (matchedByID.length && matchedByID[0]) || undefined;
|
||||
};
|
||||
|
||||
static fetchGateways = async (): Promise<GatewayResponse> => {
|
||||
const res = await fetch(GATEWAYS_API);
|
||||
return res.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(`${MIXNODES_API}/${id}/delegations`)).json();
|
||||
|
||||
static fetchStatsById = async (id: string): Promise<StatsResponse> =>
|
||||
(await fetch(`${MIXNODES_API}/${id}/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();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,217 @@
|
||||
import * as React from 'react';
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import { Alert, CircularProgress, useMediaQuery, Box } 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 { useMainContext } from 'src/context/main';
|
||||
import { ExpandMore } from '@mui/icons-material';
|
||||
|
||||
export const BondBreakdownTable: React.FC = () => {
|
||||
const { mixnodeDetailInfo, delegations } = useMainContext();
|
||||
const [allContentLoaded, setAllContentLoaded] =
|
||||
React.useState<boolean>(false);
|
||||
const [showError, setShowError] = React.useState<boolean>(false);
|
||||
const [showDelegations, toggleShowDelegations] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
const [bonds, setBonds] = React.useState({
|
||||
delegations: '0',
|
||||
pledges: '0',
|
||||
bondsTotal: '0',
|
||||
hasLoaded: false,
|
||||
});
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mixnodeDetailInfo && mixnodeDetailInfo.data?.length) {
|
||||
const thisMixnode = mixnodeDetailInfo?.data[0];
|
||||
|
||||
// delegations
|
||||
const decimalisedDelegations = printableCoin({
|
||||
amount: thisMixnode.total_delegation.amount.toString(),
|
||||
denom: thisMixnode.total_delegation.denom,
|
||||
});
|
||||
|
||||
// pledges
|
||||
const decimalisedPledges = printableCoin({
|
||||
amount: thisMixnode.bond_amount.amount.toString(),
|
||||
denom: thisMixnode.bond_amount.denom,
|
||||
});
|
||||
|
||||
// bonds total (del + pledges)
|
||||
const pledgesSum = Number(thisMixnode.bond_amount.amount);
|
||||
const delegationsSum = Number(thisMixnode.total_delegation.amount);
|
||||
const bondsTotal = printableCoin({
|
||||
amount: (delegationsSum + pledgesSum).toString(),
|
||||
denom: 'upunk',
|
||||
});
|
||||
|
||||
setBonds({
|
||||
delegations: decimalisedDelegations,
|
||||
pledges: decimalisedPledges,
|
||||
bondsTotal,
|
||||
hasLoaded: true,
|
||||
});
|
||||
}
|
||||
}, [mixnodeDetailInfo]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const hasError = Boolean(mixnodeDetailInfo?.error || delegations?.error);
|
||||
const hasAllMixnodeInfo = Boolean(
|
||||
mixnodeDetailInfo?.data !== undefined &&
|
||||
mixnodeDetailInfo?.data[0].mix_node,
|
||||
);
|
||||
const hasAllDelegationsInfo = Boolean(
|
||||
delegations?.data !== undefined && delegations?.data,
|
||||
);
|
||||
const hasAllData = Boolean(
|
||||
!hasError && hasAllMixnodeInfo && hasAllDelegationsInfo,
|
||||
);
|
||||
setShowError(hasError);
|
||||
setAllContentLoaded(hasAllData);
|
||||
}, [mixnodeDetailInfo, delegations]);
|
||||
|
||||
const expandDelegations = () => {
|
||||
if (delegations?.data && delegations.data.length > 0) {
|
||||
toggleShowDelegations(!showDelegations);
|
||||
}
|
||||
};
|
||||
const calcBondPercentage = (num: number) => {
|
||||
if (mixnodeDetailInfo?.data !== undefined && mixnodeDetailInfo?.data[0]) {
|
||||
const rawDeligationAmount = Number(
|
||||
mixnodeDetailInfo.data[0].total_delegation.amount,
|
||||
);
|
||||
const rawPledgeAmount = Number(
|
||||
mixnodeDetailInfo.data[0].bond_amount.amount,
|
||||
);
|
||||
const rawTotalBondsAmount = rawDeligationAmount + rawPledgeAmount;
|
||||
return ((num * 100) / rawTotalBondsAmount).toFixed(1);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (mixnodeDetailInfo?.isLoading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (showError) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
We are unable to retrieve a Mixnode with that ID. Please try later or
|
||||
Contact Us.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showError && allContentLoaded) {
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals">
|
||||
<TableBody>
|
||||
<TableRow sx={matches ? { minWidth: '70vw' } : null}>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
width: '150px',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Bond total
|
||||
</TableCell>
|
||||
<TableCell align="left">{bonds.bondsTotal}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell align="left">Pledge total</TableCell>
|
||||
<TableCell align="left">{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">{bonds.delegations}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{showDelegations && (
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: 400,
|
||||
overflowY: 'scroll',
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ fontWeight: 'bold', background: '#242C3D' }}
|
||||
align="left"
|
||||
>
|
||||
Delegators
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ fontWeight: 'bold', background: '#242C3D' }}
|
||||
align="left"
|
||||
>
|
||||
Stake
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
background: '#242C3D',
|
||||
width: '200px',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Share from bond
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{delegations?.data?.map(
|
||||
({ owner, amount: { amount, denom } }) => (
|
||||
<TableRow key={owner}>
|
||||
<TableCell
|
||||
sx={matches ? { width: 190 } : null}
|
||||
align="left"
|
||||
>
|
||||
{owner}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{printableCoin({ amount: amount.toString(), denom })}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{calcBondPercentage(amount)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Typography } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
|
||||
export const ComponentError: React.FC<{ text: string }> = ({ text }) => (
|
||||
<Typography
|
||||
sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }}
|
||||
variant="body1"
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Card, CardHeader, CardContent, Typography } from '@mui/material';
|
||||
import React, { ReactEventHandler } from 'react';
|
||||
|
||||
type ContentCardProps = {
|
||||
title?: string | React.ReactNode;
|
||||
subtitle?: string;
|
||||
Icon?: React.ReactNode;
|
||||
Action?: React.ReactNode;
|
||||
errorMsg?: string;
|
||||
onClick?: ReactEventHandler;
|
||||
};
|
||||
|
||||
export const ContentCard: React.FC<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,
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||
|
||||
export const CustomColumnHeading: React.FC<{ headingTitle: string }> = ({
|
||||
headingTitle,
|
||||
}) => {
|
||||
const [filter, toggleFilter] = React.useState<boolean>(false);
|
||||
|
||||
const handleClick = () => {
|
||||
toggleFilter(!filter);
|
||||
};
|
||||
return (
|
||||
<Box alignItems="center" display="flex" onClick={handleClick}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: 14,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{headingTitle}
|
||||
</Typography>
|
||||
{filter ? <ExpandMore /> : <ExpandLess />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Typography, useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Socials } from './Socials';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
mt: 3,
|
||||
pt: 3,
|
||||
pb: 3,
|
||||
}}
|
||||
>
|
||||
{isMobile && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Socials isFooter />
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
textAlign: isMobile ? 'center' : 'end',
|
||||
color: theme.palette.nym.text.footer,
|
||||
}}
|
||||
>
|
||||
© 2021 Nym Technologies SA, all rights reserved
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,397 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChevronLeft, ExpandLess, ExpandMore, Menu } from '@mui/icons-material';
|
||||
import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles';
|
||||
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 { NymLogoSVG } from 'src/icons/NymLogoSVG';
|
||||
import { BIG_DIPPER, NYM_WEBSITE } from 'src/api/constants';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import { OverviewSVG } from '../icons/OverviewSVG';
|
||||
import { NetworkComponentsSVG } from '../icons/NetworksSVG';
|
||||
import { NodemapSVG } from '../icons/NodemapSVG';
|
||||
import { Socials } from './Socials';
|
||||
import { Footer } from './Footer';
|
||||
import { DarkLightSwitchDesktop, DarkLightSwitchMobile } from './Switch';
|
||||
|
||||
const drawerWidth = 300;
|
||||
|
||||
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 navOptionType = {
|
||||
id: number;
|
||||
isActive?: boolean;
|
||||
url: string;
|
||||
title: string;
|
||||
Icon?: React.ReactNode;
|
||||
nested?: navOptionType[];
|
||||
isExpandedChild?: boolean;
|
||||
};
|
||||
|
||||
const originalNavOptions: navOptionType[] = [
|
||||
{
|
||||
id: 0,
|
||||
isActive: false,
|
||||
url: '/overview',
|
||||
title: 'Overview',
|
||||
Icon: <OverviewSVG />,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
isActive: false,
|
||||
url: '/network-components',
|
||||
title: 'Network Components',
|
||||
Icon: <NetworkComponentsSVG />,
|
||||
nested: [
|
||||
{
|
||||
id: 3,
|
||||
url: '/network-components/mixnodes',
|
||||
title: 'Mixnodes',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: '/network-components/gateways',
|
||||
title: 'Gateways',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: `${BIG_DIPPER}/validators`,
|
||||
title: 'Validators',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
isActive: false,
|
||||
url: '/nodemap',
|
||||
title: 'Nodemap',
|
||||
Icon: <NodemapSVG />,
|
||||
},
|
||||
];
|
||||
|
||||
type ExpandableButtonType = {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
isActive?: boolean;
|
||||
Icon?: React.ReactNode;
|
||||
nested?: navOptionType[];
|
||||
isChild?: boolean;
|
||||
openDrawer: () => void;
|
||||
closeDrawer: () => void;
|
||||
drawIsOpen: boolean;
|
||||
setToActive: (num: number) => void;
|
||||
};
|
||||
|
||||
const ExpandableButton: React.FC<ExpandableButtonType> = ({
|
||||
id,
|
||||
url,
|
||||
setToActive,
|
||||
isActive,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
drawIsOpen,
|
||||
Icon,
|
||||
title,
|
||||
nested,
|
||||
isChild,
|
||||
}) => {
|
||||
const [dynamicStyle, setDynamicStyle] = React.useState({});
|
||||
const [nestedOptions, toggleNestedOptions] = React.useState(false);
|
||||
const [isExternal, setIsExternal] = React.useState<boolean>(false);
|
||||
const { palette } = useTheme();
|
||||
|
||||
const handleClick = () => {
|
||||
openDrawer();
|
||||
if (title === 'Network Components' && nested) {
|
||||
toggleNestedOptions(!nestedOptions);
|
||||
}
|
||||
setToActive(id);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (url) {
|
||||
setIsExternal(url.includes('http'));
|
||||
}
|
||||
if (nested) {
|
||||
setDynamicStyle({
|
||||
background: '#242C3D',
|
||||
borderRight: `3px solid ${palette.nym.highlight}`,
|
||||
});
|
||||
}
|
||||
if (isChild) {
|
||||
setDynamicStyle({
|
||||
background: '#3C4558',
|
||||
fontWeight: 800,
|
||||
});
|
||||
}
|
||||
if (!nested && !isChild) {
|
||||
setDynamicStyle({
|
||||
background: '#242C3D',
|
||||
borderRight: `3px solid ${palette.nym.highlight}`,
|
||||
});
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!drawIsOpen && nestedOptions) {
|
||||
toggleNestedOptions(false);
|
||||
}
|
||||
}, [drawIsOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
component={!nested ? Link : 'div'}
|
||||
to={isExternal ? { pathname: url } : url}
|
||||
target={isExternal ? '_blank' : ''}
|
||||
sx={
|
||||
isActive
|
||||
? dynamicStyle
|
||||
: { background: '#111826', borderRight: 'none' }
|
||||
}
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
pt: 2,
|
||||
pb: 2,
|
||||
background: isChild ? '#3C4558' : 'none',
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>{Icon}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={title}
|
||||
sx={{
|
||||
color: palette.nym.networkExplorer.nav.text,
|
||||
}}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
fontWeight: isActive ? 800 : 300,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{nested && nestedOptions && <ExpandLess />}
|
||||
{nested && !nestedOptions && <ExpandMore />}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{nestedOptions &&
|
||||
nested?.map((each) => (
|
||||
<ExpandableButton
|
||||
id={each.id}
|
||||
url={each.url}
|
||||
key={each.title}
|
||||
title={each.title}
|
||||
openDrawer={openDrawer}
|
||||
drawIsOpen={drawIsOpen}
|
||||
closeDrawer={closeDrawer}
|
||||
setToActive={setToActive}
|
||||
isChild
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandableButton.defaultProps = {
|
||||
Icon: null,
|
||||
nested: undefined,
|
||||
isChild: false,
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
export const Nav: React.FC = ({ children }) => {
|
||||
const [navOptionsState, updateNavOptionsState] =
|
||||
React.useState(originalNavOptions);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const setToActive = (id: number) => {
|
||||
const updated = navOptionsState.map((option) => ({
|
||||
...option,
|
||||
isActive: option.id === id,
|
||||
}));
|
||||
updateNavOptionsState(updated);
|
||||
};
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<AppBar
|
||||
sx={{
|
||||
background: theme.palette.nym.networkExplorer.nav.background,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
disableGutters
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: 205,
|
||||
}}
|
||||
>
|
||||
<IconButton component="a" href={NYM_WEBSITE} target="_blank">
|
||||
<NymLogoSVG />
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
sx={{
|
||||
color: theme.palette.nym.networkExplorer.nav.text,
|
||||
fontSize: '18px',
|
||||
}}
|
||||
>
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to="/overview"
|
||||
underline="none"
|
||||
color="inherit"
|
||||
>
|
||||
Network Explorer
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
mr: 2,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{!isMobile && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
pr: 0,
|
||||
pl: 2,
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Socials />
|
||||
<DarkLightSwitchDesktop defaultChecked />
|
||||
</Box>
|
||||
)}
|
||||
{isMobile && <DarkLightSwitchMobile />}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
open={open}
|
||||
sx={{
|
||||
background: theme.palette.nym.networkExplorer.nav.background,
|
||||
}}
|
||||
>
|
||||
<DrawerHeader
|
||||
sx={{
|
||||
justifyContent: open ? 'flex-end' : 'center',
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={open ? handleDrawerClose : handleDrawerOpen}
|
||||
sx={{
|
||||
padding: 0,
|
||||
ml: '7px',
|
||||
color: theme.palette.nym.networkExplorer.nav.text,
|
||||
}}
|
||||
>
|
||||
{open ? <ChevronLeft /> : <Menu />}
|
||||
</IconButton>
|
||||
</DrawerHeader>
|
||||
|
||||
<List sx={{ pt: 0, pb: 0 }}>
|
||||
{navOptionsState.map((props) => (
|
||||
<ExpandableButton
|
||||
setToActive={setToActive}
|
||||
key={props.url}
|
||||
openDrawer={handleDrawerOpen}
|
||||
drawIsOpen={open}
|
||||
closeDrawer={handleDrawerClose}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Drawer>
|
||||
<Box sx={{ width: '100%', p: 4, mt: 7 }}>
|
||||
{children}
|
||||
<Footer />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from 'react';
|
||||
import { Box, IconButton } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import TwitterIcon from '@mui/icons-material/Twitter';
|
||||
import { GITHUB_LINK, TELEGRAM_LINK, TWITTER_LINK } from 'src/api/constants';
|
||||
|
||||
export const Socials: React.FC<{ 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">
|
||||
<TelegramIcon sx={{ color }} />
|
||||
</IconButton>
|
||||
<IconButton component="a" href={GITHUB_LINK} target="_blank">
|
||||
<GitHubIcon sx={{ color }} />
|
||||
</IconButton>
|
||||
<IconButton component="a" href={TWITTER_LINK} target="_blank">
|
||||
<TwitterIcon sx={{ color }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Socials.defaultProps = {
|
||||
isFooter: false,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Card, CardContent, IconButton, Typography } from '@mui/material';
|
||||
import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt';
|
||||
|
||||
interface StatsCardProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
count: string | number;
|
||||
errorMsg?: Error | string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
export const StatsCard: React.FC<StatsCardProps> = ({
|
||||
icon,
|
||||
title,
|
||||
count,
|
||||
onClick,
|
||||
errorMsg,
|
||||
}) => (
|
||||
<Card onClick={onClick} sx={{ height: '100%' }}>
|
||||
<CardContent
|
||||
sx={{
|
||||
padding: 2,
|
||||
'&:last-child': {
|
||||
paddingBottom: 2,
|
||||
},
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
sx={{ color: (theme) => theme.palette.text.primary, fontSize: 18 }}
|
||||
>
|
||||
{icon}
|
||||
<Typography ml={3} mr={0.75} fontSize="inherit">
|
||||
{count}
|
||||
</Typography>
|
||||
<Typography mr={1} fontSize="inherit">
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton color="inherit">
|
||||
<ArrowRightAltIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
{errorMsg && (
|
||||
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
|
||||
{errorMsg}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
StatsCard.defaultProps = {
|
||||
onClick: undefined,
|
||||
errorMsg: undefined,
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
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 'src/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: React.FC = () => {
|
||||
const { toggleMode } = useMainContext();
|
||||
return (
|
||||
<Button onClick={() => toggleMode()}>
|
||||
<LightSwitchSVG />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const DarkLightSwitchDesktop: React.FC<{ defaultChecked: boolean }> = ({
|
||||
defaultChecked,
|
||||
}) => {
|
||||
const { toggleMode } = useMainContext();
|
||||
return (
|
||||
<Button onClick={() => toggleMode()}>
|
||||
<DarkLightSwitch defaultChecked={defaultChecked} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import { Box, useMediaQuery, TextField, MenuItem } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
|
||||
type TableToolBarProps = {
|
||||
onChangeSearch: (arg: string) => void;
|
||||
onChangePageSize: (event: SelectChangeEvent<string>) => void;
|
||||
pageSize: string;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export const TableToolbar: React.FC<TableToolBarProps> = ({
|
||||
searchTerm,
|
||||
onChangeSearch,
|
||||
onChangePageSize,
|
||||
pageSize,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: matches ? 'column' : 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={pageSize}
|
||||
onChange={onChangePageSize}
|
||||
sx={{
|
||||
width: 200,
|
||||
marginBottom: matches ? 2 : 0,
|
||||
}}
|
||||
>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={30}>30</MenuItem>
|
||||
<MenuItem value={50}>50</MenuItem>
|
||||
<MenuItem value={100}>100</MenuItem>
|
||||
</Select>
|
||||
<TextField
|
||||
sx={{ width: 350 }}
|
||||
value={searchTerm}
|
||||
placeholder="search"
|
||||
onChange={(event) => onChangeSearch(event.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
export const Title: React.FC<{ text: string }> = ({ text }) => (
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
@@ -0,0 +1,79 @@
|
||||
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: React.FC<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}>{each}</TableCell>
|
||||
<TableCell sx={error ? { opacity: 0.4 } : null} align="right">
|
||||
{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,
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as React from 'react';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import {
|
||||
DataGrid,
|
||||
GridColumns,
|
||||
GridRowData,
|
||||
GridSortModel,
|
||||
useGridSlotComponentProps,
|
||||
} from '@mui/x-data-grid';
|
||||
import Pagination from '@mui/material/Pagination';
|
||||
import { SxProps } from '@mui/system';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
});
|
||||
|
||||
type DataGridProps = {
|
||||
loading?: boolean;
|
||||
rows: GridRowData[];
|
||||
columnsData: GridColumns;
|
||||
pageSize?: string;
|
||||
pagination?: boolean;
|
||||
hideFooter?: boolean;
|
||||
sortModel?: GridSortModel;
|
||||
};
|
||||
|
||||
export const cellStyles: SxProps = {
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
maxHeight: 100,
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 400,
|
||||
fontSize: 12,
|
||||
lineHeight: 2,
|
||||
textAlign: 'start',
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'break-spaces',
|
||||
};
|
||||
|
||||
function CustomPagination() {
|
||||
const { state, apiRef } = useGridSlotComponentProps();
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Pagination
|
||||
className={classes.root}
|
||||
color="primary"
|
||||
count={state.pagination.pageCount}
|
||||
page={state.pagination.page + 1}
|
||||
onChange={(event, value) => apiRef.current.setPage(value - 1)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const UniversalDataGrid: React.FC<DataGridProps> = ({
|
||||
loading,
|
||||
rows,
|
||||
columnsData,
|
||||
pageSize,
|
||||
pagination,
|
||||
hideFooter,
|
||||
sortModel,
|
||||
}) => {
|
||||
const [sortModelState, setSortModelState] = React.useState<
|
||||
GridSortModel | undefined
|
||||
>(sortModel);
|
||||
if (columnsData && rows) {
|
||||
return (
|
||||
<DataGrid
|
||||
pagination
|
||||
components={{
|
||||
Pagination: CustomPagination,
|
||||
}}
|
||||
loading={loading}
|
||||
columns={columnsData}
|
||||
rows={rows}
|
||||
pageSize={Number(pageSize)}
|
||||
rowsPerPageOptions={[5]}
|
||||
hideFooterPagination={!pagination}
|
||||
disableColumnFilter
|
||||
disableColumnMenu
|
||||
disableSelectionOnClick
|
||||
columnBuffer={0}
|
||||
autoHeight
|
||||
hideFooter={hideFooter}
|
||||
sortModel={sortModelState}
|
||||
onSortModelChange={setSortModelState}
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
UniversalDataGrid.defaultProps = {
|
||||
loading: false,
|
||||
pageSize: undefined,
|
||||
pagination: false,
|
||||
hideFooter: true,
|
||||
sortModel: undefined,
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import * as React from 'react';
|
||||
import { CircularProgress, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Chart } from 'react-google-charts';
|
||||
import { ApiState, UptimeStoryResponse } from 'src/typeDefs/explorer-api';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface ChartProps {
|
||||
title?: string;
|
||||
xLabel: string;
|
||||
yLabel: string;
|
||||
uptimeStory: ApiState<UptimeStoryResponse>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
type FormattedDateRecord = [string, number, number];
|
||||
type FormattedChartHeadings = string[];
|
||||
type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord];
|
||||
|
||||
export const UptimeChart: React.FC<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', 'UptimeV4', 'UptimeV6'],
|
||||
];
|
||||
uptimeStory.data.history.forEach((eachDate) => {
|
||||
const formattedDateUptimeRecord: FormattedDateRecord = [
|
||||
format(new Date(eachDate.date), 'MMM dd'),
|
||||
eachDate.ipv4_uptime,
|
||||
eachDate.ipv6_uptime,
|
||||
];
|
||||
allFormattedChartData.push(formattedDateUptimeRecord);
|
||||
});
|
||||
setFormattedChartData(allFormattedChartData);
|
||||
} else {
|
||||
const emptyData: any = [
|
||||
['Date', 'UptimeV4', 'UptimeV6'],
|
||||
['Jul 27', 10, 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', 'UptimeV4', 'UptimeV6'],
|
||||
[format(new Date(Date.now()), 'MMM dd'), 0, 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', '#CC808A'],
|
||||
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: {
|
||||
// % uptime vertical
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
/* 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 { ApiState, CountryDataResponse } from 'src/typeDefs/explorer-api';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import MAP_TOPOJSON from '../assets/world-110m.json';
|
||||
|
||||
type MapProps = {
|
||||
userLocation?: [number, number];
|
||||
countryData?: ApiState<CountryDataResponse>;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const WorldMap: React.FC<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,
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,295 @@
|
||||
import { PaletteMode } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { MIXNODE_API_ERROR } from 'src/api/constants';
|
||||
import {
|
||||
CountryDataResponse,
|
||||
GatewayResponse,
|
||||
MixNodeResponse,
|
||||
ValidatorsResponse,
|
||||
BlockResponse,
|
||||
ApiState,
|
||||
MixNodeResponseItem,
|
||||
DelegationsResponse,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
UptimeStoryResponse,
|
||||
} from 'src/typeDefs/explorer-api';
|
||||
import { Api } from '../api';
|
||||
|
||||
interface State {
|
||||
mode: PaletteMode;
|
||||
toggleMode: () => void;
|
||||
mixnodes?: ApiState<MixNodeResponse>;
|
||||
fetchMixnodes: () => void;
|
||||
filterMixnodes: (arg: MixNodeResponse) => void;
|
||||
gateways?: ApiState<GatewayResponse>;
|
||||
validators?: ApiState<ValidatorsResponse>;
|
||||
block?: ApiState<BlockResponse>;
|
||||
countryData?: ApiState<CountryDataResponse>;
|
||||
globalError?: string | undefined;
|
||||
mixnodeDetailInfo?: ApiState<MixNodeResponse>;
|
||||
fetchMixnodeById: (arg: string) => void;
|
||||
fetchDelegationsById: (arg: string) => void;
|
||||
delegations?: ApiState<DelegationsResponse>;
|
||||
stats?: ApiState<StatsResponse>;
|
||||
fetchStatsById: (arg: string) => void;
|
||||
status?: ApiState<StatusResponse>;
|
||||
fetchStatusById: (arg: string) => void;
|
||||
fetchUptimeStoryById: (arg: string) => void;
|
||||
uptimeStory?: ApiState<UptimeStoryResponse>;
|
||||
}
|
||||
|
||||
// TODO: remove the export and replace all uses with `useMainContext()` hook
|
||||
export const MainContext = React.createContext<State>({
|
||||
mode: 'dark',
|
||||
fetchMixnodeById: () => null,
|
||||
toggleMode: () => undefined,
|
||||
fetchDelegationsById: () => null,
|
||||
fetchStatsById: () => null,
|
||||
fetchStatusById: () => null,
|
||||
fetchUptimeStoryById: () => null,
|
||||
filterMixnodes: () => null,
|
||||
fetchMixnodes: () => null,
|
||||
status: { data: undefined, isLoading: false, error: undefined },
|
||||
stats: { data: undefined, isLoading: false, error: undefined },
|
||||
mixnodeDetailInfo: { data: undefined, isLoading: false, error: undefined },
|
||||
delegations: { data: undefined, isLoading: false, error: undefined },
|
||||
});
|
||||
|
||||
export const useMainContext = (): React.ContextType<typeof MainContext> =>
|
||||
React.useContext<State>(MainContext);
|
||||
|
||||
export const MainContextProvider: React.FC = ({ children }) => {
|
||||
// light/dark mode
|
||||
const [mode, setMode] = React.useState<PaletteMode>('dark');
|
||||
|
||||
// global / banner error messaging
|
||||
const [globalError, setGlobalError] = React.useState<string>();
|
||||
|
||||
// various APIs for Overview page
|
||||
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 [mixnodeDetailInfo, setMixnodeDetailInfo] =
|
||||
React.useState<ApiState<MixNodeResponse>>();
|
||||
|
||||
// various APIs for Detail page
|
||||
const [delegations, setDelegations] =
|
||||
React.useState<ApiState<DelegationsResponse>>();
|
||||
const [status, setStatus] = React.useState<ApiState<StatusResponse>>();
|
||||
const [stats, setStats] = React.useState<ApiState<StatsResponse>>();
|
||||
const [uptimeStory, setUptimeStory] =
|
||||
React.useState<ApiState<UptimeStoryResponse>>();
|
||||
|
||||
const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark'));
|
||||
|
||||
// actions passed down to Overview and Detail pages
|
||||
const fetchUptimeStoryById = async (id: string) => {
|
||||
setUptimeStory({
|
||||
data: uptimeStory?.data,
|
||||
isLoading: true,
|
||||
error: uptimeStory?.error,
|
||||
});
|
||||
try {
|
||||
const data = await Api.fetchUptimeStoryById(id);
|
||||
setUptimeStory({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setUptimeStory({
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Uptime Story api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDelegationsById = async (id: string) => {
|
||||
setDelegations({ data: delegations?.data, isLoading: true });
|
||||
try {
|
||||
const data = await Api.fetchDelegationsById(id);
|
||||
setDelegations({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setDelegations({
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Delegations api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStatusById = async (id: string) => {
|
||||
setStatus({ data: status?.data, isLoading: true, error: status?.error });
|
||||
try {
|
||||
const data = await Api.fetchStatusById(id);
|
||||
setStatus({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setStatus({
|
||||
error: error instanceof Error ? error : new Error('Status api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStatsById = async (id: string) => {
|
||||
setStats({ data: stats?.data, isLoading: true, error: stats?.error });
|
||||
try {
|
||||
const data = await Api.fetchStatsById(id);
|
||||
setStats({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setStats({
|
||||
error: error instanceof Error ? error : new Error('Stats api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMixnodeById = async (id: string) => {
|
||||
setMixnodeDetailInfo({ data: mixnodeDetailInfo?.data, isLoading: true });
|
||||
|
||||
// 1. if mixnode data already exists filter down to this ID
|
||||
if (mixnodes && mixnodes.data) {
|
||||
const [matchedToID] = mixnodes.data.filter(
|
||||
(eachMixnode: MixNodeResponseItem) =>
|
||||
eachMixnode.mix_node.identity_key === id,
|
||||
);
|
||||
|
||||
// b) SUCCESS | if there *IS* a matched ID in mixnodes
|
||||
if (matchedToID) {
|
||||
setMixnodeDetailInfo({ data: [matchedToID], isLoading: false });
|
||||
}
|
||||
// b) FAIL | if there is no matching ID in mixnodes
|
||||
if (!matchedToID) {
|
||||
setGlobalError(MIXNODE_API_ERROR);
|
||||
setMixnodeDetailInfo({
|
||||
isLoading: false,
|
||||
error: new Error(MIXNODE_API_ERROR),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 2. if mixnode data DOESNT already exist, fetch this specific ID's information.
|
||||
try {
|
||||
const data = await Api.fetchMixnodeByID(id);
|
||||
// a) fetches from cache^, then API, then filters down then dumps in `mixnodes` context.
|
||||
if (data) {
|
||||
setMixnodeDetailInfo({ data: [data], isLoading: false });
|
||||
} else {
|
||||
throw Error('api failed to retrieve mixnode via id');
|
||||
}
|
||||
// NOTE: Only returning mixnodes api info at the moment. Other `ping` api required also.
|
||||
} catch (error) {
|
||||
setGlobalError(MIXNODE_API_ERROR);
|
||||
setMixnodeDetailInfo({
|
||||
isLoading: false,
|
||||
error: new Error(MIXNODE_API_ERROR),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const fetchMixnodes = async () => {
|
||||
setMixnodes((d) => ({ ...d, isLoading: true }));
|
||||
try {
|
||||
const data = await Api.fetchMixnodes();
|
||||
setMixnodes({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setMixnodes({
|
||||
error: error instanceof Error ? error : new Error('Mixnode api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
const filterMixnodes = (arr: MixNodeResponse) => {
|
||||
setMixnodes({ data: arr, isLoading: false });
|
||||
};
|
||||
const fetchGateways = async () => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
Promise.all([
|
||||
fetchMixnodes(),
|
||||
fetchGateways(),
|
||||
fetchValidators(),
|
||||
fetchBlock(),
|
||||
fetchCountryData(),
|
||||
]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MainContext.Provider
|
||||
value={{
|
||||
mode,
|
||||
toggleMode,
|
||||
mixnodes,
|
||||
filterMixnodes,
|
||||
fetchMixnodes,
|
||||
gateways,
|
||||
validators,
|
||||
block,
|
||||
countryData,
|
||||
globalError,
|
||||
mixnodeDetailInfo,
|
||||
fetchMixnodeById,
|
||||
fetchDelegationsById,
|
||||
delegations,
|
||||
stats,
|
||||
fetchStatsById,
|
||||
status,
|
||||
fetchStatusById,
|
||||
uptimeStory,
|
||||
fetchUptimeStoryById,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MainContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
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]);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const GatewaysSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const LightSwitchSVG: React.FC = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const MixnodesSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const NetworkComponentsSVG: React.FC = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const NodemapSVG: React.FC = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const NymLogoSVG: React.FC = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 5389.9 5389.9"
|
||||
width="40px"
|
||||
height="40px"
|
||||
{...props}
|
||||
>
|
||||
<circle cx={2695} cy={2695} r={2585} fill="#121726" />
|
||||
<linearGradient
|
||||
id="nym_logo_svg__a"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1={0}
|
||||
y1={8058.165}
|
||||
x2={5390}
|
||||
y2={8058.165}
|
||||
gradientTransform="matrix(1 0 0 -1 0 10753.165)"
|
||||
>
|
||||
<stop offset={0} stopColor="#f77846" />
|
||||
<stop offset={1} stopColor="#ed3572" />
|
||||
</linearGradient>
|
||||
<path
|
||||
d="M2695 5390c-182.8 0-365.5-18.4-543-54.8-173.1-35.4-343.3-88.3-506-157.1-159.7-67.6-313.7-151.2-457.8-248.5-142.7-96.4-276.8-207.1-398.8-329-121.9-121.9-232.6-256.1-329-398.8-97.4-144-181-298-248.6-457.8-68.8-162.6-121.6-332.9-157-506C18.4 3060.5 0 2877.8 0 2695s18.4-365.5 54.8-543c35.4-173.1 88.3-343.3 157.1-506 67.6-159.7 151.2-313.7 248.5-457.8 96.4-142.7 207.1-276.8 329-398.8s256.1-232.6 398.8-329c144.1-97.3 298.1-180.9 457.8-248.5 162.7-68.8 332.9-121.7 506-157.1C2329.5 18.4 2512.2 0 2695 0s365.5 18.4 543 54.8c173.1 35.4 343.3 88.3 506 157.1 159.7 67.6 313.7 151.2 457.8 248.5 142.7 96.4 276.8 207.1 398.8 329 121.9 121.9 232.6 256.1 329 398.8 97.3 144.1 180.9 298.1 248.5 457.8 68.8 162.7 121.7 332.9 157.1 506 36.3 177.5 54.8 360.2 54.8 543s-18.4 365.5-54.8 543c-35.4 173.1-88.3 343.3-157.1 506-67.6 159.7-151.2 313.7-248.5 457.8-96.4 142.7-207.1 276.8-329 398.8-121.9 121.9-256.1 232.6-398.8 329-144.1 97.3-298.1 180.9-457.8 248.5-162.7 68.8-332.9 121.7-506 157.1-177.5 36.4-360.2 54.8-543 54.8zm0-5170c-168 0-335.9 16.9-498.9 50.3-158.9 32.5-315.1 81-464.4 144.2-146.6 62-288.1 138.8-420.4 228.2C1180.2 731.3 1057 833 944.9 945c-112 112-213.7 235.3-302.3 366.4-89.4 132.3-166.2 273.7-228.2 420.4-63.2 149.3-111.7 305.6-144.2 464.4C236.9 2359.1 220 2527 220 2695s16.9 335.9 50.3 498.9c32.5 158.9 81 315.1 144.2 464.4 62 146.6 138.8 288.1 228.2 420.4C731.3 4209.8 833 4333 945 4445.1c112 112 235.3 213.7 366.4 302.3 132.3 89.4 273.7 166.2 420.4 228.2 149.3 63.2 305.6 111.7 464.4 144.2 163.1 33.4 330.9 50.3 498.9 50.3s335.9-16.9 498.9-50.3c158.9-32.5 315.1-81 464.4-144.2 146.6-62 288.1-138.8 420.4-228.2 131.1-88.6 254.3-190.3 366.4-302.3 112-112 213.7-235.3 302.3-366.4 89.4-132.3 166.2-273.7 228.2-420.4 63.2-149.3 111.7-305.6 144.2-464.4 33.4-163.1 50.3-330.9 50.3-498.9s-16.9-335.9-50.3-498.9c-32.5-158.9-81-315.1-144.2-464.4-62-146.6-138.8-288.1-228.2-420.4-88.6-131.1-190.3-254.3-302.3-366.4-112-112-235.3-213.7-366.4-302.3-132.3-89.4-273.7-166.2-420.4-228.2-149.3-63.2-305.6-111.7-464.4-144.2-163.1-33.3-331-50.2-499-50.2z"
|
||||
fill="url(#nym_logo_svg__a)"
|
||||
/>
|
||||
<path
|
||||
d="M1958.5 3160.4h-269.6l-735.8-725.3v725.3H734.6v-930.9h276.2l735.8 725.1v-725.1h211.9v930.9zm2420.4-930.9l-335.7 330.9-335.7-330.9h-276.2v930.9h218.4v-725.3l345.4 340.6c26.7 26.3 69.6 26.3 96.3 0l345.4-340.6v725.3h218.4v-930.9h-276.3zm-1789.8 485.9v445h218.4v-445l502.7-485.9H3034l-335.9 330.9-335.7-330.9h-276.2l502.9 485.9z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const OverviewSVG: React.FC = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const ValidatorsSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nym Network Explorer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { App } from './App';
|
||||
import { MainContextProvider } from './context/main';
|
||||
import './styles.css';
|
||||
import { NetworkExplorerThemeProvider } from './theme';
|
||||
|
||||
ReactDOM.render(
|
||||
<MainContextProvider>
|
||||
<NetworkExplorerThemeProvider>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</NetworkExplorerThemeProvider>
|
||||
</MainContextProvider>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#121726;}
|
||||
.st1{fill:url(#SVGID_1_);}
|
||||
.st2{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<circle class="st0" cx="250" cy="250.1" r="239.8"/>
|
||||
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="4.980469e-02" y1="5613.9395" x2="500.0498" y2="5613.9395" gradientTransform="matrix(1 0 0 -1 0 5863.9897)">
|
||||
<stop offset="0" style="stop-color:#E1864B"/>
|
||||
<stop offset="1" style="stop-color:#DA465B"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M250,500.1c-17,0-33.9-1.7-50.4-5.1c-16.1-3.3-31.8-8.2-46.9-14.6c-14.8-6.3-29.1-14-42.5-23.1
|
||||
c-13.2-8.9-25.7-19.2-37-30.5c-11.3-11.3-21.6-23.8-30.5-37c-9-13.4-16.8-27.6-23.1-42.5c-6.4-15.1-11.3-30.9-14.6-46.9
|
||||
C1.8,284,0,267,0,250.1s1.7-33.9,5.1-50.4c3.3-16.1,8.2-31.8,14.6-46.9c6.3-14.8,14-29.1,23.1-42.5C51.7,97,62,84.6,73.3,73.3
|
||||
s23.8-21.6,37-30.5c13.4-9,27.7-16.8,42.5-23.1c15.1-6.4,30.9-11.3,46.9-14.6c16.5-3.4,33.4-5.1,50.4-5.1c17,0,33.9,1.7,50.4,5.1
|
||||
c16.1,3.3,31.8,8.2,46.9,14.6c14.8,6.3,29.1,14,42.5,23.1c13.2,8.9,25.7,19.2,37,30.5c11.3,11.3,21.6,23.8,30.5,37
|
||||
c9,13.4,16.8,27.7,23.1,42.5c6.4,15.1,11.3,30.9,14.6,46.9c3.4,16.5,5.1,33.4,5.1,50.4s-1.7,33.9-5.1,50.4
|
||||
c-3.3,16.1-8.2,31.8-14.6,46.9c-6.3,14.8-14,29.1-23.1,42.5c-8.9,13.2-19.2,25.7-30.5,37c-11.3,11.3-23.8,21.6-37,30.5
|
||||
c-13.4,9-27.7,16.8-42.5,23.1c-15.1,6.4-30.9,11.3-46.9,14.6C284,498.3,267,500.1,250,500.1z M250,20.5c-15.6,0-31.2,1.6-46.3,4.7
|
||||
c-14.7,3-29.2,7.5-43.1,13.4c-13.6,5.8-26.7,12.9-39,21.2c-12.2,8.2-23.6,17.7-34,28c-10.4,10.4-19.8,21.8-28,34
|
||||
c-8.3,12.3-15.4,25.4-21.2,39c-5.9,13.8-10.4,28.3-13.4,43.1c-3.1,15.1-4.7,30.7-4.7,46.3s1.6,31.2,4.7,46.3
|
||||
c3,14.7,7.5,29.2,13.4,43.1c5.8,13.6,12.9,26.7,21.2,39c8.2,12.2,17.7,23.6,28,34c10.4,10.4,21.8,19.8,34,28
|
||||
c12.3,8.3,25.4,15.4,39,21.2c13.8,5.9,28.3,10.4,43.1,13.4c15.1,3.1,30.7,4.7,46.3,4.7c15.6,0,31.2-1.6,46.3-4.7
|
||||
c14.7-3,29.2-7.5,43.1-13.4c13.6-5.8,26.7-12.9,39-21.2c12.2-8.2,23.6-17.7,34-28c10.4-10.4,19.8-21.8,28-34
|
||||
c8.3-12.3,15.4-25.4,21.2-39c5.9-13.8,10.4-28.3,13.4-43.1c3.1-15.1,4.7-30.7,4.7-46.3s-1.6-31.2-4.7-46.3
|
||||
c-3-14.7-7.5-29.2-13.4-43.1c-5.8-13.6-12.9-26.7-21.2-39c-8.2-12.2-17.7-23.6-28-34c-10.4-10.4-21.8-19.8-34-28
|
||||
c-12.3-8.3-25.4-15.4-39-21.2c-13.8-5.9-28.3-10.4-43.1-13.4C281.2,22,265.6,20.5,250,20.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st2" d="M369.8,341.2h-52.8l-144-142v142h-42.8V158.9h54.1l144,141.9V158.9h41.5V341.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Grid, Paper, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { NymLogoSVG } from 'src/icons/NymLogoSVG';
|
||||
|
||||
export const Page404: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const { mode } = useMainContext();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<Grid container spacing={0} alignItems="center" justifyContent="center">
|
||||
<Grid item xs={12} sm={12} md={6}>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 3,
|
||||
height: 450,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-evenly',
|
||||
flexDirection: 'column',
|
||||
background:
|
||||
mode === 'dark'
|
||||
? theme.palette.secondary.dark
|
||||
: theme.palette.primary.light,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<NymLogoSVG />
|
||||
<Typography variant="h2">Oh No!</Typography>
|
||||
<Typography variant="body1">
|
||||
It looks like you might be lost.
|
||||
</Typography>
|
||||
<Typography variant="body1" textAlign="center">
|
||||
Please try the link again or navigate back to{' '}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
bgcolor: theme.palette.primary.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}}
|
||||
onClick={() => history.push('/overview')}
|
||||
>
|
||||
Overview
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Grid, Typography } from '@mui/material';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import {
|
||||
cellStyles,
|
||||
UniversalDataGrid,
|
||||
} from 'src/components/Universal-DataGrid';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { gatewayToGridRow } from 'src/utils';
|
||||
import { GatewayResponse } from 'src/typeDefs/explorer-api';
|
||||
import { TableToolbar } from 'src/components/TableToolbar';
|
||||
import { ContentCard } from 'src/components/ContentCard';
|
||||
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
|
||||
import { Title } from 'src/components/Title';
|
||||
|
||||
export const PageGateways: React.FC = () => {
|
||||
const { gateways } = useMainContext();
|
||||
const [filteredGateways, setFilteredGateways] =
|
||||
React.useState<GatewayResponse>([]);
|
||||
const [pageSize, setPageSize] = React.useState<string>('50');
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
|
||||
const handleSearch = (str: string) => {
|
||||
setSearchTerm(str.toLowerCase());
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (searchTerm === '' && gateways?.data) {
|
||||
setFilteredGateways(gateways?.data);
|
||||
} else {
|
||||
const filtered = gateways?.data?.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]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
width: 120,
|
||||
type: 'number',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const bondAsPunk = printableCoin({
|
||||
amount: params.value as string,
|
||||
denom: 'upunk',
|
||||
});
|
||||
return <Typography sx={cellStyles}>{bondAsPunk}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
||||
width: 150,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Button
|
||||
onClick={() => handleSearch(params.value as string)}
|
||||
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
|
||||
>
|
||||
{params.value}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
setPageSize(event.target.value);
|
||||
};
|
||||
|
||||
if (gateways?.data) {
|
||||
return (
|
||||
<>
|
||||
<Title text="Gateways" />
|
||||
<Grid>
|
||||
<Grid item>
|
||||
<ContentCard>
|
||||
<TableToolbar
|
||||
onChangeSearch={handleSearch}
|
||||
onChangePageSize={handlePageSize}
|
||||
pageSize={pageSize}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
<UniversalDataGrid
|
||||
loading={gateways?.isLoading}
|
||||
columnsData={columns}
|
||||
rows={gatewayToGridRow(filteredGateways)}
|
||||
pageSize={pageSize}
|
||||
pagination={gateways?.data?.length >= 12}
|
||||
hideFooter={gateways?.data?.length < 12}
|
||||
sortModel={[
|
||||
{
|
||||
field: 'bond',
|
||||
sort: 'desc',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,283 @@
|
||||
import * as React from 'react';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { Box, Grid, Typography } from '@mui/material';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ContentCard } from 'src/components/ContentCard';
|
||||
import { WorldMap } from 'src/components/WorldMap';
|
||||
import { BondBreakdownTable } from 'src/components/BondBreakdown';
|
||||
import { TwoColSmallTable } from 'src/components/TwoColSmallTable';
|
||||
import { UptimeChart } from 'src/components/UptimeChart';
|
||||
import { mixnodeToGridRow, scrollToRef } from 'src/utils';
|
||||
import { ComponentError } from 'src/components/ComponentError';
|
||||
import {
|
||||
cellStyles,
|
||||
UniversalDataGrid,
|
||||
} from 'src/components/Universal-DataGrid';
|
||||
import { MixNodeResponseItem } from 'src/typeDefs/explorer-api';
|
||||
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import { Title } from 'src/components/Title';
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
headerName: 'Bond',
|
||||
type: 'number',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const bondAsPunk = printableCoin({
|
||||
amount: params.value as string,
|
||||
denom: 'upunk',
|
||||
});
|
||||
return <Typography sx={cellStyles}>{bondAsPunk}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'self_percentage',
|
||||
headerName: 'Self %',
|
||||
headerAlign: 'left',
|
||||
type: 'number',
|
||||
width: 99,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Self %" />,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}%</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'layer',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Layer" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
type: 'number',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const PageMixnodeDetail: React.FC = () => {
|
||||
const ref = React.useRef();
|
||||
const [row, setRow] = React.useState<MixNodeResponseItem[]>([]);
|
||||
const {
|
||||
fetchMixnodeById,
|
||||
mixnodeDetailInfo,
|
||||
fetchStatsById,
|
||||
fetchDelegationsById,
|
||||
fetchUptimeStoryById,
|
||||
fetchStatusById,
|
||||
stats,
|
||||
status,
|
||||
uptimeStory,
|
||||
} = useMainContext();
|
||||
const { id }: any = useParams();
|
||||
|
||||
React.useEffect(() => {
|
||||
const hasNoDetail = id && !mixnodeDetailInfo;
|
||||
const hasIncorrectDetail =
|
||||
id &&
|
||||
mixnodeDetailInfo?.data &&
|
||||
mixnodeDetailInfo?.data[0].mix_node.identity_key !== id;
|
||||
if (hasNoDetail || hasIncorrectDetail) {
|
||||
fetchMixnodeById(id);
|
||||
fetchDelegationsById(id);
|
||||
fetchStatsById(id);
|
||||
fetchStatusById(id);
|
||||
fetchUptimeStoryById(id);
|
||||
} else if (mixnodeDetailInfo?.data !== undefined) {
|
||||
setRow(mixnodeDetailInfo?.data);
|
||||
}
|
||||
}, [id, mixnodeDetailInfo]);
|
||||
|
||||
React.useEffect(() => {
|
||||
scrollToRef(ref);
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box component="main" ref={ref}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Title text="Mixnode Detail" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
{mixnodeDetailInfo && (
|
||||
<ContentCard>
|
||||
<UniversalDataGrid
|
||||
columnsData={columns}
|
||||
rows={mixnodeToGridRow(row)}
|
||||
loading={mixnodeDetailInfo.isLoading}
|
||||
pageSize="1"
|
||||
pagination={false}
|
||||
hideFooter
|
||||
/>
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12}>
|
||||
<ContentCard title="Bond Breakdown">
|
||||
<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="Uptime story">
|
||||
{uptimeStory.error && (
|
||||
<ComponentError text="There was a problem retrieving uptime history." />
|
||||
)}
|
||||
<UptimeChart
|
||||
loading={uptimeStory.isLoading}
|
||||
xLabel="date"
|
||||
yLabel="uptime"
|
||||
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}>
|
||||
{mixnodeDetailInfo && (
|
||||
<ContentCard title="Location">
|
||||
{mixnodeDetailInfo?.error && (
|
||||
<ComponentError text="There was a problem retrieving this mixnode location" />
|
||||
)}
|
||||
{mixnodeDetailInfo.data &&
|
||||
mixnodeDetailInfo?.data[0]?.location && (
|
||||
<WorldMap
|
||||
loading={mixnodeDetailInfo.isLoading}
|
||||
userLocation={[
|
||||
mixnodeDetailInfo?.data[0]?.location?.lng,
|
||||
mixnodeDetailInfo?.data[0]?.location?.lat,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
import * as React from 'react';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import { Link as RRDLink } from 'react-router-dom';
|
||||
import { Button, Grid, Link as MuiLink } from '@mui/material';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import {
|
||||
cellStyles,
|
||||
UniversalDataGrid,
|
||||
} from 'src/components/Universal-DataGrid';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { mixnodeToGridRow } from 'src/utils';
|
||||
import { TableToolbar } from 'src/components/TableToolbar';
|
||||
import { MixNodeResponse } from 'src/typeDefs/explorer-api';
|
||||
import { BIG_DIPPER } from 'src/api/constants';
|
||||
import { ContentCard } from 'src/components/ContentCard';
|
||||
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
|
||||
import { Title } from 'src/components/Title';
|
||||
|
||||
export const PageMixnodes: React.FC = () => {
|
||||
const { mixnodes } = useMainContext();
|
||||
const [filteredMixnodes, setFilteredMixnodes] =
|
||||
React.useState<MixNodeResponse>([]);
|
||||
const [pageSize, setPageSize] = React.useState<string>('10');
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
|
||||
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]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
flex: 3,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
href={`${BIG_DIPPER}/account/${params.value}`}
|
||||
target="_blank"
|
||||
sx={cellStyles}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
flex: 3,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={cellStyles}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.value}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
headerName: 'Bond',
|
||||
type: 'number',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const bondAsPunk = printableCoin({
|
||||
amount: params.value as string,
|
||||
denom: 'upunk',
|
||||
});
|
||||
return (
|
||||
<MuiLink
|
||||
sx={cellStyles}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.row.identity_key}`}
|
||||
>
|
||||
{bondAsPunk}
|
||||
</MuiLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'self_percentage',
|
||||
headerName: 'Self %',
|
||||
headerAlign: 'left',
|
||||
type: 'number',
|
||||
width: 99,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Self %" />,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={cellStyles}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}%
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={cellStyles}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Button
|
||||
onClick={() => handleSearch(params.value as string)}
|
||||
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
|
||||
>
|
||||
{params.value}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'layer',
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Layer" />,
|
||||
flex: 1,
|
||||
type: 'number',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles, textAlign: 'left' }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
setPageSize(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title text="Mixnodes" />
|
||||
<Grid>
|
||||
<Grid item>
|
||||
<ContentCard>
|
||||
<TableToolbar
|
||||
onChangeSearch={handleSearch}
|
||||
onChangePageSize={handlePageSize}
|
||||
pageSize={pageSize}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
<UniversalDataGrid
|
||||
loading={mixnodes?.isLoading}
|
||||
columnsData={columns}
|
||||
rows={mixnodeToGridRow(filteredMixnodes)}
|
||||
pageSize={pageSize}
|
||||
pagination
|
||||
hideFooter={false}
|
||||
sortModel={[
|
||||
{
|
||||
field: 'bond',
|
||||
sort: 'desc',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Grid,
|
||||
SelectChangeEvent,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { WorldMap } from 'src/components/WorldMap';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import {
|
||||
cellStyles,
|
||||
UniversalDataGrid,
|
||||
} from 'src/components/Universal-DataGrid';
|
||||
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { TableToolbar } from 'src/components/TableToolbar';
|
||||
import { CountryDataRowType, countryDataToGridRow } from 'src/utils';
|
||||
import { Title } from 'src/components/Title';
|
||||
import { ContentCard } from '../../components/ContentCard';
|
||||
|
||||
export const PageMixnodesMap: React.FC = () => {
|
||||
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 sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'nodes',
|
||||
renderHeader: () => (
|
||||
<CustomColumnHeading headingTitle="Number of Nodes" />
|
||||
),
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'percentage',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Percentage %" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{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) => {
|
||||
if (
|
||||
m.countryName.toLowerCase().includes(searchTerm) ||
|
||||
m.ISO3.toLowerCase().includes(searchTerm)
|
||||
) {
|
||||
return m;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
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 container spacing={1} sx={{ mb: 4 }}>
|
||||
<Grid item xs={12}>
|
||||
<Title text="Mixnodes Around the Globe" />
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={9}>
|
||||
<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
|
||||
loading={countryData?.isLoading}
|
||||
columnsData={columns}
|
||||
rows={formattedCountries}
|
||||
pageSize={pageSize}
|
||||
pagination
|
||||
/>
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return <Alert severity="error">{countryData?.error}</Alert>;
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Grid, Link } from '@mui/material';
|
||||
import { WorldMap } from 'src/components/WorldMap';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { formatNumber } from 'src/utils';
|
||||
import { BIG_DIPPER } from 'src/api/constants';
|
||||
import { ValidatorsSVG } from 'src/icons/ValidatorsSVG';
|
||||
import { GatewaysSVG } from 'src/icons/GatewaysSVG';
|
||||
import { MixnodesSVG } from 'src/icons/MixnodesSVG';
|
||||
import { Title } from 'src/components/Title';
|
||||
import { ContentCard } from '../../components/ContentCard';
|
||||
import { StatsCard } from '../../components/StatsCard';
|
||||
|
||||
export const PageOverview: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const { mixnodes, gateways, validators, block, countryData } =
|
||||
useMainContext();
|
||||
return (
|
||||
<>
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<Grid>
|
||||
<Grid item>
|
||||
<Title text="Overview" />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid container spacing={2}>
|
||||
{mixnodes && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => history.push('/network-components/mixnodes')}
|
||||
title="Mixnodes"
|
||||
icon={<MixnodesSVG />}
|
||||
count={mixnodes?.data?.length || ''}
|
||||
errorMsg={mixnodes?.error}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{gateways && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => history.push('/network-components/gateways')}
|
||||
title="Gateways"
|
||||
count={gateways?.data?.length || ''}
|
||||
errorMsg={gateways?.error}
|
||||
icon={<GatewaysSVG />}
|
||||
/>
|
||||
</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}>
|
||||
<ContentCard
|
||||
title={
|
||||
<Link
|
||||
href={`${BIG_DIPPER}/blocks`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
underline="none"
|
||||
color="inherit"
|
||||
>
|
||||
Current block height is {formatNumber(block.data)}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { PageOverview } from 'src/pages/Overview';
|
||||
import { PageMixnodesMap } from 'src/pages/MixnodesMap';
|
||||
import { Page404 } from '../pages/404';
|
||||
import { NetworkComponentsRoutes } from './network-components';
|
||||
|
||||
export const Routes: React.FC = () => (
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/overview" />
|
||||
</Route>
|
||||
<Route exact path="/overview">
|
||||
<PageOverview />
|
||||
</Route>
|
||||
<Route path="/network-components">
|
||||
<NetworkComponentsRoutes />
|
||||
</Route>
|
||||
<Route path="/nodemap">
|
||||
<PageMixnodesMap />
|
||||
</Route>
|
||||
<Route component={Page404} />
|
||||
</Switch>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { Switch, Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { BIG_DIPPER } from 'src/api/constants';
|
||||
import { PageGateways } from 'src/pages/Gateways';
|
||||
import { PageMixnodeDetail } from 'src/pages/MixnodeDetail';
|
||||
import { PageMixnodes } from '../pages/Mixnodes';
|
||||
|
||||
export const NetworkComponentsRoutes: React.FC = () => (
|
||||
<>
|
||||
<Switch>
|
||||
<Route exact path="/network-components/mixnodes">
|
||||
<PageMixnodes />
|
||||
</Route>
|
||||
<Route path="/network-components/mixnodes/:id">
|
||||
<PageMixnodeDetail />
|
||||
</Route>
|
||||
<Route path="/network-components/gateways">
|
||||
<PageGateways />
|
||||
</Route>
|
||||
<Route
|
||||
path="/network-components/validators"
|
||||
component={(props: RouteComponentProps) => {
|
||||
window.open(`${BIG_DIPPER}/validators`);
|
||||
props.history.goBack();
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Route path="/network-components/gateways/:id">
|
||||
<h1> Specific Gateways ID</h1>
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
/* 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-sortIcon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Unable to style nested MUI paper within the Drawer
|
||||
so styling manually here */
|
||||
div.MuiDrawer-root > .MuiDrawer-paperAnchorLeft {
|
||||
background-color: #111826;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
describe('testing jest config', () => {
|
||||
test('jest works with typescript', () => {
|
||||
const foo = () => 42;
|
||||
|
||||
expect(foo()).toBe(42);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user