Compare commits

...

36 Commits

Author SHA1 Message Date
Tommy Verrall 5bf36f1367 cargo update
removed windows headings
changed scripts for building
2021-11-10 11:44:20 +00:00
Mark Sinclair 5da1d89501 Update webview version - see https://github.com/tauri-apps/tauri/issues/2570#issuecomment-916780781
Ignore output in `dist`
2021-11-10 09:29:19 +00:00
Mark Sinclair d7090ecb5a Add webpack config for prod 2021-11-10 09:06:16 +00:00
Mark Sinclair 1a1a2a0355 Build nym wallet with GH Actions 2021-11-10 08:45:43 +00:00
Jędrzej Stuczyński 8bcc931d9b Feature/removal of monitor good nodes (#833)
* Removed separated ipv4 and ipv6 testing

* Testing network using chosen core nodes

This should have probably been like 20 independent commits... sorry...

* SQL migrations for updated schema

* SQL updates

* Using absolute uptime directly

* New uptime calculations

* Config entries, more DB work, some cleanup

* Additional API query routes

* More SQL and API work

* Changed `_` to `-` in new routes

* Removed good topology from config

* Fixed gateways reader yield condition

* Initial gateways pinger

* Minor cleanup and logging level decreases

* Missing trait derivations

* Further logging adjustments

* Unused commented out import

* Claiming additional bandwidth in coconut feature when low

* Fixed build with coconut feature

* Minimum number of test routes

* Making beta/nightly clippy happier
2021-11-09 12:25:41 +00:00
Mark Sinclair 955ef7b871 GitHub Actions: only run job to generate types when not in a pull request (#886) 2021-11-09 11:56:43 +00:00
Mark Sinclair 9ee7ad4fa8 GitHub Actions: only run job to generate types when not in a pull request 2021-11-09 11:42:00 +00:00
Tommy Verrall 79aa1febbe Run all the tests 2021-11-08 15:01:21 +00:00
Fouad 32c4974c44 Merge pull request #873 from nymtech/update/remove-gateway-delegation
remove gateway selection on delegation and undelegation pages
2021-11-08 15:31:58 +01:00
Mark Sinclair 5dadd73dfd Network Explorer (#881)
* fixed styled component

* dynamic colour for isSelected

* corrected type names

* wrapped nav for routing and positioning of main section and dynamic colour for selected section

* overview info panes added

* quick refactor break out components separately

* WorldMap implemented but not data

* map changed and updated to shades correctly

* live data in cards

* added any types for react simple map

* nested routing added but needs tidying and types refactored

* added tooltip to worldmap

* worldmap killed unused props

* updated MUI version to stable v5

* dark mode and ContentCard refactor complete

* refactor of DarkMode context and API into class and context setup

* context refactor for multiple APIs at top level

* mui typography used for error msging instead of jsx/html

* added typeDefs for node api types

* small changes to sx styling

* added types for api responses and main context

* promiseAll for better error handling of individual async calls

* switch out to live API for country nodes

* removal of unnecessary type any and shortening sx style block

* routing and basic mixnodes table and linking

* fixed TS error handling and ts exclude files

* refactor of class API fetch reqs

* renaming to more appropriate explorer-api

* broken - passing to Fouad

* fix for types in context main

* mixnode detail page

* rebasing back before fetch mixnode by ID was implemented

* added basic cache for huge dump of mixnode data

* broken mixnodes context

* fixed mixnode detail fetch

* added hardcoded BondBreakdown section

* added 2 col table for detail page and small refactor of ApiState type for consistent use throughout app

* basic chart with basic dark/theme implemented - no live data

* added scrollToTop useRef for Detail page

* tidied grid items

* media qry for smaller screens

* small changes

* added live data to bonds breakdown 1/2

* small changes/tweaks

* Bondbreakdown retrieves live data

* mixnode stats using live data

* added node status live data

* uptime story added with live data

* date formatting added

* mixnode map

* error handling for mixnode stats

* error handling for port stats

* improved error handling for table - unfinished

* error handling for mixnode table

* handle Loading state for 2colSmallTable

* Uptime story loading handling

* set up data grid component

* remove mixnode value check as handled inside MixnodesDataGrid component

* use loading prop in data grid component

* undo unintentional code formatting

* map blur and linkable data-grid added

* getting ready for gateways and removing con logs

* quickfix for map blur

* PR comment changes

* refactored data grid for reuseability

* Link to open Big Dipper for Blocks

* passing element to title instead of string for routing to Big Dipper

* quick fix for element passed as title for contentCard

* fix for colour coding nodes

* nuts and bolts of search and results per page are working

* media query for responsive search and no-per-page toolbar

* broke out search and pagesize to separate toolbar

* fix for going back to mixnodes datagrid and refetching

* corrected typings for WorldMap

* removed API for topojson

* Cleaner implementation of formatting inline for datagrid

* added Type to Datagrid Rows for mixnode

* removed optional from type for Datagrid

* added page listing the Gateway nodes

* adding clickable location to handleSearch

* tidying util functions and removing dead useEffect

* Add missing constant

* Validators link to Big Dipper

* added validators link to side nav as per Issues card

* SVG icons

* PR tweak to move logic to routes

* removed dead code post rebase

* fixed light dark mode for DataGrid

* light dark mode works on SVGs in Nav

* moving logic back to Nav to avoid window object issues

* neater ternary for SVG icons dark mode

* Better Linking/Styling for cells

* corrected prop/attr name in svg to Reactify it

* moved api url to constants

* SVGs dark now governed by context not props so reverting renderIcon method back to key value setup

* percentage for bond total added

* SVGs for Overview cards Mixnodes Gateways and Validators

* decimalised formatted punks and % of bond for BondBreakdown card

* number formatting via validator module

* adding cossmjs math pkg

* unfinished refactor BondBreakdown

* first few ui tweaks

* Adding google font Open Sans as per designs

* DataGrid unstylable in theme so nuking in css

* adding theming to Block Height card not hardcoded colours

* DataGrid styling

* Nav styling colours but without hover fix

* theme for bond breakdown

* killing con logs

* Datagrid styling

* Nav bar working

* added lines to nav

* removed cursive from fallback fonts

* trimming and refactoring

* removed dead isActive code from nav options

* Color correction for theme on 2col table

* Moved cell styles out to UniversalDatagrid for reuseability

* Nav colors moved to theme

* Removing comments and dead code

* DataGrid UI improvements

* theming for Overview content card

* Bonds updated from UPUNK to PUNK

* corrected SVG warning on stroke-width

* added Boolean class instead of ternary

* fixing up svg attr to jsx props

* merging UPUNK changes into ui-tweaks

* corrected SVG warning on stroke-width

* added Boolean class instead of ternary

* last instance of Boolean

* BondBreakdown handles 0 delegations

* formatting for webpack config and svgs

* Add `npm run lint` and `npm run lint:fix` targets to `package.json`

* Allow `.vscode` directories - exclude them individually like has been done already in the `.gitignore` directory

* Add `vscode` action to run `eslint` on save for the `/explorer/**` sub-directory

* eslint auto fix

* Fix some easy eslint issues

* removing grid pipes and pastel map colors

* Grid xl lg values to align with Search Toolbar

* GitHub Actions: do not trigger Rust actions when the paths are only `/explorer/**`

See https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths for details.

* GitHub Actions: run eslint and annotate pull requests

* socials added to Appbar and Footer

* smaller darkmode icon for mobile

* cleaner code for nav dark light selector

* almost all lint fixes

* post linting Nav fix

* killed con log and removed unused dep

* ref type and removed 1x ts ignore from worldMap

* disabl nested tern w/ nav is refactored on diff br

* icons smaller put into mui List format

* Added hover effect to match DarkLight switch

* ts ignore for worldMap vs no ts decl

* parking changes

* Flipped to MUI SVGs

* re-added external links to Socials

* nav functionality working

* spacing on Mixnode detail page

* datagrid alignment & detail page spacing

* map but no datagrid yet

* killed old SVGs now using mui icons

* added palette instead of strings

* Mixnode Map page working still needs tidying

* better lg xl responsive on Overview and sanitized Page Titles

* removed typography from imports as unused now

* search, sort working & added LG XL responsiveness

* Routing root reqs direct to Overview

* basic 404 page and btn back to overview

* killed fragments and comments

* updated Bond total in column

* Change bond col to type number not text

* Added field to DataGrid and updated MixnodeToGridrow logic

* Added type number so sorting works properly.

* added %self to Detail page

* basic scroll working desktop

* delegations now popout and scroll according to designs

* added stickyHeader and killed dead code

* ExpandMore only renders if delegations exist

* killed old svg icons

* added theme to Overview SVGs

* bringing Title into other pages

* linting fix

* pagination and spacing of gateways cols

* linting fix

* style override for pagination

* added hamburger and changed appbar to fixed

* bringing in other lint fix to pass linting

* PR feedback changes

* Add README.md for theme customisation

* Add hook to get app state context

* Add Nym theme typings to MUI `Theme` types

* Use new theme provider

* Fixing up components to use theme typings
Updated Overview Footer and ContentCard
Footer and Nav socials
Title, Nav chevron and Nav SVGs
Overview SVGs
Light Dark switch
BondBreakdown and 2 col table
DataGrid and 2col Tables
WorldMap UptimeChart and theme changes
WorldMap colors
merge changes
added StatsCard for overview

* Bug fix: do no close drawer when clicking on mixnodes or gateways

* Theme primary colour set to orange highlight, so that default/primary actions are clear to the user. This fixes colours on the pagination page list.

* Fixing up map projection

* Map view uses stroke colour from theme

* added useTheme from correct pkg

* react types upgrade to kill SXProps issue

* SXProps fix removing dead mixins from fixed AppBar

* Scale of Map changed to see more countries

* return type for main Context required

* Fixed map so more countries show

* type for useMainContext hook added

* Remove unused file

* Tidy up imports

* Remove use of `any` by using strongly typed hook to get the app state

* Remove module declaration so that @types/react-simple-maps is used

* API map response changed to indexed object

* Map view uses correct typings from `@types/react-simple-maps` and `d3-scale`

* Make content responsive and fills the view when screen widest

* Link network explorer in title to overview page

* Increase size of card headers to differentiate

* Fix column widths

* Fixed icons showing incorrectly the stats card in mixnode detail view

* Set default sort on mixnodes and gateways to be `bond` descending.

There is an error in MUI data-grid that does not adjust the sort caret based on initial `sortModel` value. Needs investigation.

* GitHub Action for deployment: prefix with network explorer to stop collisions with other deployment projects

* Mixnode list: fix up header title for `host`

* Fix up notification URLs and tidy up readme

* Fix up license information

Co-authored-by: Adrian Thompson <adrian@nymtech.net>
Co-authored-by: Adrian Thompson <adrianthompson@Adrians-MacBook-Air.local>
Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk>
Co-authored-by: Aid19801 <adrianThompson19801@gmail.com>
2021-11-08 12:23:27 +00:00
Mark Sinclair 6ad5badef4 GitHub Action to publish wallet
This is placeholder to allow the action to trigger in branches
2021-11-08 11:03:49 +00:00
Bogdan-Ștefan Neacşu 480ad18b2e Put client_address and id in the correct order (#875) 2021-11-04 16:33:18 +02:00
Jędrzej Stuczyński cdb21f418b Made daily uptime calculation be independent of epoch rewarding (#860)
* Made daily uptime calculation be independent of epoch rewarding

It was moved into a separate timer

* Don't lookup active gateways during rewarding
2021-11-04 13:06:20 +00:00
Jędrzej Stuczyński bd4c18c723 Set MSRV on all binaries to 1.56 (#872) 2021-11-03 20:07:43 +00:00
fmtabbara e7716ae852 remove gateway selection on delegation and undelegation pages 2021-11-03 15:50:36 +00:00
Jędrzej Stuczyński b19528d47e Removed gateway rewarding and delegation (#856)
* Removed gateway rewarding and delegation

* Removed redundant error variants

* [ci skip] Generate TS types

* Test fixes

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
2021-11-03 15:01:59 +00:00
Tommy Verrall ef88ce9252 Merge pull request #871 from nymtech/bug-fix/keyboard-shortcuts-macos
add native window items (copy/paste) via tauri
2021-11-03 14:24:00 +00:00
Bogdan-Ștefan Neacşu 488f1c7742 Remove stale migration code (#868) 2021-11-03 16:01:13 +02:00
fmtabbara 63c698d903 add native menu items via tauri 2021-11-03 11:42:47 +00:00
Jędrzej Stuczyński 7e1c3b4501 Chore/cosmrs update (#862)
* Re-enabled `get_tx` endpoint

* Updated cosmrs to 0.3 and prost to 0.9

* [ci skip] Generate TS types

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
2021-11-03 11:03:18 +00:00
Jędrzej Stuczyński e1d7772a78 Removed epoch rewarding variance (#857) 2021-11-03 11:02:44 +00:00
Tommy Verrall 9db589e0b3 Merge pull request #842 from nymtech/bug-fix/success-overflow
fix delegate success overflow
2021-11-03 10:33:31 +00:00
Tommy Verrall 57c8d69785 Merge branch 'develop' into bug-fix/success-overflow 2021-11-03 10:32:55 +00:00
Jędrzej Stuczyński 4a9cc5b757 Fixed most recent nightly clippy warnings (#865) 2021-11-02 10:48:54 +02:00
Tommy Verrall 06aac9393b Merge pull request #855 from nymtech/feature-request-template
Update feature-request template
2021-11-01 11:31:24 +00:00
Tommy Verrall f4c5b1d67f Update feature-request template 2021-11-01 11:05:54 +00:00
Tommy Verrall 2e0aba6100 Merge pull request #854 from nymtech/issue-template
Update issue templates
2021-11-01 10:51:37 +00:00
Tommy Verrall 8c6549c5dd Update report.md
consolidated the component area
2021-11-01 10:05:24 +00:00
Tommy Verrall 7b431ebfa8 Update .github/ISSUE_TEMPLATE/report.md
Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
2021-11-01 09:55:12 +00:00
Tommy Verrall 3152fd6887 Update .github/ISSUE_TEMPLATE/report.md
Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
2021-11-01 09:54:35 +00:00
Tommy Verrall e03e7f186a Delete create-an-issue.md 2021-11-01 09:47:10 +00:00
Tommy Verrall ec056a548f Delete issue-report.md 2021-11-01 09:46:59 +00:00
Tommy Verrall 24aa075a07 Update issue templates
new issue template for individuals
2021-11-01 09:41:29 +00:00
Jędrzej Stuczyński 8488c4cb0f Overflow checks in release (#846) 2021-10-28 13:07:08 +01:00
Tommy Verrall 20c96b940f Update nym_wallet.yml
check binary exists after name change
2021-10-26 14:10:23 +01:00
fmtabbara 7aad7daa5e fix overflow 2021-10-22 17:52:15 +01:00
170 changed files with 79111 additions and 6332 deletions
+32
View File
@@ -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.
+43
View File
@@ -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
-136
View File
@@ -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
-14
View File
@@ -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
-44
View File
@@ -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
-56
View File
@@ -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
-72
View File
@@ -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 }}
+136
View File
@@ -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 }}
```
-22
View File
@@ -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 }}
-44
View File
@@ -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
-1
View File
@@ -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
View File
@@ -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",
+1
View File
@@ -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;
+1
View File
@@ -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
+1
View File
@@ -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"
+1
View File
@@ -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"]
+1 -1
View File
@@ -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,
+2 -9
View File
@@ -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)
}
-42
View File
@@ -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>>,
+11 -46
View File
@@ -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
+1 -9
View File
@@ -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)
}
+11 -4
View File
@@ -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,
}
-32
View File
@@ -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 {},
}
+1 -18
View File
@@ -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
)
}
}
+3 -6
View File
@@ -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(),
-2
View File
@@ -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,
-56
View File
@@ -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?)
-12
View File
@@ -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,
},
}
+1 -66
View File
@@ -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()),
);
}
}
+6 -636
View File
@@ -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());
}
}
}
-2
View File
@@ -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
}
-435
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["@babel/env", "@babel/react"]
}
+18
View File
@@ -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
+103
View File
@@ -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"]
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
dist
.DS_Store
detailAPI.md
+1
View File
@@ -0,0 +1 @@
14
+6
View File
@@ -0,0 +1,6 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2
}
+5
View File
@@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
+50
View File
@@ -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.
+46
View File
@@ -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
```
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
+30149
View File
File diff suppressed because it is too large Load Diff
+108
View File
@@ -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"
]
}
}
+9
View File
@@ -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>
);
+24
View File
@@ -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';
+102
View File
@@ -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
+217
View File
@@ -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>
);
+47
View File
@@ -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}&nbsp;
</Typography>
{filter ? <ExpandMore /> : <ExpandLess />}
</Box>
);
};
+49
View File
@@ -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>
);
};
+397
View File
@@ -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>
</>
);
};
+31
View File
@@ -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,
};
+57
View File
@@ -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,
};
+72
View File
@@ -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>
);
};
+56
View File
@@ -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>
</>
);
};
+13
View File
@@ -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,
};
+129
View File
@@ -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,
};
+132
View File
@@ -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,
};
+295
View File
@@ -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>
);
};
+14
View File
@@ -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]);
}
+46
View File
@@ -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>
);
};
+24
View File
@@ -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>
);
};
+114
View File
@@ -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>
);
};
+32
View File
@@ -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>
);
};
+33
View File
@@ -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>
);
};
+33
View File
@@ -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>
);
+42
View File
@@ -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>
);
};
+53
View File
@@ -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>
);
};
+17
View File
@@ -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>
+18
View File
@@ -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'),
);
+37
View File
@@ -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

+54
View File
@@ -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>
);
};
+150
View File
@@ -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;
};
+283
View File
@@ -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>
</>
);
};
+211
View File
@@ -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>
</>
);
};
+135
View File
@@ -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>;
};
+90
View File
@@ -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>
</>
);
};
+24
View File
@@ -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>
</>
);
+33
View File
@@ -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;
}
+13
View File
@@ -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();
});
});
+28
View File
@@ -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);
});
});
+7
View File
@@ -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