Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6a81d9213 | |||
| a6db5fe704 | |||
| fe57d08f3e | |||
| ae29b2300c | |||
| 7b98d62f96 | |||
| 6abe95ed61 | |||
| db743578a9 | |||
| ba7f535cb7 | |||
| fd4f5b319c | |||
| 6045f57612 | |||
| 878cb3f0e5 | |||
| 686c0de5eb | |||
| 5ce2e0c8bb | |||
| 68e120dbf4 | |||
| 1362fcdbfa | |||
| 3281f10443 | |||
| 2409f85e31 | |||
| 37c06f338f | |||
| 8a37df64b5 | |||
| 20828d0d28 | |||
| 1e3c8ed3e0 | |||
| 830dbcecfc | |||
| a0d3144837 | |||
| a4988e3547 | |||
| 0b585a15b7 | |||
| 86a93a71e9 | |||
| 967692bf88 | |||
| 662b4d2fff | |||
| add4747e99 | |||
| 242733f144 | |||
| 0cc1926636 | |||
| 0f54c073a7 | |||
| 3bfce128a6 | |||
| 1b3e79e84d | |||
| e94f99211f | |||
| 003ea095cc | |||
| 74d93df74b | |||
| 0c66cc7393 | |||
| 82abfa5c5c | |||
| f7486f0490 | |||
| ee5f0f5808 | |||
| 6cb25cf1fb | |||
| db01c245d9 | |||
| 53347bec67 | |||
| 252385688d | |||
| cbcef9fbcd | |||
| e27fc82524 | |||
| f661bf0446 | |||
| 1d22f35c82 | |||
| fa41fe62c4 | |||
| 94a006a725 | |||
| 98cbd2509c | |||
| d2d99ca5c9 | |||
| 9e4904ff37 | |||
| bf9db4128d | |||
| bce86235c7 | |||
| 423f6bfb55 | |||
| 38fcbb7f2e | |||
| 1785b10d91 | |||
| 7771c5d3d9 | |||
| d6206a04bd | |||
| 83a7f6577b | |||
| 97c6567139 | |||
| a1534d23af | |||
| bcbda85477 | |||
| 54e95d795e | |||
| 11c0e79725 | |||
| 4525be9871 | |||
| b83d4ca1a3 | |||
| bfb868bfc7 | |||
| 7b00282b27 | |||
| 54194c03e1 | |||
| 87c2a317d5 | |||
| be92171fec | |||
| 04eef83c15 | |||
| 25cc7dbebf | |||
| beeb67e9c2 | |||
| ec19de6fa3 | |||
| 575845af38 | |||
| b6b757436e | |||
| eda69447de | |||
| 0f6f47c5ac | |||
| b57c17e5af | |||
| c237b37ea6 | |||
| f2fa221489 | |||
| d7e82b075f | |||
| 0e4787f078 | |||
| f684664472 | |||
| cd5fff92ad | |||
| 9b0b961d43 | |||
| 4b95e71adb | |||
| cde5b66306 | |||
| ac72e20447 | |||
| 571fd5cb93 | |||
| 7f3166d230 | |||
| 68050d77df | |||
| 943337db0d | |||
| 8fbf84174d | |||
| d42a175289 | |||
| ac01df0817 | |||
| 7b27065608 | |||
| 0523ccdce8 | |||
| df8ae52d8c | |||
| 7cfaf6fa1e | |||
| 6e9eab4edb | |||
| 0812378fdd | |||
| 188a7ec91d | |||
| c91bf3f8d1 | |||
| ecb27e2cc2 | |||
| a1961dbc2f | |||
| ef4af0a1db | |||
| 5930ec1f18 | |||
| 72c7049fca | |||
| 92cbe651de | |||
| 777fcf8cb3 | |||
| 945dda0c24 | |||
| b44b074af7 | |||
| 7a0dff5f00 | |||
| 6685b129bb | |||
| e0a80c777e | |||
| 479d410d20 | |||
| ab019266cc | |||
| 92fcae9a37 | |||
| 413e2662ff | |||
| e026a532dd | |||
| a1ca330ce9 | |||
| 139e89643c | |||
| a1e0087760 | |||
| b15cc094ea | |||
| 8eb3f6f862 | |||
| 9032d81d52 | |||
| 7adee63ebe | |||
| 1a5580229b | |||
| df4c6493d4 | |||
| 30a41261ea | |||
| 6d874cc34a | |||
| 7e356ea3b3 | |||
| f7be9e7e6f | |||
| 05374393ef | |||
| 580656c002 | |||
| a7caf97b73 | |||
| 091507b6d8 | |||
| 71b5bc9e71 | |||
| 8bbffb6a88 | |||
| 31c4fc6807 | |||
| 66b9b13edc | |||
| 1b945ae918 | |||
| be9b83a87d | |||
| ba1fb17908 | |||
| 5446874ebe | |||
| afac630a77 | |||
| 3250d6982e | |||
| ecdbe1a6fb | |||
| 6ba79ee924 | |||
| 2bebb4b0c2 | |||
| 81b7d49624 | |||
| f118a0c854 | |||
| db6ecaaecb | |||
| bd13aa6f35 | |||
| 0cad7f635d | |||
| a5e6032393 | |||
| 4edc0700a1 | |||
| 019a04c0fc | |||
| 2e7b8e911f | |||
| 50f4699c95 | |||
| 3265df019a | |||
| 902721bda3 | |||
| 585724fc79 | |||
| c6578384a8 | |||
| 3b245e16db | |||
| 6d0e2cf491 |
@@ -0,0 +1,9 @@
|
||||
# Description
|
||||
|
||||
Closes: #XXXX
|
||||
|
||||
<!-- If appropriate, insert relevant description here -->
|
||||
|
||||
# Checklist:
|
||||
|
||||
- [ ] added a changelog entry to `CHANGELOG.md`
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Continuous integration on dispatch
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ self-hosted, custom-linux-exoscale ]
|
||||
# Enable sccache via environment variable
|
||||
env:
|
||||
RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --workspace
|
||||
|
||||
- name: Run all tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace --all-features
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
name: Clippy checks
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace -- -D warnings
|
||||
|
||||
- name: Build all binaries with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --workspace --features=coconut
|
||||
|
||||
- name: Run all tests with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace --features=coconut
|
||||
|
||||
- name: Run clippy with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --features=coconut -- -D warnings
|
||||
@@ -31,6 +31,8 @@ jobs:
|
||||
# continue-on-error: true
|
||||
- run: yarn && yarn build
|
||||
continue-on-error: true
|
||||
- run: yarn storybook:build
|
||||
name: Build storybook
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
@@ -42,6 +44,17 @@ jobs:
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/network-explorer-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Deploy storybook 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/storybook-static/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ne-sb-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Keybase - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
@@ -51,6 +64,7 @@ jobs:
|
||||
NYM_PROJECT_NAME: "Network Explorer"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "network-explorer-${{ env.GITHUB_REF_SLUG }}"
|
||||
NYM_CI_WWW_LOCATION_STORYBOOK: "ne-sb-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
|
||||
|
||||
@@ -75,6 +75,12 @@ jobs:
|
||||
command: clippy
|
||||
args: --workspace --all-targets -- -D warnings
|
||||
|
||||
- name: Reclaim some disk space (because Windows is being annoying)
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
with:
|
||||
command: clean
|
||||
|
||||
# COCONUT stuff
|
||||
- name: Build all binaries with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -100,6 +106,33 @@ jobs:
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets --features=coconut -- -D warnings
|
||||
|
||||
# nym-wallet (the rust part)
|
||||
- name: Build nym-wallet rust code
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace
|
||||
|
||||
- name: Run nym-wallet tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace
|
||||
|
||||
- name: Check nym-wallet formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path nym-wallet/Cargo.toml --all -- --check
|
||||
|
||||
- name: Run clippy for nym-wallet
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.rust != 'nightly' }}
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings
|
||||
|
||||
notification:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -44,3 +44,4 @@ jobs:
|
||||
target/release/nym-mixnode
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-validator-api
|
||||
target/release/nym-network-requester
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
|
||||
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}/
|
||||
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION_STORYBOOK }}.{{ env.NYM_CI_WWW_BASE }}
|
||||
> ✅ **SUCCESS**
|
||||
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
|
||||
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Nym Wallet (rust)
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'explorer/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'explorer/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ self-hosted, custom-linux-exoscale ]
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace
|
||||
|
||||
- name: Run all tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path nym-wallet/Cargo.toml --all -- --check
|
||||
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
name: Clippy checks
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features
|
||||
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features -- -D warnings
|
||||
@@ -1,5 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- wallet: require password to switch accounts
|
||||
- wallet: add simple CLI tool for decrypting and recovering the wallet file.
|
||||
- wallet: added support for multiple accounts ([#1265])
|
||||
- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint.
|
||||
- mixnet-contract: Replace all naked `-` with `saturating_sub`.
|
||||
- validator-api: add Swagger to document the REST API ([#1249]).
|
||||
- all: added network compilation target to `--help` (or `--version`) commands ([#1256]).
|
||||
- network-requester: send traffic statistics from all network requesters and receive it in a special network-requester that aggregates the data and exposes it via a rest API ([#1267], [#1278]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- vesting-contract: replaced `checked_sub` with `saturating_sub` to fix the underflow in `get_vesting_tokens` ([#1275])
|
||||
- mixnet-contract: removed `expect` in `query_delegator_reward` and queries containing invalid proxy address should now return a more human-readable error ([#1257])
|
||||
- mixnet-contract: Under certain circumstances nodes could not be unbonded ([#1255](https://github.com/nymtech/nym/issues/1255)) ([#1258])
|
||||
- mixnode, gateway: attempting to determine reconnection backoff to persistently failing mixnode could result in a crash ([#1260])
|
||||
|
||||
[#1258]: https://github.com/nymtech/nym/pull/1258
|
||||
[#1249]: https://github.com/nymtech/nym/pull/1249
|
||||
[#1256]: https://github.com/nymtech/nym/pull/1256
|
||||
[#1257]: https://github.com/nymtech/nym/pull/1257
|
||||
[#1260]: https://github.com/nymtech/nym/pull/1260
|
||||
[#1265]: https://github.com/nymtech/nym/pull/1265
|
||||
[#1267]: https://github.com/nymtech/nym/pull/1267
|
||||
[#1275]: https://github.com/nymtech/nym/pull/1275
|
||||
[#1278]: https://github.com/nymtech/nym/pull/1278
|
||||
|
||||
## [nym-wallet-v1.0.4](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.4) (2022-05-04)
|
||||
|
||||
### Changed
|
||||
|
||||
- all: the default behaviour of validator client is changed to use `broadcast_sync` and poll for transaction inclusion instead of using `broadcast_commit` to deal with timeouts ([#1246])
|
||||
|
||||
## [v1.0.1](https://github.com/nymtech/nym/tree/v1.0.1) (2022-05-04)
|
||||
|
||||
### Added
|
||||
|
||||
- validator-api: introduced endpoint for getting average mixnode uptime ([#1238])
|
||||
|
||||
### Changed
|
||||
|
||||
- all: the default behaviour of validator client is changed to use `broadcast_sync` and poll for transaction inclusion instead of using `broadcast_commit` to deal with timeouts ([#1246])
|
||||
|
||||
### Fixed
|
||||
|
||||
- nym-network-requester: is included in the Github Actions for building release binaries
|
||||
|
||||
[#1238]: https://github.com/nymtech/nym/pull/1238
|
||||
[#1246]: https://github.com/nymtech/nym/pull/1246
|
||||
|
||||
## [v1.0.0](https://github.com/nymtech/nym/tree/v1.0.0) (2022-05-03)
|
||||
|
||||
[Full Changelog](https://github.com/nymtech/nym/compare/v0.12.1...v1.0.0)
|
||||
|
||||
Generated
+22
-10
@@ -581,7 +581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "client-core"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"config",
|
||||
"crypto",
|
||||
@@ -648,6 +648,7 @@ dependencies = [
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
@@ -1062,6 +1063,8 @@ dependencies = [
|
||||
"pemstore",
|
||||
"rand 0.7.3",
|
||||
"rand_chacha 0.2.2",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"subtle-encoding",
|
||||
"x25519-dalek",
|
||||
]
|
||||
@@ -1426,6 +1429,7 @@ version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"signature",
|
||||
]
|
||||
|
||||
@@ -1439,6 +1443,7 @@ dependencies = [
|
||||
"ed25519",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"sha2",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -1594,7 +1599,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "explorer-api"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"humantime-serde",
|
||||
@@ -3043,7 +3048,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"clap 2.34.0",
|
||||
"client-core",
|
||||
@@ -3078,7 +3083,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-gateway"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3124,7 +3129,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnode"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
@@ -3162,20 +3167,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"clap 2.34.0",
|
||||
"dirs",
|
||||
"futures",
|
||||
"ipnetwork",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"ordered-buffer",
|
||||
"pretty_env_logger",
|
||||
"proxy-helpers",
|
||||
"publicsuffix",
|
||||
"rand 0.7.3",
|
||||
"rocket",
|
||||
"serde",
|
||||
"socks5-requests",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"websocket-requests",
|
||||
@@ -3183,7 +3194,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"clap 2.34.0",
|
||||
"client-core",
|
||||
@@ -3219,7 +3230,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-validator-api"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4827,9 +4838,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_bytes"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9"
|
||||
checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -5218,6 +5229,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"either",
|
||||
|
||||
@@ -9,6 +9,10 @@ overflow-checks = true
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.test]
|
||||
# equivalent of running in `--release` (but since we're in test profile we're keeping overflow checks and all of those by default)
|
||||
opt-level = 3
|
||||
|
||||
[workspace]
|
||||
|
||||
resolver = "2"
|
||||
|
||||
@@ -26,7 +26,7 @@ clippy-all-wallet:
|
||||
cargo clippy --workspace --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
|
||||
|
||||
test-main:
|
||||
cargo test --all-features --workspace --release
|
||||
cargo test --all-features --workspace
|
||||
|
||||
test-contracts:
|
||||
cargo test --manifest-path contracts/Cargo.toml --all-features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "client-core"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
|
||||
@@ -592,9 +592,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -4825,9 +4825,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::{crate_version, App, ArgMatches};
|
||||
use network_defaults::DEFAULT_NETWORK;
|
||||
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
@@ -67,6 +68,7 @@ fn long_version() -> String {
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
"#,
|
||||
"Build Timestamp:",
|
||||
env!("VERGEN_BUILD_TIMESTAMP"),
|
||||
@@ -84,6 +86,8 @@ fn long_version() -> String {
|
||||
env!("VERGEN_RUSTC_CHANNEL"),
|
||||
"cargo Profile:",
|
||||
env!("VERGEN_CARGO_PROFILE"),
|
||||
"Network:",
|
||||
DEFAULT_NETWORK
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::{crate_version, App, ArgMatches};
|
||||
use network_defaults::DEFAULT_NETWORK;
|
||||
|
||||
pub mod client;
|
||||
mod commands;
|
||||
@@ -67,6 +68,7 @@ fn long_version() -> String {
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
"#,
|
||||
"Build Timestamp:",
|
||||
env!("VERGEN_BUILD_TIMESTAMP"),
|
||||
@@ -84,6 +86,8 @@ fn long_version() -> String {
|
||||
env!("VERGEN_RUSTC_CHANNEL"),
|
||||
"cargo Profile:",
|
||||
env!("VERGEN_CARGO_PROFILE"),
|
||||
"Network:",
|
||||
DEFAULT_NETWORK
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use proxy_helpers::connection_controller::{
|
||||
};
|
||||
use proxy_helpers::proxy_runner::ProxyRunner;
|
||||
use rand::RngCore;
|
||||
use socks5_requests::{ConnectionId, RemoteAddress, Request};
|
||||
use socks5_requests::{ConnectionId, Message, RemoteAddress, Request};
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
@@ -224,8 +224,9 @@ impl SocksClient {
|
||||
|
||||
async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) {
|
||||
let req = Request::new_connect(self.connection_id, remote_address, self.self_address);
|
||||
let msg = Message::Request(req);
|
||||
|
||||
let input_message = InputMessage::new_fresh(self.service_provider, req.into_bytes(), false);
|
||||
let input_message = InputMessage::new_fresh(self.service_provider, msg.into_bytes(), false);
|
||||
self.input_sender.unbounded_send(input_message).unwrap();
|
||||
}
|
||||
|
||||
@@ -252,7 +253,8 @@ impl SocksClient {
|
||||
)
|
||||
.run(move |conn_id, read_data, socket_closed| {
|
||||
let provider_request = Request::new_send(conn_id, read_data, socket_closed);
|
||||
InputMessage::new_fresh(recipient, provider_request.into_bytes(), false)
|
||||
let provider_message = Message::Request(provider_request);
|
||||
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false)
|
||||
})
|
||||
.await
|
||||
.into_inner();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-client-wasm"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
|
||||
license = "Apache-2.0"
|
||||
|
||||
+7
-8
@@ -24,8 +24,7 @@
|
||||
},
|
||||
"../pkg": {
|
||||
"name": "@nymproject/nym-client-wasm",
|
||||
"version": "0.12.0",
|
||||
"license": "Apache-2.0"
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
"version": "0.5.7",
|
||||
@@ -586,9 +585,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -4305,9 +4304,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
|
||||
@@ -133,20 +133,14 @@ impl Client {
|
||||
if current_attempt == 0 {
|
||||
None
|
||||
} else {
|
||||
// according to https://github.com/tokio-rs/tokio/issues/1953 there's an undocumented
|
||||
// limit of tokio delay of about 2 years.
|
||||
// let's ensure our delay is always on a sane side of being maximum 1 hour.
|
||||
let maximum_sane_delay = Duration::from_secs(60 * 60);
|
||||
let exp = 2_u32.checked_pow(current_attempt);
|
||||
let backoff = exp
|
||||
.and_then(|exp| self.config.initial_reconnection_backoff.checked_mul(exp))
|
||||
.unwrap_or(self.config.maximum_reconnection_backoff);
|
||||
|
||||
Some(std::cmp::min(
|
||||
maximum_sane_delay,
|
||||
std::cmp::min(
|
||||
self.config
|
||||
.initial_reconnection_backoff
|
||||
.checked_mul(2_u32.pow(current_attempt))
|
||||
.unwrap_or(self.config.maximum_reconnection_backoff),
|
||||
self.config.maximum_reconnection_backoff,
|
||||
),
|
||||
backoff,
|
||||
self.config.maximum_reconnection_backoff,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -254,3 +248,45 @@ impl SendWithoutResponse for Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn dummy_client() -> Client {
|
||||
Client::new(Config {
|
||||
initial_reconnection_backoff: Duration::from_millis(10_000),
|
||||
maximum_reconnection_backoff: Duration::from_millis(300_000),
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn determining_backoff_works_regardless_of_attempt() {
|
||||
let client = dummy_client();
|
||||
assert!(client.determine_backoff(0).is_none());
|
||||
assert!(client.determine_backoff(1).is_some());
|
||||
assert!(client.determine_backoff(2).is_some());
|
||||
assert_eq!(
|
||||
client.determine_backoff(16).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
assert_eq!(
|
||||
client.determine_backoff(32).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
assert_eq!(
|
||||
client.determine_backoff(1024).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
assert_eq!(
|
||||
client.determine_backoff(65536).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
assert_eq!(
|
||||
client.determine_backoff(u32::MAX).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ use crate::{validator_api, ValidatorClientError};
|
||||
use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse};
|
||||
use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond};
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "nymd-client")]
|
||||
use validator_api_requests::models::UptimeResponse;
|
||||
use validator_api_requests::models::{
|
||||
CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse,
|
||||
@@ -582,6 +585,12 @@ impl<C> Client<C> {
|
||||
Ok(delegations)
|
||||
}
|
||||
|
||||
pub async fn get_mixnode_avg_uptimes(
|
||||
&self,
|
||||
) -> Result<Vec<UptimeResponse>, ValidatorClientError> {
|
||||
Ok(self.validator_api.get_mixnode_avg_uptimes().await?)
|
||||
}
|
||||
|
||||
pub async fn blind_sign(
|
||||
&self,
|
||||
request_body: &BlindSignRequestBody,
|
||||
|
||||
@@ -30,12 +30,26 @@ use cosmwasm_std::Coin as CosmWasmCoin;
|
||||
use prost::Message;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::time::Duration;
|
||||
|
||||
#[async_trait]
|
||||
impl CosmWasmClient for HttpClient {}
|
||||
impl CosmWasmClient for HttpClient {
|
||||
fn broadcast_polling_rate(&self) -> Duration {
|
||||
Duration::from_secs(4)
|
||||
}
|
||||
|
||||
fn broadcast_timeout(&self) -> Duration {
|
||||
Duration::from_secs(60)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait CosmWasmClient: rpc::Client {
|
||||
// this should probably get redesigned, but I'm leaving those like that temporarily to fix
|
||||
// the underlying issue more quickly
|
||||
fn broadcast_polling_rate(&self) -> Duration;
|
||||
fn broadcast_timeout(&self) -> Duration;
|
||||
|
||||
// helper method to remove duplicate code involved in making abci requests with protobuf messages
|
||||
// TODO: perhaps it should have an additional argument to determine whether the response should
|
||||
// require proof?
|
||||
@@ -253,6 +267,42 @@ pub trait CosmWasmClient: rpc::Client {
|
||||
Ok(rpc::Client::broadcast_tx_commit(self, tx).await?)
|
||||
}
|
||||
|
||||
async fn broadcast_tx(&self, tx: Transaction) -> Result<TxResponse, NymdError> {
|
||||
let broadcasted = CosmWasmClient::broadcast_tx_sync(self, tx).await?;
|
||||
|
||||
if broadcasted.code.is_err() {
|
||||
let code_val = broadcasted.code.value();
|
||||
return Err(NymdError::BroadcastTxErrorDeliverTx {
|
||||
hash: broadcasted.hash,
|
||||
height: None,
|
||||
code: code_val,
|
||||
raw_log: broadcasted.log.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let tx_hash = broadcasted.hash;
|
||||
|
||||
let start = tokio::time::Instant::now();
|
||||
loop {
|
||||
log::debug!(
|
||||
"Polling for result of including {} in a block...",
|
||||
broadcasted.hash
|
||||
);
|
||||
if tokio::time::Instant::now().duration_since(start) >= self.broadcast_timeout() {
|
||||
return Err(NymdError::BroadcastTimeout {
|
||||
hash: tx_hash,
|
||||
timeout: self.broadcast_timeout(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(poll_res) = self.get_tx(tx_hash).await {
|
||||
return Ok(poll_res);
|
||||
}
|
||||
|
||||
tokio::time::sleep(self.broadcast_polling_rate()).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_codes(&self) -> Result<Vec<Code>, NymdError> {
|
||||
let path = Some("/cosmwasm.wasm.v1.Query/Codes".parse().unwrap());
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ impl CheckResponse for broadcast::tx_commit::Response {
|
||||
if self.check_tx.code.is_err() {
|
||||
return Err(NymdError::BroadcastTxErrorCheckTx {
|
||||
hash: self.hash,
|
||||
height: self.height,
|
||||
height: Some(self.height),
|
||||
code: self.check_tx.code.value(),
|
||||
raw_log: self.check_tx.log.value().to_owned(),
|
||||
});
|
||||
@@ -28,7 +28,7 @@ impl CheckResponse for broadcast::tx_commit::Response {
|
||||
if self.deliver_tx.code.is_err() {
|
||||
return Err(NymdError::BroadcastTxErrorDeliverTx {
|
||||
hash: self.hash,
|
||||
height: self.height,
|
||||
height: Some(self.height),
|
||||
code: self.deliver_tx.code.value(),
|
||||
raw_log: self.deliver_tx.log.value().to_owned(),
|
||||
});
|
||||
@@ -38,6 +38,21 @@ impl CheckResponse for broadcast::tx_commit::Response {
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckResponse for crate::nymd::TxResponse {
|
||||
fn check_response(self) -> Result<Self, NymdError> {
|
||||
if self.tx_result.code.is_err() {
|
||||
return Err(NymdError::BroadcastTxErrorDeliverTx {
|
||||
hash: self.hash,
|
||||
height: Some(self.height),
|
||||
code: self.tx_result.code.value(),
|
||||
raw_log: self.tx_result.log.value().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compress_wasm_code(code: &[u8]) -> Result<Vec<u8>, NymdError> {
|
||||
// using compression level 9, same as cosmjs, that optimises for size
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::bank::MsgSend;
|
||||
@@ -24,7 +25,7 @@ use crate::nymd::cosmwasm_client::types::*;
|
||||
use crate::nymd::error::NymdError;
|
||||
use crate::nymd::fee::{Fee, DEFAULT_SIMULATED_GAS_MULTIPLIER};
|
||||
use crate::nymd::wallet::DirectSecp256k1HdWallet;
|
||||
use crate::nymd::{CosmosCoin, GasPrice};
|
||||
use crate::nymd::{CosmosCoin, GasPrice, TxResponse};
|
||||
|
||||
// we need to have **a** valid secp256k1 signature for simulation purposes.
|
||||
// it doesn't matter what it is as long as it parses correctly
|
||||
@@ -35,6 +36,9 @@ const DUMMY_SECP256K1_SIGNATURE: &[u8] = &[
|
||||
91,
|
||||
];
|
||||
|
||||
const DEFAULT_BROADCAST_POLLING_RATE: Duration = Duration::from_secs(4);
|
||||
const DEFAULT_BROADCAST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[async_trait]
|
||||
pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
fn signer(&self) -> &DirectSecp256k1HdWallet;
|
||||
@@ -111,12 +115,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.map_err(|_| NymdError::SerializationError("MsgStoreCode".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
.sign_and_broadcast_commit(sender_address, vec![upload_msg], fee, memo)
|
||||
.sign_and_broadcast(sender_address, vec![upload_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()?;
|
||||
|
||||
let logs = parse_raw_logs(tx_res.deliver_tx.log)?;
|
||||
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
|
||||
let logs = parse_raw_logs(tx_res.tx_result.log)?;
|
||||
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
|
||||
|
||||
// TODO: should those strings be extracted into some constants?
|
||||
// the reason I think unwrap here is fine is that if the transaction succeeded and those
|
||||
@@ -172,12 +176,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.map_err(|_| NymdError::SerializationError("MsgInstantiateContract".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
.sign_and_broadcast_commit(sender_address, vec![init_msg], fee, memo)
|
||||
.sign_and_broadcast(sender_address, vec![init_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()?;
|
||||
|
||||
let logs = parse_raw_logs(tx_res.deliver_tx.log)?;
|
||||
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
|
||||
let logs = parse_raw_logs(tx_res.tx_result.log)?;
|
||||
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
|
||||
|
||||
// TODO: should those strings be extracted into some constants?
|
||||
// the reason I think unwrap here is fine is that if the transaction succeeded and those
|
||||
@@ -214,14 +218,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.map_err(|_| NymdError::SerializationError("MsgUpdateAdmin".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
.sign_and_broadcast_commit(sender_address, vec![change_admin_msg], fee, memo)
|
||||
.sign_and_broadcast(sender_address, vec![change_admin_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()?;
|
||||
|
||||
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
|
||||
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
|
||||
|
||||
Ok(ChangeAdminResult {
|
||||
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
|
||||
logs: parse_raw_logs(tx_res.tx_result.log)?,
|
||||
transaction_hash: tx_res.hash,
|
||||
gas_info,
|
||||
})
|
||||
@@ -242,14 +246,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.map_err(|_| NymdError::SerializationError("MsgClearAdmin".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
.sign_and_broadcast_commit(sender_address, vec![change_admin_msg], fee, memo)
|
||||
.sign_and_broadcast(sender_address, vec![change_admin_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()?;
|
||||
|
||||
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
|
||||
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
|
||||
|
||||
Ok(ChangeAdminResult {
|
||||
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
|
||||
logs: parse_raw_logs(tx_res.tx_result.log)?,
|
||||
transaction_hash: tx_res.hash,
|
||||
gas_info,
|
||||
})
|
||||
@@ -277,14 +281,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.map_err(|_| NymdError::SerializationError("MsgMigrateContract".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
.sign_and_broadcast_commit(sender_address, vec![migrate_msg], fee, memo)
|
||||
.sign_and_broadcast(sender_address, vec![migrate_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()?;
|
||||
|
||||
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
|
||||
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
|
||||
|
||||
Ok(MigrateResult {
|
||||
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
|
||||
logs: parse_raw_logs(tx_res.tx_result.log)?,
|
||||
transaction_hash: tx_res.hash,
|
||||
gas_info,
|
||||
})
|
||||
@@ -312,14 +316,15 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned()))?;
|
||||
|
||||
let tx_res = self
|
||||
.sign_and_broadcast_commit(sender_address, vec![execute_msg], fee, memo)
|
||||
.sign_and_broadcast(sender_address, vec![execute_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()?;
|
||||
|
||||
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
|
||||
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
|
||||
|
||||
Ok(ExecuteResult {
|
||||
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
|
||||
logs: parse_raw_logs(tx_res.tx_result.log)?,
|
||||
data: tx_res.tx_result.data,
|
||||
transaction_hash: tx_res.hash,
|
||||
gas_info,
|
||||
})
|
||||
@@ -352,14 +357,15 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let tx_res = self
|
||||
.sign_and_broadcast_commit(sender_address, messages, fee, memo)
|
||||
.sign_and_broadcast(sender_address, messages, fee, memo)
|
||||
.await?
|
||||
.check_response()?;
|
||||
|
||||
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
|
||||
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
|
||||
|
||||
Ok(ExecuteResult {
|
||||
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
|
||||
logs: parse_raw_logs(tx_res.tx_result.log)?,
|
||||
data: tx_res.tx_result.data,
|
||||
transaction_hash: tx_res.hash,
|
||||
gas_info,
|
||||
})
|
||||
@@ -372,7 +378,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
amount: Vec<Coin>,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_commit::Response, NymdError> {
|
||||
) -> Result<TxResponse, NymdError> {
|
||||
let send_msg = MsgSend {
|
||||
from_address: sender_address.clone(),
|
||||
to_address: recipient_address.clone(),
|
||||
@@ -381,7 +387,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?;
|
||||
|
||||
self.sign_and_broadcast_commit(sender_address, vec![send_msg], fee, memo)
|
||||
self.sign_and_broadcast(sender_address, vec![send_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()
|
||||
}
|
||||
@@ -392,7 +398,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
msgs: I,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_commit::Response, NymdError>
|
||||
) -> Result<TxResponse, NymdError>
|
||||
where
|
||||
I: IntoIterator<Item = (AccountId, Vec<Coin>)> + Send,
|
||||
{
|
||||
@@ -409,7 +415,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
self.sign_and_broadcast_commit(sender_address, messages, fee, memo)
|
||||
self.sign_and_broadcast(sender_address, messages, fee, memo)
|
||||
.await?
|
||||
.check_response()
|
||||
}
|
||||
@@ -421,7 +427,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
amount: Coin,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_commit::Response, NymdError> {
|
||||
) -> Result<TxResponse, NymdError> {
|
||||
let delegate_msg = MsgDelegate {
|
||||
delegator_address: delegator_address.to_owned(),
|
||||
validator_address: validator_address.to_owned(),
|
||||
@@ -430,7 +436,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgDelegate".to_owned()))?;
|
||||
|
||||
self.sign_and_broadcast_commit(delegator_address, vec![delegate_msg], fee, memo)
|
||||
self.sign_and_broadcast(delegator_address, vec![delegate_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()
|
||||
}
|
||||
@@ -442,7 +448,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
amount: Coin,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_commit::Response, NymdError> {
|
||||
) -> Result<TxResponse, NymdError> {
|
||||
let undelegate_msg = MsgUndelegate {
|
||||
delegator_address: delegator_address.to_owned(),
|
||||
validator_address: validator_address.to_owned(),
|
||||
@@ -451,7 +457,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgUndelegate".to_owned()))?;
|
||||
|
||||
self.sign_and_broadcast_commit(delegator_address, vec![undelegate_msg], fee, memo)
|
||||
self.sign_and_broadcast(delegator_address, vec![undelegate_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()
|
||||
}
|
||||
@@ -462,7 +468,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
validator_address: &AccountId,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_commit::Response, NymdError> {
|
||||
) -> Result<TxResponse, NymdError> {
|
||||
let withdraw_msg = MsgWithdrawDelegatorReward {
|
||||
delegator_address: delegator_address.to_owned(),
|
||||
validator_address: validator_address.to_owned(),
|
||||
@@ -470,7 +476,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
.to_any()
|
||||
.map_err(|_| NymdError::SerializationError("MsgWithdrawDelegatorReward".to_owned()))?;
|
||||
|
||||
self.sign_and_broadcast_commit(delegator_address, vec![withdraw_msg], fee, memo)
|
||||
self.sign_and_broadcast(delegator_address, vec![withdraw_msg], fee, memo)
|
||||
.await?
|
||||
.check_response()
|
||||
}
|
||||
@@ -573,6 +579,27 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
CosmWasmClient::broadcast_tx_commit(self, tx_bytes.into()).await
|
||||
}
|
||||
|
||||
/// Broadcast a transaction to the network and monitors its inclusion in a block.
|
||||
async fn sign_and_broadcast(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
messages: Vec<Any>,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<TxResponse, NymdError> {
|
||||
let memo = memo.into();
|
||||
let fee = self
|
||||
.determine_transaction_fee(signer_address, &messages, fee, &memo)
|
||||
.await?;
|
||||
|
||||
let tx_raw = self.sign(signer_address, messages, fee, memo).await?;
|
||||
let tx_bytes = tx_raw
|
||||
.to_bytes()
|
||||
.map_err(|_| NymdError::SerializationError("Tx".to_owned()))?;
|
||||
|
||||
self.broadcast_tx(tx_bytes.into()).await
|
||||
}
|
||||
|
||||
fn sign_direct(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
@@ -638,6 +665,9 @@ pub struct Client {
|
||||
rpc_client: HttpClient,
|
||||
signer: DirectSecp256k1HdWallet,
|
||||
gas_price: GasPrice,
|
||||
|
||||
broadcast_polling_rate: Duration,
|
||||
broadcast_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -654,8 +684,18 @@ impl Client {
|
||||
rpc_client,
|
||||
signer,
|
||||
gas_price,
|
||||
broadcast_polling_rate: DEFAULT_BROADCAST_POLLING_RATE,
|
||||
broadcast_timeout: DEFAULT_BROADCAST_TIMEOUT,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_broadcast_polling_rate(&mut self, broadcast_polling_rate: Duration) {
|
||||
self.broadcast_polling_rate = broadcast_polling_rate
|
||||
}
|
||||
|
||||
pub fn set_broadcast_timeout(&mut self, broadcast_timeout: Duration) {
|
||||
self.broadcast_timeout = broadcast_timeout
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -669,7 +709,15 @@ impl rpc::Client for Client {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CosmWasmClient for Client {}
|
||||
impl CosmWasmClient for Client {
|
||||
fn broadcast_polling_rate(&self) -> Duration {
|
||||
self.broadcast_polling_rate
|
||||
}
|
||||
|
||||
fn broadcast_timeout(&self) -> Duration {
|
||||
self.broadcast_timeout
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SigningCosmWasmClient for Client {
|
||||
|
||||
@@ -25,6 +25,7 @@ use cosmrs::proto::cosmwasm::wasm::v1::{
|
||||
CodeInfoResponse, ContractCodeHistoryEntry as ProtoContractCodeHistoryEntry,
|
||||
ContractCodeHistoryOperationType, ContractInfo as ProtoContractInfo,
|
||||
};
|
||||
use cosmrs::tendermint::abci::Data;
|
||||
use cosmrs::tendermint::{abci, chain};
|
||||
use cosmrs::tx::{AccountNumber, Gas, SequenceNumber};
|
||||
use cosmrs::{tx, AccountId, Any, Coin};
|
||||
@@ -672,6 +673,8 @@ pub struct MigrateResult {
|
||||
pub struct ExecuteResult {
|
||||
pub logs: Vec<Log>,
|
||||
|
||||
pub data: Data,
|
||||
|
||||
/// Transaction hash (might be used as transaction ID)
|
||||
pub transaction_hash: tx::Hash,
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::nymd::cosmwasm_client::types::ContractCodeId;
|
||||
use cosmrs::tendermint::{abci, block};
|
||||
use cosmrs::{bip32, tx, AccountId};
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use cosmrs::rpc::error::{
|
||||
@@ -81,21 +82,21 @@ pub enum NymdError {
|
||||
MalformedLogString,
|
||||
|
||||
#[error(
|
||||
"Error when broadcasting tx {hash} at height {height}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
|
||||
"Error when broadcasting tx {hash} at height {height:?}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
|
||||
)]
|
||||
BroadcastTxErrorCheckTx {
|
||||
hash: tx::Hash,
|
||||
height: block::Height,
|
||||
height: Option<block::Height>,
|
||||
code: u32,
|
||||
raw_log: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Error when broadcasting tx {hash} at height {height}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
|
||||
"Error when broadcasting tx {hash} at height {height:?}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
|
||||
)]
|
||||
BroadcastTxErrorDeliverTx {
|
||||
hash: tx::Hash,
|
||||
height: block::Height,
|
||||
height: Option<block::Height>,
|
||||
code: u32,
|
||||
raw_log: String,
|
||||
},
|
||||
@@ -117,6 +118,9 @@ pub enum NymdError {
|
||||
|
||||
#[error("This account does not have BaseAccount information available to it")]
|
||||
NoBaseAccountInformationAvailable,
|
||||
|
||||
#[error("Transaction with ID {hash} has been submitted but not yet found on the chain. You might want to check for it later. There was a total wait of {} seconds", .timeout.as_secs())]
|
||||
BroadcastTimeout { hash: tx::Hash, timeout: Duration },
|
||||
}
|
||||
|
||||
impl NymdError {
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::nymd::cosmwasm_client::types::{
|
||||
};
|
||||
use crate::nymd::error::NymdError;
|
||||
use crate::nymd::wallet::DirectSecp256k1HdWallet;
|
||||
use cosmrs::rpc::endpoint::broadcast;
|
||||
use cosmrs::rpc::Error as TendermintRpcError;
|
||||
use cosmrs::rpc::HttpClientUrl;
|
||||
use cosmwasm_std::{Coin, Uint128};
|
||||
@@ -23,12 +22,14 @@ use mixnet_contract_common::{
|
||||
PagedRewardedSetResponse, QueryMsg, RewardedSetUpdateDetails,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
|
||||
pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
|
||||
pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient;
|
||||
pub use crate::nymd::fee::Fee;
|
||||
use crate::nymd::fee::DEFAULT_SIMULATED_GAS_MULTIPLIER;
|
||||
pub use cosmrs::bank::MsgSend;
|
||||
pub use cosmrs::rpc::endpoint::tx::Response as TxResponse;
|
||||
pub use cosmrs::rpc::endpoint::validators::Response as ValidatorResponse;
|
||||
pub use cosmrs::rpc::HttpClient as QueryNymdClient;
|
||||
@@ -43,7 +44,6 @@ pub use cosmrs::tx::{self, Gas};
|
||||
pub use cosmrs::Coin as CosmosCoin;
|
||||
pub use cosmrs::{AccountId, Decimal, Denom};
|
||||
pub use signing_client::Client as SigningNymdClient;
|
||||
use std::collections::HashMap;
|
||||
pub use traits::{VestingQueryClient, VestingSigningClient};
|
||||
|
||||
pub mod cosmwasm_client;
|
||||
@@ -640,7 +640,7 @@ impl<C> NymdClient<C> {
|
||||
recipient: &AccountId,
|
||||
amount: Vec<CosmosCoin>,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_commit::Response, NymdError>
|
||||
) -> Result<TxResponse, NymdError>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
{
|
||||
@@ -655,7 +655,7 @@ impl<C> NymdClient<C> {
|
||||
&self,
|
||||
msgs: Vec<(AccountId, Vec<CosmosCoin>)>,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<broadcast::tx_commit::Response, NymdError>
|
||||
) -> Result<TxResponse, NymdError>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::collections::HashMap;
|
||||
use url::Url;
|
||||
use validator_api_requests::models::{
|
||||
CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse,
|
||||
RewardEstimationResponse, StakeSaturationResponse,
|
||||
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
@@ -253,6 +253,36 @@ impl Client {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_mixnode_avg_uptime(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
) -> Result<UptimeResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
identity,
|
||||
routes::AVG_UPTIME,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_mixnode_avg_uptimes(&self) -> Result<Vec<UptimeResponse>, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODES,
|
||||
routes::AVG_UPTIME,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn blind_sign(
|
||||
&self,
|
||||
request_body: &BlindSignRequestBody,
|
||||
|
||||
@@ -26,5 +26,6 @@ pub const SINCE_ARG: &str = "since";
|
||||
|
||||
pub const STATUS: &str = "status";
|
||||
pub const REWARD_ESTIMATION: &str = "reward-estimation";
|
||||
pub const AVG_UPTIME: &str = "avg_uptime";
|
||||
pub const STAKE_SATURATION: &str = "stake-saturation";
|
||||
pub const INCLUSION_CHANCE: &str = "inclusion-probability";
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1.0.0"
|
||||
handlebars = "3.0.1"
|
||||
humantime-serde = "1.0"
|
||||
log = "0.4"
|
||||
|
||||
@@ -69,14 +69,16 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
let location = custom_location
|
||||
.unwrap_or_else(|| self.config_directory().join(Self::config_file_name()));
|
||||
|
||||
fs::write(location.clone(), templated_config)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
let mut perms = fs::metadata(location.clone())?.permissions();
|
||||
#[cfg(unix)]
|
||||
perms.set_mode(0o600);
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(location, perms)?;
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(unix)] {
|
||||
fs::write(location.clone(), templated_config)?;
|
||||
let mut perms = fs::metadata(location.clone())?.permissions();
|
||||
perms.set_mode(0o600);
|
||||
fs::set_permissions(location, perms)?;
|
||||
} else {
|
||||
fs::write(location, templated_config)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -47,11 +47,8 @@ impl NodeEpochRewards {
|
||||
|
||||
pub fn node_profit(&self) -> U128 {
|
||||
let reward = U128::from_num(self.reward().u128());
|
||||
if reward < self.operator_cost() {
|
||||
U128::from_num(0u128)
|
||||
} else {
|
||||
reward - self.operator_cost()
|
||||
}
|
||||
// if operating cost is higher then the reward node profit is 0
|
||||
reward.saturating_sub(self.operator_cost())
|
||||
}
|
||||
|
||||
pub fn operator_reward(&self, profit_margin: U128) -> Result<Uint128, MixnetContractError> {
|
||||
|
||||
@@ -19,6 +19,8 @@ cipher = { version = "0.4.3", optional = true }
|
||||
x25519-dalek = { version = "1.1", optional = true }
|
||||
ed25519-dalek = { version = "1.0", optional = true }
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"], optional = true }
|
||||
serde_bytes = { version = "0.11.6", optional = true }
|
||||
serde_crate = { version = "1.0", optional = true, default_features = false, package = "serde" }
|
||||
subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
|
||||
|
||||
# internal
|
||||
@@ -30,6 +32,7 @@ config = { path="../../common/config" }
|
||||
rand_chacha = "0.2"
|
||||
|
||||
[features]
|
||||
serde = ["serde_crate", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"]
|
||||
symmetric = ["aes", "ctr", "cipher", "generic-array"]
|
||||
|
||||
@@ -68,6 +68,7 @@ pub fn creating_dealing_for_3_parties(c: &mut Criterion) {
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -89,6 +90,7 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
);
|
||||
|
||||
let first_key = dks.get_mut(0).unwrap();
|
||||
@@ -99,7 +101,7 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing
|
||||
.verify(¶ms, epoch, threshold, &receivers)
|
||||
.verify(¶ms, epoch, threshold, &receivers, None)
|
||||
.is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
|
||||
})
|
||||
@@ -128,6 +130,7 @@ pub fn creating_dealing_for_20_parties(c: &mut Criterion) {
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -150,6 +153,7 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
);
|
||||
|
||||
let first_key = dks.get_mut(0).unwrap();
|
||||
@@ -160,7 +164,7 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing
|
||||
.verify(¶ms, epoch, threshold, &receivers)
|
||||
.verify(¶ms, epoch, threshold, &receivers, None)
|
||||
.is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
|
||||
})
|
||||
@@ -189,6 +193,7 @@ pub fn creating_dealing_for_100_parties(c: &mut Criterion) {
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -211,6 +216,7 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
);
|
||||
|
||||
let first_key = dks.get_mut(0).unwrap();
|
||||
@@ -221,7 +227,7 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing
|
||||
.verify(¶ms, epoch, threshold, &receivers)
|
||||
.verify(¶ms, epoch, threshold, &receivers, None)
|
||||
.is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
|
||||
})
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
use pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
#[cfg(feature = "rand")]
|
||||
use rand::{CryptoRng, RngCore};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Size of a X25519 private key
|
||||
@@ -127,6 +129,28 @@ impl PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for PublicKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
Ok(PublicKey(x25519_dalek::PublicKey::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKey for PublicKey {
|
||||
type Error = KeyRecoveryError;
|
||||
|
||||
@@ -143,7 +167,6 @@ impl PemStorableKey for PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PrivateKey(x25519_dalek::StaticSecret);
|
||||
|
||||
impl Display for PrivateKey {
|
||||
@@ -187,6 +210,28 @@ impl PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PrivateKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for PrivateKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
Ok(PrivateKey(x25519_dalek::StaticSecret::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKey for PrivateKey {
|
||||
type Error = KeyRecoveryError;
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@ use pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::Error as SerdeError;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde_bytes::{ByteBuf as SerdeByteBuf, Bytes as SerdeBytes};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Ed25519RecoveryError {
|
||||
MalformedBytes(SignatureError),
|
||||
@@ -40,6 +47,7 @@ impl fmt::Display for Ed25519RecoveryError {
|
||||
impl std::error::Error for Ed25519RecoveryError {}
|
||||
|
||||
/// Keypair for usage in ed25519 EdDSA.
|
||||
#[derive(Debug)]
|
||||
pub struct KeyPair {
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
@@ -135,6 +143,28 @@ impl PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for PublicKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
Ok(PublicKey(ed25519_dalek::PublicKey::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKey for PublicKey {
|
||||
type Error = Ed25519RecoveryError;
|
||||
|
||||
@@ -200,6 +230,28 @@ impl PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PrivateKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for PrivateKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
Ok(PrivateKey(ed25519_dalek::SecretKey::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKey for PrivateKey {
|
||||
type Error = Ed25519RecoveryError;
|
||||
|
||||
@@ -216,7 +268,7 @@ impl PemStorableKey for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Signature(ed25519_dalek::Signature);
|
||||
|
||||
impl Signature {
|
||||
@@ -237,3 +289,24 @@ impl Signature {
|
||||
Ok(Signature(ed25519_dalek::Signature::from_bytes(bytes)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Signature {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
SerdeBytes::new(&self.to_bytes()).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for Signature {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
let bytes = <SerdeByteBuf>::deserialize(deserializer)?;
|
||||
Signature::from_bytes(bytes.as_ref()).map_err(SerdeError::custom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +29,5 @@ pub use blake3;
|
||||
#[cfg(feature = "symmetric")]
|
||||
pub use ctr;
|
||||
|
||||
// TODO: this function uses all three modules: asymmetric crypto, symmetric crypto and derives key...,
|
||||
// so I don't know where to put it...
|
||||
#[cfg(feature = "serde")]
|
||||
extern crate serde_crate as serde;
|
||||
|
||||
@@ -56,6 +56,10 @@ impl Network {
|
||||
self.details().rewarding_validator_address
|
||||
}
|
||||
|
||||
pub fn stats_provider_network_address(&self) -> &str {
|
||||
self.details().stats_provider_network_address
|
||||
}
|
||||
|
||||
pub fn validators(&self) -> impl Iterator<Item = &ValidatorDetails> {
|
||||
self.details().validators.iter()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ cfg_if::cfg_if! {
|
||||
if #[cfg(network = "mainnet")] {
|
||||
pub const DEFAULT_NETWORK: all::Network = all::Network::MAINNET;
|
||||
pub const DENOM: &str = mainnet::DENOM;
|
||||
pub const STAKE_DENOM: &str = mainnet::STAKE_DENOM;
|
||||
|
||||
pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS;
|
||||
pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_ERC20_CONTRACT_ADDRESS;
|
||||
@@ -25,6 +26,7 @@ cfg_if::cfg_if! {
|
||||
} else if #[cfg(network = "qa")] {
|
||||
pub const DEFAULT_NETWORK: all::Network = all::Network::QA;
|
||||
pub const DENOM: &str = qa::DENOM;
|
||||
pub const STAKE_DENOM: &str = qa::STAKE_DENOM;
|
||||
|
||||
pub const ETH_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_CONTRACT_ADDRESS;
|
||||
pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_ERC20_CONTRACT_ADDRESS;
|
||||
@@ -32,6 +34,7 @@ cfg_if::cfg_if! {
|
||||
} else if #[cfg(network = "sandbox")] {
|
||||
pub const DEFAULT_NETWORK: all::Network = all::Network::SANDBOX;
|
||||
pub const DENOM: &str = sandbox::DENOM;
|
||||
pub const STAKE_DENOM: &str = sandbox::STAKE_DENOM;
|
||||
|
||||
pub const ETH_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_CONTRACT_ADDRESS;
|
||||
pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_ERC20_CONTRACT_ADDRESS;
|
||||
@@ -49,6 +52,7 @@ pub struct DefaultNetworkDetails<'a> {
|
||||
vesting_contract_address: &'a str,
|
||||
bandwidth_claim_contract_address: &'a str,
|
||||
rewarding_validator_address: &'a str,
|
||||
stats_provider_network_address: &'a str,
|
||||
validators: Vec<ValidatorDetails>,
|
||||
}
|
||||
|
||||
@@ -60,6 +64,7 @@ static MAINNET_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> =
|
||||
vesting_contract_address: mainnet::VESTING_CONTRACT_ADDRESS,
|
||||
bandwidth_claim_contract_address: mainnet::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
|
||||
rewarding_validator_address: mainnet::REWARDING_VALIDATOR_ADDRESS,
|
||||
stats_provider_network_address: mainnet::STATS_PROVIDER_CLIENT_ADDRESS,
|
||||
validators: mainnet::validators(),
|
||||
});
|
||||
|
||||
@@ -71,6 +76,7 @@ static SANDBOX_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> =
|
||||
vesting_contract_address: sandbox::VESTING_CONTRACT_ADDRESS,
|
||||
bandwidth_claim_contract_address: sandbox::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
|
||||
rewarding_validator_address: sandbox::REWARDING_VALIDATOR_ADDRESS,
|
||||
stats_provider_network_address: sandbox::STATS_PROVIDER_CLIENT_ADDRESS,
|
||||
validators: sandbox::validators(),
|
||||
});
|
||||
|
||||
@@ -81,6 +87,7 @@ static QA_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> = Lazy::new(|| DefaultN
|
||||
vesting_contract_address: qa::VESTING_CONTRACT_ADDRESS,
|
||||
bandwidth_claim_contract_address: qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
|
||||
rewarding_validator_address: qa::REWARDING_VALIDATOR_ADDRESS,
|
||||
stats_provider_network_address: qa::STATS_PROVIDER_CLIENT_ADDRESS,
|
||||
validators: qa::validators(),
|
||||
});
|
||||
|
||||
@@ -101,6 +108,13 @@ impl ValidatorDetails {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_name(nymd_url: &str, api_url: Option<&str>) -> Self {
|
||||
ValidatorDetails {
|
||||
nymd_url: nymd_url.to_string(),
|
||||
api_url: api_url.map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nymd_url(&self) -> Url {
|
||||
self.nymd_url
|
||||
.parse()
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::ValidatorDetails;
|
||||
|
||||
pub(crate) const BECH32_PREFIX: &str = "n";
|
||||
pub const DENOM: &str = "unym";
|
||||
pub const STAKE_DENOM: &str = "unyx";
|
||||
|
||||
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
|
||||
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
|
||||
@@ -18,9 +19,11 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
|
||||
|
||||
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "3V3me68qkEYNNShSQ5yLkrzC8rUJmcmtrTFbLKPqytEZ.7dGmnRAheEozNeGAsp9LXM8oPgS5YgJraNmYguj2t7Bn@BNjYZPxzcJwczXHHgBxCAyVJKxN6LPteDRrKapxWmexv";
|
||||
|
||||
pub(crate) fn validators() -> Vec<ValidatorDetails> {
|
||||
vec![ValidatorDetails::new(
|
||||
"https://rpc.nyx.nodes.guru/",
|
||||
Some("https://validator.nymtech.net/api"),
|
||||
Some("https://validator.nymtech.net/api/"),
|
||||
)]
|
||||
}
|
||||
|
||||
@@ -3,22 +3,27 @@
|
||||
|
||||
use crate::ValidatorDetails;
|
||||
|
||||
pub(crate) const BECH32_PREFIX: &str = "nymt";
|
||||
pub const DENOM: &str = "unymt";
|
||||
pub(crate) const BECH32_PREFIX: &str = "n";
|
||||
pub const DENOM: &str = "unym";
|
||||
pub const STAKE_DENOM: &str = "unyx";
|
||||
|
||||
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt17x6pt4msccvawgxjeg5nmnygttu56tftg5l6j3";
|
||||
pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt1t4dmskxea0avvrj8xtmu66hv7dkyg9s8059t3c";
|
||||
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
|
||||
"n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep";
|
||||
pub(crate) const VESTING_CONTRACT_ADDRESS: &str =
|
||||
"n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav";
|
||||
pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str =
|
||||
"nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv";
|
||||
"n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1dn52nx8wv9wkqmrvj6tcmdzh4es6jt8tr7f6j9";
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq";
|
||||
|
||||
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "BLFPkyQ68xtR3TmrUWJZUKJF4SVwJR23wzQEmLHi2QcZ.5zms2X4ANsgY1VB4iC9kTqvbsHWmWUNSuvTtYr4Cp5qT@ExyJVqTSrgHTwzXm2r9RawfF5qYpvZjSVN2dLTs6bnWH";
|
||||
|
||||
pub(crate) fn validators() -> Vec<ValidatorDetails> {
|
||||
vec![ValidatorDetails::new(
|
||||
"https://qa-validator.nymtech.net",
|
||||
Some("https://qa-validator.nymtech.net/api"),
|
||||
Some("https://qa-validator-api.nymtech.net/api"),
|
||||
)]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::ValidatorDetails;
|
||||
|
||||
pub(crate) const BECH32_PREFIX: &str = "nymt";
|
||||
pub const DENOM: &str = "unymt";
|
||||
pub const STAKE_DENOM: &str = "unyxt";
|
||||
|
||||
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j";
|
||||
pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt14ejqjyq8um4p3xfqj74yld5waqljf88fn549lh";
|
||||
@@ -16,6 +17,8 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("E8883BAeF3869e14E4823F46662e81D4F7d2A81F");
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1jh0s6qu6tuw9ut438836mmn7f3f2wencrnmdj4";
|
||||
|
||||
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "HqYWvCcB4sswYiyMj5Q8H5oc71kLf96vfrLK3npM7stH.CoeC5dcqurgdxr5zcgU77nZBSBCc8ntCiwUivQ9TX3KT@E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM";
|
||||
|
||||
pub(crate) fn validators() -> Vec<ValidatorDetails> {
|
||||
vec![ValidatorDetails::new(
|
||||
"https://sandbox-validator.nymtech.net",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod msg;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use msg::*;
|
||||
pub use request::*;
|
||||
pub use response::*;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::request::{Request, RequestError};
|
||||
use crate::response::{Response, ResponseError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MessageError {
|
||||
Request(RequestError),
|
||||
Response(ResponseError),
|
||||
NoData,
|
||||
UnknownMessageType,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MessageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MessageError::Request(r) => write!(f, "{}", r),
|
||||
MessageError::Response(r) => write!(f, "{:?}", r),
|
||||
MessageError::NoData => write!(f, "no data provided"),
|
||||
MessageError::UnknownMessageType => write!(f, "unknown message type received"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
Request(Request),
|
||||
Response(Response),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
const REQUEST_FLAG: u8 = 0;
|
||||
const RESPONSE_FLAG: u8 = 1;
|
||||
|
||||
pub fn try_from_bytes(b: &[u8]) -> Result<Message, MessageError> {
|
||||
if b.is_empty() {
|
||||
return Err(MessageError::NoData);
|
||||
}
|
||||
|
||||
if b[0] == Self::REQUEST_FLAG {
|
||||
Request::try_from_bytes(&b[1..])
|
||||
.map(Message::Request)
|
||||
.map_err(MessageError::Request)
|
||||
} else if b[0] == Self::RESPONSE_FLAG {
|
||||
Response::try_from_bytes(&b[1..])
|
||||
.map(Message::Response)
|
||||
.map_err(MessageError::Response)
|
||||
} else {
|
||||
Err(MessageError::UnknownMessageType)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
match self {
|
||||
Self::Request(r) => std::iter::once(Self::REQUEST_FLAG)
|
||||
.chain(r.into_bytes().iter().cloned())
|
||||
.collect(),
|
||||
Self::Response(r) => std::iter::once(Self::RESPONSE_FLAG)
|
||||
.chain(r.into_bytes().iter().cloned())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -227,6 +227,7 @@ dependencies = [
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
|
||||
@@ -48,12 +48,15 @@ pub fn query_delegator_reward(
|
||||
mix_identity: IdentityKey,
|
||||
proxy: Option<String>,
|
||||
) -> Result<Uint128, ContractError> {
|
||||
let proxy = proxy.map(|p| {
|
||||
deps.api
|
||||
.addr_validate(&p)
|
||||
.map_err(|_| ContractError::InvalidAddress(p))
|
||||
.expect("proxy address is invalid")
|
||||
});
|
||||
let proxy = match proxy {
|
||||
Some(proxy) => Some(
|
||||
deps.api
|
||||
.addr_validate(&proxy)
|
||||
.map_err(|_| ContractError::InvalidAddress(proxy))?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let key = mixnet_contract_common::delegation::generate_storage_key(
|
||||
&deps.api.addr_validate(&owner)?,
|
||||
proxy.as_ref(),
|
||||
|
||||
@@ -60,5 +60,5 @@ pub fn decr_reward_pool(
|
||||
|
||||
pub fn circulating_supply(storage: &dyn Storage) -> StdResult<Uint128> {
|
||||
let reward_pool = REWARD_POOL.load(storage)?;
|
||||
Ok(Uint128::new(TOTAL_SUPPLY) - reward_pool)
|
||||
Ok(Uint128::new(TOTAL_SUPPLY).saturating_sub(reward_pool))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::rewards::helpers;
|
||||
use crate::support::helpers::is_authorized;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{Addr, Api, Coin, DepsMut, Env, MessageInfo, Order, Response, Storage, Uint128};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract_common::events::{
|
||||
new_compound_delegator_reward_event, new_compound_operator_reward_event,
|
||||
new_mix_operator_rewarding_event, new_not_found_mix_operator_rewarding_event,
|
||||
@@ -81,7 +82,8 @@ pub fn _try_compound_operator_reward(
|
||||
|
||||
let mut updated_bond = bond.clone();
|
||||
let reward = calculate_operator_reward(storage, api, owner, &bond)?;
|
||||
updated_bond.accumulated_rewards = Some(updated_bond.accumulated_rewards() - reward);
|
||||
updated_bond.accumulated_rewards =
|
||||
Some(updated_bond.accumulated_rewards().saturating_sub(reward));
|
||||
updated_bond.pledge_amount.amount += reward;
|
||||
mixnodes().replace(
|
||||
storage,
|
||||
@@ -113,9 +115,13 @@ pub fn calculate_operator_reward(
|
||||
let accumulated_rewards = mixnodes()
|
||||
.changelog()
|
||||
.prefix(bond.identity())
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
.keys(
|
||||
storage,
|
||||
Some(Bound::exclusive(last_claimed_height)),
|
||||
None,
|
||||
Order::Ascending,
|
||||
)
|
||||
.filter_map(|height| height.ok())
|
||||
.filter(|height| last_claimed_height <= *height)
|
||||
.fold(
|
||||
Ok(Uint128::zero()),
|
||||
|acc, height| -> Result<Uint128, ContractError> {
|
||||
@@ -254,7 +260,7 @@ pub fn _try_compound_delegator_reward(
|
||||
|
||||
{
|
||||
if let Some(mut bond) = mixnodes().may_load(deps.storage, mix_identity)? {
|
||||
bond.accumulated_rewards = Some(bond.accumulated_rewards() - reward);
|
||||
bond.accumulated_rewards = Some(bond.accumulated_rewards().saturating_sub(reward));
|
||||
mixnodes().save(deps.storage, mix_identity, &bond, block_height)?;
|
||||
}
|
||||
}
|
||||
@@ -283,32 +289,46 @@ pub fn calculate_delegator_reward(
|
||||
|
||||
// Get delegations newer then last_claimed_height, it would be nice to also fold this into the iteration bellow but it should be ok for now, as
|
||||
// I doubt folks refresh their delegations often
|
||||
let delegations = delegations_storage::delegations()
|
||||
let mut delegations = delegations_storage::delegations()
|
||||
.prefix((mix_identity.to_string(), key))
|
||||
.range(storage, None, None, Order::Descending)
|
||||
.range(
|
||||
storage,
|
||||
Some(Bound::exclusive(last_claimed_height)),
|
||||
None,
|
||||
Order::Descending,
|
||||
)
|
||||
.filter_map(|record| record.ok())
|
||||
.filter(|(height, _)| last_claimed_height <= *height)
|
||||
.map(|(_, delegation)| delegation)
|
||||
.collect::<Vec<Delegation>>();
|
||||
|
||||
// Accumulate outside of the loop to gain some speed, on a log of checkpoints
|
||||
let mut delegation_at_height = Uint128::zero();
|
||||
|
||||
// This is a bit gnarly, but we want to avoid loading all heights, the loading mixnodes, so we're doing it all in the iterator
|
||||
let accumulated_rewards = mixnodes()
|
||||
.changelog()
|
||||
.prefix(mix_identity)
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
.keys(
|
||||
storage,
|
||||
Some(Bound::exclusive(last_claimed_height)),
|
||||
None,
|
||||
Order::Ascending,
|
||||
)
|
||||
.filter_map(|height| height.ok())
|
||||
// Get all checkpoints greater then last claimed delegation height
|
||||
.filter(|height| last_claimed_height <= *height)
|
||||
.fold(
|
||||
Ok(Uint128::zero()),
|
||||
|acc, height| -> Result<Uint128, ContractError> {
|
||||
let accumulated_reward = acc?;
|
||||
let delegation_at_height = delegations
|
||||
delegation_at_height = delegations
|
||||
.iter()
|
||||
.filter(|d| d.block_height <= height)
|
||||
.fold(Uint128::zero(), |total, delegation| {
|
||||
.fold(delegation_at_height, |total, delegation| {
|
||||
total + delegation.amount.amount
|
||||
});
|
||||
// Drop what we've processed
|
||||
// This should be replaced with drain_filter once it stabilizes
|
||||
delegations.retain(|d| d.block_height > height);
|
||||
// debug_with_visibility(
|
||||
// api,
|
||||
// format!("delegation at height {} - {}", height, delegation_at_height),
|
||||
|
||||
@@ -14,24 +14,22 @@ impl VestingAccount for Account {
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Coin, ContractError> {
|
||||
// Returns 0 in case of underflow.
|
||||
// Returns 0 in case of underflow. Which is fine, as the amount of pledged and delegated tokens can be larger then vesting_coins due to rewards and vesting periods expiring
|
||||
Ok(Coin {
|
||||
amount: Uint128::new(
|
||||
self.get_vesting_coins(block_time, env)?
|
||||
.amount
|
||||
.u128()
|
||||
.checked_sub(
|
||||
.saturating_sub(
|
||||
self.get_delegated_vesting(block_time, env, storage)?
|
||||
.amount
|
||||
.u128(),
|
||||
)
|
||||
.ok_or(ContractError::Underflow)?
|
||||
.checked_sub(
|
||||
.saturating_sub(
|
||||
self.get_pledged_vesting(block_time, env, storage)?
|
||||
.amount
|
||||
.u128(),
|
||||
)
|
||||
.ok_or(ContractError::Underflow)?,
|
||||
),
|
||||
),
|
||||
denom: DENOM.to_string(),
|
||||
})
|
||||
|
||||
+116
-100
@@ -1,106 +1,122 @@
|
||||
version: '3.7'
|
||||
x-bech32-prefix: &BECH32_PREFIX
|
||||
nymt
|
||||
x-wasmd-version: &WASMD_VERSION
|
||||
v0.21.0
|
||||
x-wasmd-commit-hash: &WASMD_COMMIT_HASH
|
||||
1d436638af7cacb5aeeb7248b57b085c64f3ae35
|
||||
|
||||
x-network: &NETWORK
|
||||
BECH32_PREFIX: nymt
|
||||
DENOM: nymt
|
||||
STAKE_DENOM: nyxt
|
||||
WASMD_VERSION: v0.26.0
|
||||
WASMD_COMMIT_HASH: dc5ef6fe84f0a5e3b0894692a18cc48fb5b00adf
|
||||
|
||||
services:
|
||||
genesis_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
WASMD_VERSION: *WASMD_VERSION
|
||||
WASMD_COMMIT_HASH: *WASMD_COMMIT_HASH
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "26657:26657"
|
||||
- "1317:1317"
|
||||
container_name: genesis_validator
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
environment:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
WASMD_VERSION: *WASMD_VERSION
|
||||
command: ["genesis"]
|
||||
secondary_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
WASMD_VERSION: *WASMD_VERSION
|
||||
image: validator:latest
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
environment:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
WASMD_VERSION: *WASMD_VERSION
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
command: ["secondary"]
|
||||
mixnet_contract:
|
||||
build: docker/mixnet_contract
|
||||
image: contract:latest
|
||||
volumes:
|
||||
- ".:/nym"
|
||||
vesting_contract:
|
||||
build: docker/vesting_contract
|
||||
image: vesting_contract:latest
|
||||
volumes:
|
||||
- ".:/nym"
|
||||
contract_uploader:
|
||||
build: docker/typescript_client
|
||||
image: contract_uploader:typescript
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
- "contract_volume:/contract_volume"
|
||||
- ".:/nym"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
- "secondary_validator"
|
||||
- "mixnet_contract"
|
||||
environment:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
mnemonic_echo:
|
||||
build: docker/mnemonic_echo
|
||||
image: mnemonic_echo:latest
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
genesis_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args: *NETWORK
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "26657:26657"
|
||||
- "1317:1317"
|
||||
container_name: genesis_validator
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
- "genesis_nymd:/root/.nymd"
|
||||
environment: *NETWORK
|
||||
networks:
|
||||
localnet:
|
||||
ipv4_address: 172.168.10.2
|
||||
command: [ "genesis" ]
|
||||
secondary_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args: *NETWORK
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "36657:26657"
|
||||
- "2317:1317"
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
- "secondary_nymd:/root/.nymd"
|
||||
environment: *NETWORK
|
||||
networks:
|
||||
localnet:
|
||||
ipv4_address: 172.168.10.3
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
command: [ "secondary" ]
|
||||
# mixnet_contract:
|
||||
# build: docker/mixnet_contract
|
||||
# image: contract:latest
|
||||
# volumes:
|
||||
# - ".:/nym"
|
||||
# vesting_contract:
|
||||
# build: docker/vesting_contract
|
||||
# image: vesting_contract:latest
|
||||
# volumes:
|
||||
# - ".:/nym"
|
||||
# contract_uploader:
|
||||
# build: docker/typescript_client
|
||||
# image: contract_uploader:typescript
|
||||
# volumes:
|
||||
# - "genesis_volume:/genesis_volume:ro"
|
||||
# - "contract_volume:/contract_volume"
|
||||
# - ".:/nym"
|
||||
# depends_on:
|
||||
# - "genesis_validator"
|
||||
# - "secondary_validator"
|
||||
# - "mixnet_contract"
|
||||
# environment:
|
||||
# BECH32_PREFIX: *BECH32_PREFIX
|
||||
mnemonic_echo:
|
||||
build: docker/mnemonic_echo
|
||||
image: mnemonic_echo:latest
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
- "secondary_validator"
|
||||
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
command:
|
||||
- --storageEngine=wiredTiger
|
||||
volumes:
|
||||
- mongo_data:/data/db
|
||||
block_explorer:
|
||||
build:
|
||||
context: https://github.com/forbole/big-dipper.git#v0.41.x-7
|
||||
image: block_explorer:v0.41.x-7
|
||||
ports:
|
||||
- "3080:3000"
|
||||
depends_on:
|
||||
- "mongo"
|
||||
environment:
|
||||
ROOT_URL: ${APP_ROOT_URL:-http://localhost}
|
||||
MONGO_URL: mongodb://mongo:27017/meteor
|
||||
PORT: 3000
|
||||
METEOR_SETTINGS: ${METEOR_SETTINGS}
|
||||
explorer:
|
||||
build:
|
||||
context: docker/explorer
|
||||
image: explorer:latest
|
||||
ports:
|
||||
- "3040:3000"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
- "block_explorer"
|
||||
# mongo:
|
||||
# image: mongo:latest
|
||||
# command:
|
||||
# - --storageEngine=wiredTiger
|
||||
# volumes:
|
||||
# - mongo_data:/data/db
|
||||
# block_explorer:
|
||||
# build:
|
||||
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
|
||||
# image: block_explorer:v0.41.x-7
|
||||
# ports:
|
||||
# - "3080:3000"
|
||||
# depends_on:
|
||||
# - "mongo"
|
||||
# environment:
|
||||
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
|
||||
# MONGO_URL: mongodb://mongo:27017/meteor
|
||||
# PORT: 3000
|
||||
# METEOR_SETTINGS: ${METEOR_SETTINGS}
|
||||
# explorer:
|
||||
# build:
|
||||
# context: docker/explorer
|
||||
# image: explorer:latest
|
||||
# ports:
|
||||
# - "3040:3000"
|
||||
# depends_on:
|
||||
# - "genesis_validator"
|
||||
# - "block_explorer"
|
||||
|
||||
volumes:
|
||||
genesis_volume:
|
||||
contract_volume:
|
||||
mongo_data:
|
||||
genesis_volume:
|
||||
genesis_nymd:
|
||||
secondary_nymd:
|
||||
|
||||
# contract_volume:
|
||||
# mongo_data:
|
||||
|
||||
|
||||
networks:
|
||||
localnet:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.168.10.0/25
|
||||
@@ -1,9 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Wait for the mnemonic to be generated
|
||||
# Wait for the mnemonic(s) to be generated
|
||||
while ! [ -s /genesis_volume/genesis_mnemonic ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "This is the current mnemonic:"
|
||||
while ! [ -s /genesis_volume/secondary_mnemonic ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "This is the current genesis mnemonic:"
|
||||
cat /genesis_volume/genesis_mnemonic
|
||||
|
||||
echo "This is the current secondary mnemonic:"
|
||||
cat /genesis_volume/secondary_mnemonic
|
||||
|
||||
@@ -6,17 +6,29 @@ PASSPHRASE=passphrase
|
||||
cd /root
|
||||
|
||||
if [ "$1" = "genesis" ]; then
|
||||
if [ ! -d "/root/.nymd" ]; then
|
||||
if [ ! -f "/root/.nymd/config/genesis.json" ]; then
|
||||
./nymd init nymnet --chain-id nymnet 2> /dev/null
|
||||
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${BECH32_PREFIX}"'"/' /root/.nymd/config/app.toml
|
||||
# staking/governance token is hardcoded in config, change this
|
||||
sed -i "s/\"stake\"/\"u${STAKE_DENOM}\"/" /root/.nymd/config/genesis.json
|
||||
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${DENOM}"'"/' /root/.nymd/config/app.toml
|
||||
sed -i '0,/enable = false/s//enable = true/g' /root/.nymd/config/app.toml
|
||||
sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' /root/.nymd/config/config.toml
|
||||
sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' /root/.nymd/config/config.toml
|
||||
sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' /root/.nymd/config/config.toml
|
||||
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2>&1 >/dev/null | tail -n 1 > /genesis_volume/genesis_mnemonic
|
||||
ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show node_admin -a)
|
||||
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${ADDRESS}" 1000000000000000u${BECH32_PREFIX},1000000000000000stake
|
||||
yes "${PASSPHRASE}" | ./nymd gentx node_admin 1000000000stake --chain-id nymnet 2> /dev/null
|
||||
|
||||
# create accounts
|
||||
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2>&1 >/dev/null | tail -n 1 > /root/.nymd/mnemonic
|
||||
yes "${PASSPHRASE}" | ./nymd keys add secondary 2>&1 >/dev/null | tail -n 1 > /root/.nymd/secondary_mnemonic
|
||||
cp /root/.nymd/mnemonic /genesis_volume/genesis_mnemonic
|
||||
cp /root/.nymd/secondary_mnemonic /genesis_volume/secondary_mnemonic
|
||||
|
||||
# add genesis accounts with some initial tokens
|
||||
GENESIS_ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show node_admin -a)
|
||||
SECONDARY_ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show secondary -a)
|
||||
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${GENESIS_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}"
|
||||
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${SECONDARY_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}"
|
||||
|
||||
yes "${PASSPHRASE}" | ./nymd gentx node_admin 1000000000u"${STAKE_DENOM}" --chain-id nymnet 2> /dev/null
|
||||
./nymd collect-gentxs 2> /dev/null
|
||||
./nymd validate-genesis > /dev/null
|
||||
cp /root/.nymd/config/genesis.json /genesis_volume/genesis.json
|
||||
@@ -26,7 +38,7 @@ if [ "$1" = "genesis" ]; then
|
||||
fi
|
||||
./nymd start
|
||||
elif [ "$1" = "secondary" ]; then
|
||||
if [ ! -d "/root/.nymd" ]; then
|
||||
if [ ! -f "/root/.nymd/config/genesis.json" ]; then
|
||||
./nymd init nymnet --chain-id nym-secondary 2> /dev/null
|
||||
|
||||
# Wait until the genesis node writes the genesis.json to the shared volume
|
||||
@@ -34,16 +46,27 @@ elif [ "$1" = "secondary" ]; then
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# wait for the actual validator to start up
|
||||
sleep 5
|
||||
|
||||
cp /genesis_volume/genesis.json /root/.nymd/config/genesis.json
|
||||
GENESIS_PEER=$(cat /root/.nymd/config/genesis.json | grep '"memo"' | cut -d'"' -f 4)
|
||||
GENESIS_IP=$(cat /root/.nymd/config/genesis.json | grep '"memo"' | cut -d'@' -f2 | cut -d: -f1)
|
||||
sed -i 's/persistent_peers = ""/persistent_peers = "'"${GENESIS_PEER}"'"/' /root/.nymd/config/config.toml
|
||||
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${BECH32_PREFIX}"'"/' /root/.nymd/config/app.toml
|
||||
sed -i '0,/enable = false/s//enable = true/g' /root/.nymd/config/app.toml
|
||||
sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' /root/.nymd/config/config.toml
|
||||
sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' /root/.nymd/config/config.toml
|
||||
sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' /root/.nymd/config/config.toml
|
||||
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2> mnemonic > /dev/null
|
||||
|
||||
# import mnemonic generated by the genesis validator (have a local copy for ease of use)
|
||||
cp /genesis_volume/secondary_mnemonic /root/.nymd/mnemonic
|
||||
{ cat /root/.nymd/mnemonic; echo "${PASSPHRASE}"; echo "${PASSPHRASE}"; } | ./nymd keys add node_admin --recover #> /dev/null
|
||||
./nymd validate-genesis > /dev/null
|
||||
|
||||
# create validator
|
||||
# don't even ask about those sleeps...
|
||||
{ echo "${PASSPHRASE}"; sleep 10; yes; sleep 10; } | ./nymd tx staking create-validator --amount=10000000u"${STAKE_DENOM}" --fees 100000u"${DENOM}" --pubkey="$(./nymd tendermint show-validator)" --moniker="secondary" --commission-rate="0.10" --commission-max-rate="0.20" --commission-max-change-rate="0.01" --min-self-delegation="1" --chain-id=nymnet --from=node_admin -b async --node http://"${GENESIS_IP}":26657
|
||||
else
|
||||
echo "Validator already initialized, starting with the existing configuration."
|
||||
echo "If you want to re-init the validator, destroy the existing container"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "explorer-api"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(crate) struct PrettyDetailedMixNodeBond {
|
||||
pub owner: Addr,
|
||||
pub layer: Layer,
|
||||
pub mix_node: MixNode,
|
||||
pub avg_uptime: Option<u8>,
|
||||
}
|
||||
|
||||
pub(crate) struct MixNodeCache {
|
||||
|
||||
@@ -9,7 +9,9 @@ use serde::Serialize;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use mixnet_contract_common::MixNodeBond;
|
||||
use validator_client::models::UptimeResponse;
|
||||
|
||||
use crate::cache::Cache;
|
||||
use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond};
|
||||
use crate::mix_nodes::location::{Location, LocationCache, LocationCacheItem};
|
||||
use crate::mix_nodes::CACHE_ENTRY_TTL;
|
||||
@@ -76,10 +78,16 @@ impl MixNodesResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct MixNodeHealth {
|
||||
avg_uptime: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadsafeMixNodesCache {
|
||||
mixnodes: Arc<RwLock<MixNodesResult>>,
|
||||
locations: Arc<RwLock<LocationCache>>,
|
||||
mixnode_health: Arc<RwLock<Cache<MixNodeHealth>>>,
|
||||
}
|
||||
|
||||
impl ThreadsafeMixNodesCache {
|
||||
@@ -87,6 +95,7 @@ impl ThreadsafeMixNodesCache {
|
||||
ThreadsafeMixNodesCache {
|
||||
mixnodes: Arc::new(RwLock::new(MixNodesResult::new())),
|
||||
locations: Arc::new(RwLock::new(LocationCache::new())),
|
||||
mixnode_health: Arc::new(RwLock::new(Cache::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +103,7 @@ impl ThreadsafeMixNodesCache {
|
||||
ThreadsafeMixNodesCache {
|
||||
mixnodes: Arc::new(RwLock::new(MixNodesResult::new())),
|
||||
locations: Arc::new(RwLock::new(locations)),
|
||||
mixnode_health: Arc::new(RwLock::new(Cache::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,9 +142,11 @@ impl ThreadsafeMixNodesCache {
|
||||
) -> Option<PrettyDetailedMixNodeBond> {
|
||||
let mixnodes_guard = self.mixnodes.read().await;
|
||||
let location_guard = self.locations.read().await;
|
||||
let mixnode_health_guard = self.mixnode_health.read().await;
|
||||
|
||||
let bond = mixnodes_guard.get_mixnode(identity_key);
|
||||
let location = location_guard.get(identity_key);
|
||||
let health = mixnode_health_guard.get(identity_key);
|
||||
|
||||
match bond {
|
||||
Some(bond) => Some(PrettyDetailedMixNodeBond {
|
||||
@@ -145,6 +157,7 @@ impl ThreadsafeMixNodesCache {
|
||||
owner: bond.owner,
|
||||
layer: bond.layer,
|
||||
mix_node: bond.mix_node,
|
||||
avg_uptime: health.map(|m| m.avg_uptime),
|
||||
}),
|
||||
None => None,
|
||||
}
|
||||
@@ -153,6 +166,7 @@ impl ThreadsafeMixNodesCache {
|
||||
pub(crate) async fn get_detailed_mixnodes(&self) -> Vec<PrettyDetailedMixNodeBond> {
|
||||
let mixnodes_guard = self.mixnodes.read().await;
|
||||
let location_guard = self.locations.read().await;
|
||||
let mixnode_health_guard = self.mixnode_health.read().await;
|
||||
|
||||
mixnodes_guard
|
||||
.all_mixnodes
|
||||
@@ -160,6 +174,7 @@ impl ThreadsafeMixNodesCache {
|
||||
.map(|bond| {
|
||||
let location = location_guard.get(&bond.mix_node.identity_key);
|
||||
let copy = bond.clone();
|
||||
let health = mixnode_health_guard.get(&bond.mix_node.identity_key);
|
||||
PrettyDetailedMixNodeBond {
|
||||
location: location.and_then(|l| l.location.clone()),
|
||||
status: mixnodes_guard.determine_node_status(&bond.mix_node.identity_key),
|
||||
@@ -168,6 +183,7 @@ impl ThreadsafeMixNodesCache {
|
||||
owner: copy.owner,
|
||||
layer: copy.layer,
|
||||
mix_node: copy.mix_node,
|
||||
avg_uptime: health.map(|m| m.avg_uptime),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -188,4 +204,14 @@ impl ThreadsafeMixNodesCache {
|
||||
guard.active_mixnodes = active_nodes;
|
||||
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_health_cache(&self, all_uptimes: Vec<UptimeResponse>) {
|
||||
let mut mixnode_health = self.mixnode_health.write().await;
|
||||
for uptime in all_uptimes {
|
||||
let health = MixNodeHealth {
|
||||
avg_uptime: uptime.avg_uptime,
|
||||
};
|
||||
mixnode_health.set(&uptime.identity, health);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use std::future::Future;
|
||||
|
||||
use mixnet_contract_common::{GatewayBond, MixNodeBond};
|
||||
use validator_client::models::UptimeResponse;
|
||||
use validator_client::nymd::error::NymdError;
|
||||
use validator_client::nymd::{Paging, QueryNymdClient, ValidatorResponse};
|
||||
use validator_client::ValidatorClientError;
|
||||
@@ -88,6 +89,17 @@ impl ExplorerApiTasks {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn retrieve_all_mixnode_avg_uptimes(
|
||||
&self,
|
||||
) -> Result<Vec<UptimeResponse>, ValidatorClientError> {
|
||||
self.state
|
||||
.inner
|
||||
.validator_client
|
||||
.0
|
||||
.get_mixnode_avg_uptimes()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_mixnode_cache(&self) {
|
||||
let all_bonds = self.retrieve_all_mixnodes().await;
|
||||
let rewarded_nodes = self
|
||||
@@ -109,6 +121,21 @@ impl ExplorerApiTasks {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn update_mixnode_health_cache(&self) {
|
||||
match self.retrieve_all_mixnode_avg_uptimes().await {
|
||||
Ok(response) => {
|
||||
self.state
|
||||
.inner
|
||||
.mixnodes
|
||||
.update_health_cache(response)
|
||||
.await
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get mixnode avg uptimes: {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_validators_cache(&self) {
|
||||
match self.retrieve_all_validators().await {
|
||||
Ok(response) => self.state.inner.validators.update_cache(response).await,
|
||||
@@ -145,6 +172,9 @@ impl ExplorerApiTasks {
|
||||
|
||||
info!("Updating mix node cache...");
|
||||
self.update_mixnode_cache().await;
|
||||
|
||||
info!("Updating mix node health cache...");
|
||||
self.update_mixnode_health_cache().await;
|
||||
info!("Done");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'@nymproject/eslint-config-react-typescript'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"@nymproject/eslint-config-react-typescript"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
builder: 'webpack5',
|
||||
},
|
||||
// webpackFinal: async (config, { configType }) => {
|
||||
// // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
|
||||
// // You can change the configuration based on that.
|
||||
// // 'PRODUCTION' is used when building the static version of storybook.
|
||||
webpackFinal: async (config) => {
|
||||
config.module.rules.forEach((rule) => {
|
||||
// look for SVG import rule and replace
|
||||
// NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/
|
||||
if (rule.test?.toString().includes('svg')) {
|
||||
rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
|
||||
}
|
||||
});
|
||||
|
||||
// handle asset loading with this
|
||||
config.module.rules.unshift({
|
||||
test: /\.svg(\?.*)?$/i,
|
||||
issuer: /\.[jt]sx?$/,
|
||||
use: ['@svgr/webpack'],
|
||||
});
|
||||
|
||||
config.resolve.extensions = ['.tsx', '.ts', '.js'];
|
||||
config.resolve.plugins = [new TsconfigPathsPlugin()];
|
||||
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
tls: false,
|
||||
path: false,
|
||||
http: false,
|
||||
https: false,
|
||||
stream: false,
|
||||
crypto: false,
|
||||
net: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
config.plugins.push(new ForkTsCheckerWebpackPlugin({
|
||||
typescript: {
|
||||
mode: 'write-references',
|
||||
diagnosticOptions: {
|
||||
semantic: true,
|
||||
syntactic: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Return the altered config
|
||||
return config;
|
||||
},
|
||||
features: {
|
||||
emotionAlias: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const withThemeProvider = (Story, context) => (
|
||||
<div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}>
|
||||
<div>
|
||||
<NymNetworkExplorerThemeProvider mode="light">
|
||||
<Box
|
||||
p={4}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: '80vh 2rem',
|
||||
background: (theme) => theme.palette.background.default,
|
||||
color: (theme) => theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ overflowY: 'auto' }}>
|
||||
<Story {...context} />
|
||||
</Box>
|
||||
<h4 style={{ textAlign: 'center' }}>Light mode</h4>
|
||||
</Box>
|
||||
</NymNetworkExplorerThemeProvider>
|
||||
</div>
|
||||
<div>
|
||||
<NymNetworkExplorerThemeProvider mode="dark">
|
||||
<Box
|
||||
p={4}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: '80vh 2rem',
|
||||
background: (theme) => theme.palette.background.default,
|
||||
color: (theme) => theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ overflowY: 'auto' }}>
|
||||
<Story {...context} />
|
||||
</Box>
|
||||
<h4 style={{ textAlign: 'center' }}>Dark mode</h4>
|
||||
</Box>
|
||||
</NymNetworkExplorerThemeProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const decorators = [withThemeProvider];
|
||||
+13
-3
@@ -30,8 +30,15 @@
|
||||
"use-clipboard-copy": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@storybook/addon-actions": "^6.4.19",
|
||||
"@storybook/addon-essentials": "^6.4.19",
|
||||
"@storybook/addon-interactions": "^6.4.19",
|
||||
"@storybook/addon-links": "^6.4.19",
|
||||
"@storybook/react": "^6.4.19",
|
||||
"@storybook/testing-library": "^0.0.9",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
@@ -95,10 +102,13 @@
|
||||
"build:serve": "npx serve dist",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"tsc": "tsc",
|
||||
"tsc:watch": "tsc --watch",
|
||||
"tsc": "tsc --noEmit true",
|
||||
"tsc:watch": "tsc --watch --noEmit true",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
"lint:fix": "eslint src --fix",
|
||||
"prestorybook": "yarn --cwd .. build",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook:build": "build-storybook"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
MixNodeResponse,
|
||||
MixNodeResponseItem,
|
||||
MixnodeStatus,
|
||||
MixNodeEconomicDynamicsStatsResponse,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
SummaryOverviewResponse,
|
||||
@@ -122,6 +123,9 @@ export class Api {
|
||||
static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
|
||||
|
||||
static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json();
|
||||
|
||||
static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json();
|
||||
|
||||
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
|
||||
@@ -12,12 +12,13 @@ export type ColumnsType = {
|
||||
headerAlign: string;
|
||||
flex?: number;
|
||||
width?: number;
|
||||
tooltipInfo?: string;
|
||||
};
|
||||
|
||||
export interface UniversalTableProps {
|
||||
export interface UniversalTableProps<T = any> {
|
||||
tableName: string;
|
||||
columnsData: ColumnsType[];
|
||||
rows: any[];
|
||||
rows: T[];
|
||||
}
|
||||
|
||||
function formatCellValues(val: string | number, field: string) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, Box, CircularProgress, useMediaQuery } from '@mui/material';
|
||||
import { Alert, Box, CircularProgress, useMediaQuery, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
@@ -98,7 +98,7 @@ export const BondBreakdownTable: React.FC = () => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell align="left">Pledge total</TableCell>
|
||||
<TableCell align="left">Self</TableCell>
|
||||
<TableCell align="left" data-testid="pledge-total-amount">
|
||||
{bonds.pledges}
|
||||
</TableCell>
|
||||
@@ -127,21 +127,62 @@ export const BondBreakdownTable: React.FC = () => {
|
||||
sx={{
|
||||
maxHeight: 400,
|
||||
overflowY: 'scroll',
|
||||
p: 2,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
width: '100%',
|
||||
p: 2,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
data-testid="delegations-total-amount"
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Delegations
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{`(${delegations?.data?.length} delegators)`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Table stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600, background: '#242C3D' }} align="left">
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Delegators
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, background: '#242C3D' }} align="left">
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: theme.palette.background.paper,
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Stake
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
background: '#242C3D',
|
||||
background: theme.palette.background.paper,
|
||||
width: '200px',
|
||||
}}
|
||||
align="left"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ColumnsType } from '../../DetailTable';
|
||||
|
||||
export const EconomicsInfoColumns: ColumnsType[] = [
|
||||
{
|
||||
field: 'estimatedTotalReward',
|
||||
title: 'Estimated Total Reward',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo: 'Estimated reward per epoch for this profit margin if your node is selected in the active set.',
|
||||
},
|
||||
{
|
||||
field: 'estimatedOperatorReward',
|
||||
title: 'Estimated Operator Reward',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo: 'Estimated reward per epoch for this profit margin if your node is selected in the active set.',
|
||||
},
|
||||
{
|
||||
field: 'selectionChance',
|
||||
title: 'Active Set Probability',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.',
|
||||
},
|
||||
{
|
||||
field: 'stakeSaturation',
|
||||
title: 'Stake Saturation',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is: 1 million NYM, computed as S/K where S is total amount of tokens available to stakeholders and K is the number of nodes in the reward set.',
|
||||
},
|
||||
{
|
||||
field: 'profitMargin',
|
||||
title: 'Profit Margin',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Percentage of the delegates rewards that the operator takes as fee before rewards are distributed to the delegates.',
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Uptime',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo: 'Node’s average uptime in the last 24h.',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { EconomicsProgress } from './EconomicsProgress';
|
||||
|
||||
export default {
|
||||
title: 'Mix Node Detail/Economics/ProgressBar',
|
||||
component: EconomicsProgress,
|
||||
} as ComponentMeta<typeof EconomicsProgress>;
|
||||
|
||||
const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />;
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {};
|
||||
|
||||
export const OverThreshold = Template.bind({});
|
||||
OverThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 120,
|
||||
};
|
||||
|
||||
export const UnderThreshold = Template.bind({});
|
||||
UnderThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 80,
|
||||
};
|
||||
|
||||
export const OnThreshold = Template.bind({});
|
||||
OnThreshold.args = {
|
||||
threshold: 100,
|
||||
value: 100,
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box } from '@mui/system';
|
||||
|
||||
const parseToNumber = (value: number | undefined | string) =>
|
||||
typeof value === 'string' ? parseInt(value || '', 10) : value || 0;
|
||||
|
||||
export const EconomicsProgress: React.FC<
|
||||
LinearProgressProps & {
|
||||
threshold?: number;
|
||||
}
|
||||
> = ({ threshold, ...props }) => {
|
||||
const theme = useTheme();
|
||||
const { value } = props;
|
||||
|
||||
const valueNumber: number = parseToNumber(value);
|
||||
const thresholdNumber: number = parseToNumber(threshold);
|
||||
const percentageColor = valueNumber > (threshold || 100) ? 'warning' : 'inherit';
|
||||
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 6 / 10,
|
||||
color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee,
|
||||
}}
|
||||
>
|
||||
<LinearProgress
|
||||
{...props}
|
||||
variant="determinate"
|
||||
color={percentageColor}
|
||||
value={percentageToDisplay}
|
||||
sx={{ width: '100%', borderRadius: '5px', backgroundColor: theme.palette.common.white }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { DelegatorsInfoTable } from './Table';
|
||||
import { EconomicsInfoColumns } from './Columns';
|
||||
import { EconomicsInfoRowWithIndex } from './types';
|
||||
|
||||
export default {
|
||||
title: 'Mix Node Detail/Economics',
|
||||
component: DelegatorsInfoTable,
|
||||
} as ComponentMeta<typeof DelegatorsInfoTable>;
|
||||
|
||||
const row: EconomicsInfoRowWithIndex = {
|
||||
id: 1,
|
||||
selectionChance: {
|
||||
value: 'High',
|
||||
},
|
||||
avgUptime: {
|
||||
value: '65 %',
|
||||
},
|
||||
estimatedOperatorReward: {
|
||||
value: '80000.123456 NYM',
|
||||
},
|
||||
estimatedTotalReward: {
|
||||
value: '80000.123456 NYM',
|
||||
},
|
||||
profitMargin: {
|
||||
value: '10 %',
|
||||
},
|
||||
stakeSaturation: {
|
||||
value: '80 %',
|
||||
progressBarValue: 80,
|
||||
},
|
||||
};
|
||||
|
||||
const rowVeryHighProbabilitySelection: EconomicsInfoRowWithIndex = {
|
||||
...row,
|
||||
selectionChance: {
|
||||
value: 'Very High',
|
||||
},
|
||||
};
|
||||
|
||||
const rowModerateProbabilitySelection: EconomicsInfoRowWithIndex = {
|
||||
...row,
|
||||
selectionChance: {
|
||||
value: 'Moderate',
|
||||
},
|
||||
};
|
||||
|
||||
const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = {
|
||||
...row,
|
||||
selectionChance: {
|
||||
value: 'Low',
|
||||
},
|
||||
};
|
||||
|
||||
const rowVeryLowProbabilitySelection: EconomicsInfoRowWithIndex = {
|
||||
...row,
|
||||
selectionChance: {
|
||||
value: 'Very Low',
|
||||
},
|
||||
};
|
||||
|
||||
const emptyRow: EconomicsInfoRowWithIndex = {
|
||||
id: 1,
|
||||
selectionChance: {
|
||||
value: '-',
|
||||
progressBarValue: 0,
|
||||
},
|
||||
avgUptime: {
|
||||
value: '-',
|
||||
},
|
||||
estimatedOperatorReward: {
|
||||
value: '-',
|
||||
},
|
||||
estimatedTotalReward: {
|
||||
value: '-',
|
||||
},
|
||||
profitMargin: {
|
||||
value: '-',
|
||||
},
|
||||
stakeSaturation: {
|
||||
value: '-',
|
||||
progressBarValue: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />;
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
rows: [emptyRow],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceVeryHigh = Template.bind({});
|
||||
selectionChanceVeryHigh.args = {
|
||||
rows: [rowVeryHighProbabilitySelection],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceHigh = Template.bind({});
|
||||
selectionChanceHigh.args = {
|
||||
rows: [row],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceModerate = Template.bind({});
|
||||
selectionChanceModerate.args = {
|
||||
rows: [rowModerateProbabilitySelection],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceLow = Template.bind({});
|
||||
selectionChanceLow.args = {
|
||||
rows: [rowLowProbabilitySelection],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
|
||||
export const selectionChanceVeryLow = Template.bind({});
|
||||
selectionChanceVeryLow.args = {
|
||||
rows: [rowVeryLowProbabilitySelection],
|
||||
columnsData: EconomicsInfoColumns,
|
||||
tableName: 'storybook',
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { currencyToString } from '../../../utils/currency';
|
||||
import { useMixnodeContext } from '../../../context/mixnode';
|
||||
import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api';
|
||||
import { EconomicsInfoRowWithIndex } from './types';
|
||||
|
||||
const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => {
|
||||
const inclusionProbability = economicDynamicsStats?.data?.active_set_inclusion_probability;
|
||||
switch (inclusionProbability) {
|
||||
case 'High':
|
||||
case 'Moderate':
|
||||
case 'Low':
|
||||
return inclusionProbability;
|
||||
case 'VeryHigh':
|
||||
return 'Very High';
|
||||
case 'VeryLow':
|
||||
return 'Very Low';
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
||||
const { economicDynamicsStats, mixNode } = useMixnodeContext();
|
||||
|
||||
const estimatedNodeRewards =
|
||||
currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-';
|
||||
const estimatedOperatorRewards =
|
||||
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
|
||||
const stakeSaturation = economicDynamicsStats?.data?.stake_saturation || '-';
|
||||
const profitMargin = mixNode?.data?.mix_node.profit_margin_percent || '-';
|
||||
const avgUptime = economicDynamicsStats?.data?.current_interval_uptime;
|
||||
|
||||
return {
|
||||
id: 1,
|
||||
estimatedTotalReward: {
|
||||
value: estimatedNodeRewards,
|
||||
},
|
||||
estimatedOperatorReward: {
|
||||
value: estimatedOperatorRewards,
|
||||
},
|
||||
selectionChance: {
|
||||
value: selectionChance(economicDynamicsStats),
|
||||
},
|
||||
stakeSaturation: {
|
||||
progressBarValue: typeof stakeSaturation === 'number' ? stakeSaturation * 100 : 0,
|
||||
value: typeof stakeSaturation === 'number' ? `${(stakeSaturation * 100).toFixed(2)} %` : '-',
|
||||
},
|
||||
profitMargin: {
|
||||
value: profitMargin ? `${profitMargin} %` : '-',
|
||||
},
|
||||
avgUptime: {
|
||||
value: avgUptime ? `${avgUptime} %` : '-',
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import { styled, useTheme, Theme } from '@mui/material/styles';
|
||||
import Tooltip, { tooltipClasses, TooltipProps } from '@mui/material/Tooltip';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types';
|
||||
import { EconomicsProgress } from './EconomicsProgress';
|
||||
import { cellStyles } from '../../Universal-DataGrid';
|
||||
import { UniversalTableProps } from '../../DetailTable';
|
||||
|
||||
const tooltipBackGroundColor = '#A0AED1';
|
||||
const threshold = 100;
|
||||
|
||||
const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => {
|
||||
const progressBarValue = value?.progressBarValue || 0;
|
||||
const fieldValue = value.value;
|
||||
|
||||
if (progressBarValue > 100) {
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
if (field === 'selectionChance') {
|
||||
switch (fieldValue) {
|
||||
case 'High':
|
||||
case 'Very High':
|
||||
return theme.palette.nym.networkExplorer.selectionChance.overModerate;
|
||||
case 'Moderate':
|
||||
return theme.palette.nym.networkExplorer.selectionChance.moderate;
|
||||
case 'Low':
|
||||
case 'Very Low':
|
||||
return theme.palette.nym.networkExplorer.selectionChance.underModerate;
|
||||
default:
|
||||
return theme.palette.nym.wallet.fee;
|
||||
}
|
||||
}
|
||||
return theme.palette.nym.wallet.fee;
|
||||
};
|
||||
|
||||
const formatCellValues = (value: EconomicsRowsType, field: string, theme: Theme) => {
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
if (value.progressBarValue) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }} id="field">
|
||||
<Typography
|
||||
sx={{
|
||||
mr: isTablet ? 0 : 1,
|
||||
mb: isTablet ? 1 : 0,
|
||||
fontWeight: '600',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
id={field}
|
||||
>
|
||||
{value.value}
|
||||
</Typography>
|
||||
<EconomicsProgress threshold={threshold} value={value.progressBarValue} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
|
||||
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
|
||||
{value.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const DelegatorsInfoTable: React.FC<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({
|
||||
tableName,
|
||||
columnsData,
|
||||
rows,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
))({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
maxWidth: 230,
|
||||
background: tooltipBackGroundColor,
|
||||
color: theme.palette.nym.networkExplorer.nav.hover,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{tooltipInfo && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CustomTooltip
|
||||
title={tooltipInfo}
|
||||
id={field}
|
||||
placement="top-start"
|
||||
sx={{
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: '#A0AED1',
|
||||
},
|
||||
}}
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
padding: 0,
|
||||
py: 1,
|
||||
pr: 1,
|
||||
}}
|
||||
disableFocusRipple
|
||||
disableRipple
|
||||
>
|
||||
<InfoOutlinedIcon
|
||||
sx={{
|
||||
height: '18px',
|
||||
width: '18px',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
</Box>
|
||||
)}
|
||||
{title}
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows?.map((eachRow) => (
|
||||
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{columnsData?.map((_, index: number) => {
|
||||
const { field } = columnsData[index];
|
||||
const value: EconomicsRowsType = (eachRow as any)[field];
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={_.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
...cellStyles,
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: textColour(value, field, theme),
|
||||
}}
|
||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(value, columnsData[index].field, theme)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { DelegatorsInfoTable } from './Table';
|
||||
export { EconomicsInfoColumns } from './Columns';
|
||||
export { EconomicsInfoRows } from './Rows';
|
||||
@@ -0,0 +1,15 @@
|
||||
export type EconomicsRowsType = {
|
||||
progressBarValue?: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface EconomicsInfoRow {
|
||||
estimatedTotalReward: EconomicsRowsType;
|
||||
estimatedOperatorReward: EconomicsRowsType;
|
||||
selectionChance: EconomicsRowsType;
|
||||
stakeSaturation: EconomicsRowsType;
|
||||
profitMargin: EconomicsRowsType;
|
||||
avgUptime: EconomicsRowsType;
|
||||
}
|
||||
|
||||
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
|
||||
@@ -11,6 +11,8 @@ export type MixnodeRowType = {
|
||||
self_percentage: string;
|
||||
host: string;
|
||||
layer: string;
|
||||
profit_percentage: string;
|
||||
avg_uptime: string;
|
||||
};
|
||||
|
||||
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
|
||||
@@ -22,6 +24,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
|
||||
const delegations = Number(item.total_delegation.amount) || 0;
|
||||
const totalBond = pledge + delegations;
|
||||
const selfPercentage = ((pledge * 100) / totalBond).toFixed(2);
|
||||
const profitPercentage = item.mix_node.profit_margin_percent || 0;
|
||||
return {
|
||||
id: item.owner,
|
||||
status: item.status,
|
||||
@@ -32,5 +35,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
|
||||
self_percentage: selfPercentage,
|
||||
host: item?.mix_node?.host || '',
|
||||
layer: item?.layer || '',
|
||||
profit_percentage: `${profitPercentage}%`,
|
||||
avg_uptime: `${item.avg_uptime}%` || '-',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,9 +22,12 @@ export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
|
||||
<IconButton component="a" href={TELEGRAM_LINK} target="_blank" data-testid="telegram">
|
||||
<TelegramIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
|
||||
<DiscordIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
{false && (
|
||||
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
|
||||
<DiscordIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<IconButton component="a" href={TWITTER_LINK} target="_blank" data-testid="twitter">
|
||||
<TwitterIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export type RowsType = {
|
||||
value?: string | number;
|
||||
visualProgressValue?: number;
|
||||
};
|
||||
|
||||
export interface DelegatorsInfoRow {
|
||||
estimated_total_reward: RowsType;
|
||||
estimated_operator_reward: RowsType;
|
||||
active_set_probability: RowsType;
|
||||
stake_saturation: RowsType;
|
||||
profit_margin: RowsType;
|
||||
avg_uptime: RowsType;
|
||||
}
|
||||
|
||||
export type DelegatorsInfoRowWithIndex = DelegatorsInfoRow & { id: number };
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ApiState,
|
||||
DelegationsResponse,
|
||||
MixNodeDescriptionResponse,
|
||||
MixNodeEconomicDynamicsStatsResponse,
|
||||
MixNodeResponseItem,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
@@ -18,6 +19,7 @@ import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../componen
|
||||
interface MixnodeState {
|
||||
delegations?: ApiState<DelegationsResponse>;
|
||||
description?: ApiState<MixNodeDescriptionResponse>;
|
||||
economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse>;
|
||||
mixNode?: ApiState<MixNodeResponseItem | undefined>;
|
||||
mixNodeRow?: MixnodeRowType;
|
||||
stats?: ApiState<StatsResponse>;
|
||||
@@ -71,6 +73,13 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
||||
'Failed to fetch mixnode description',
|
||||
);
|
||||
|
||||
const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] =
|
||||
useApiState<MixNodeEconomicDynamicsStatsResponse>(
|
||||
mixNodeIdentityKey,
|
||||
Api.fetchMixnodeEconomicDynamicsStatsById,
|
||||
'Failed to fetch mixnode dynamics stats by id',
|
||||
);
|
||||
|
||||
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>(
|
||||
mixNodeIdentityKey,
|
||||
Api.fetchUptimeStoryById,
|
||||
@@ -84,6 +93,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
||||
clearStatus();
|
||||
clearStats();
|
||||
clearDescription();
|
||||
clearEconomicDynamicsStats();
|
||||
clearUptimeHistory();
|
||||
|
||||
// fetch the mixnode, then get all the other stuff
|
||||
@@ -93,7 +103,14 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
||||
return;
|
||||
}
|
||||
setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data));
|
||||
Promise.all([fetchDelegations(), fetchStatus(), fetchStats(), fetchDescription(), fetchUptimeHistory()]);
|
||||
Promise.all([
|
||||
fetchDelegations(),
|
||||
fetchStatus(),
|
||||
fetchStats(),
|
||||
fetchDescription(),
|
||||
fetchEconomicDynamicsStats(),
|
||||
fetchUptimeHistory(),
|
||||
]);
|
||||
});
|
||||
}, [mixNodeIdentityKey]);
|
||||
|
||||
@@ -103,6 +120,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
||||
mixNode,
|
||||
mixNodeRow,
|
||||
description,
|
||||
economicDynamicsStats,
|
||||
stats,
|
||||
status,
|
||||
uptimeStory,
|
||||
@@ -113,6 +131,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
||||
mixNode,
|
||||
mixNodeRow,
|
||||
description,
|
||||
economicDynamicsStats,
|
||||
stats,
|
||||
status,
|
||||
uptimeStory,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Alert, AlertTitle, Box, CircularProgress, Grid, Typography } from '@mui
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ColumnsType, DetailTable } from '../../components/DetailTable';
|
||||
import { BondBreakdownTable } from '../../components/MixNodes/BondBreakdown';
|
||||
import { DelegatorsInfoTable, EconomicsInfoColumns, EconomicsInfoRows } from '../../components/MixNodes/Economics';
|
||||
import { ComponentError } from '../../components/ComponentError';
|
||||
import { ContentCard } from '../../components/ContentCard';
|
||||
import { TwoColSmallTable } from '../../components/TwoColSmallTable';
|
||||
@@ -82,6 +83,16 @@ const PageMixnodeDetailWithState: React.FC = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12}>
|
||||
<DelegatorsInfoTable
|
||||
columnsData={EconomicsInfoColumns}
|
||||
tableName="Delegators info table"
|
||||
rows={[EconomicsInfoRows()]}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12}>
|
||||
<ContentCard title="Bond Breakdown">
|
||||
|
||||
@@ -16,6 +16,7 @@ import { CustomColumnHeading } from '../../components/CustomColumnHeading';
|
||||
import { Title } from '../../components/Title';
|
||||
import { cellStyles, UniversalDataGrid } from '../../components/Universal-DataGrid';
|
||||
import { currencyToString } from '../../utils/currency';
|
||||
import { splice } from '../../utils';
|
||||
import { getMixNodeStatusColor } from '../../components/MixNodes/Status';
|
||||
import { MixNodeStatusDropdown } from '../../components/MixNodes/StatusDropdown';
|
||||
|
||||
@@ -85,7 +86,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
field: 'owner',
|
||||
headerName: 'Owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 380,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
@@ -95,7 +96,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
sx={getCellStyles(theme, params.row)}
|
||||
data-testid="big-dipper-link"
|
||||
>
|
||||
{params.value}
|
||||
{splice(7, 29, params.value)}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
@@ -104,7 +105,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
headerName: 'Identity Key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 380,
|
||||
width: 180,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<>
|
||||
@@ -119,7 +120,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
to={`/network-components/mixnode/${params.value}`}
|
||||
data-testid="identity-link"
|
||||
>
|
||||
{params.value}
|
||||
{splice(7, 29, params.value)}
|
||||
</MuiLink>
|
||||
</>
|
||||
),
|
||||
@@ -130,7 +131,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
type: 'number',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 150,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
@@ -213,6 +214,40 @@ export const PageMixnodes: React.FC = () => {
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'profit_percentage',
|
||||
headerName: 'Profit Margin',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Profit Margin" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 140,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'avg_uptime',
|
||||
headerName: 'Average Uptime',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Average Uptime" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 160,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Introduction" />
|
||||
|
||||
# Nym Network Explorer Storybook
|
||||
|
||||
This is the Storybook for the Nym Network Explorer.
|
||||
@@ -83,6 +83,7 @@ export interface MixNodeResponseItem {
|
||||
two_letter_iso_country_code: string;
|
||||
};
|
||||
mix_node: MixNode;
|
||||
avg_uptime: number;
|
||||
}
|
||||
|
||||
export type MixNodeResponse = MixNodeResponseItem[];
|
||||
@@ -203,3 +204,13 @@ export type UptimeStoryResponse = {
|
||||
identity: string;
|
||||
owner: string;
|
||||
};
|
||||
|
||||
export type MixNodeEconomicDynamicsStatsResponse = {
|
||||
stake_saturation: number;
|
||||
active_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow';
|
||||
reserve_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow';
|
||||
estimated_total_node_reward: number;
|
||||
estimated_operator_reward: number;
|
||||
estimated_delegators_reward: number;
|
||||
current_interval_uptime: number;
|
||||
};
|
||||
|
||||
@@ -36,3 +36,12 @@ export function countryDataToGridRow(countriesData: CountryData[]): CountryDataR
|
||||
const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1));
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export const splice = (start: number, deleteCount: number, address?: string): string => {
|
||||
if (address) {
|
||||
const array = address.split('');
|
||||
array.splice(start, deleteCount, '...');
|
||||
return array.join('');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-gateway"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Mixnet Gateway"
|
||||
edition = "2021"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::{crate_version, Parser};
|
||||
use network_defaults::DEFAULT_NETWORK;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
mod commands;
|
||||
@@ -63,6 +64,7 @@ fn long_version() -> String {
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
"#,
|
||||
"Build Timestamp:",
|
||||
env!("VERGEN_BUILD_TIMESTAMP"),
|
||||
@@ -80,6 +82,8 @@ fn long_version() -> String {
|
||||
env!("VERGEN_RUSTC_CHANNEL"),
|
||||
"cargo Profile:",
|
||||
env!("VERGEN_CARGO_PROFILE"),
|
||||
"Network:",
|
||||
DEFAULT_NETWORK
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-mixnode"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
authors = [
|
||||
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
|
||||
"Jędrzej Stuczyński <andrew@nymtech.net>",
|
||||
|
||||
@@ -4,6 +4,7 @@ extern crate rocket;
|
||||
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use ::config::defaults::DEFAULT_NETWORK;
|
||||
use clap::{crate_version, Parser};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
@@ -65,6 +66,7 @@ fn long_version() -> String {
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
{:<20}{}
|
||||
"#,
|
||||
"Build Timestamp:",
|
||||
env!("VERGEN_BUILD_TIMESTAMP"),
|
||||
@@ -82,6 +84,8 @@ fn long_version() -> String {
|
||||
env!("VERGEN_RUSTC_CHANNEL"),
|
||||
"cargo Profile:",
|
||||
env!("VERGEN_CARGO_PROFILE"),
|
||||
"Network:",
|
||||
DEFAULT_NETWORK
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Generated
+97
-7
@@ -75,9 +75,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.55"
|
||||
version = "1.0.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
|
||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
@@ -87,7 +87,18 @@ checksum = "25df3c03f1040d0069fcd3907e24e36d59f9b6fa07ba49be0eb25a794f036ba7"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"password-hash",
|
||||
"password-hash 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"password-hash 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -619,6 +630,45 @@ dependencies = [
|
||||
"generic-array 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"strsim 0.10.0",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
|
||||
dependencies = [
|
||||
"heck 0.4.0",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
version = "0.0.3"
|
||||
@@ -694,6 +744,7 @@ dependencies = [
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
@@ -2946,10 +2997,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym_wallet"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
"argon2 0.3.4",
|
||||
"base64",
|
||||
"bip39",
|
||||
"cfg-if",
|
||||
@@ -2965,6 +3016,7 @@ dependencies = [
|
||||
"itertools",
|
||||
"log",
|
||||
"mixnet-contract-common",
|
||||
"once_cell",
|
||||
"pretty_env_logger",
|
||||
"rand 0.6.5",
|
||||
"reqwest",
|
||||
@@ -3106,6 +3158,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.20.0"
|
||||
@@ -3182,6 +3240,17 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.3",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.6"
|
||||
@@ -4260,9 +4329,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.79"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||
dependencies = [
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
@@ -5049,6 +5118,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "thin-slice"
|
||||
version = "0.1.1"
|
||||
@@ -5498,6 +5573,21 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wallet-recovery-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"argon2 0.4.0",
|
||||
"base64",
|
||||
"bip39",
|
||||
"clap",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
[workspace]
|
||||
members = ["src-tauri"]
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"src-tauri",
|
||||
"wallet-recovery-cli",
|
||||
]
|
||||
|
||||
@@ -42,7 +42,9 @@
|
||||
"react-hook-form": "^7.14.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"semver": "^6.3.0",
|
||||
"string-to-color": "^2.2.2",
|
||||
"use-clipboard-copy": "^0.2.0",
|
||||
"uuid": "^8.3.2",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -71,6 +73,7 @@
|
||||
"@types/react-router": "^5.1.18",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/semver": "^7.3.8",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym_wallet"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
description = "Nym Native Wallet"
|
||||
authors = ["Nym Technologies SA"]
|
||||
license = ""
|
||||
@@ -28,6 +28,7 @@ eyre = "0.6.5"
|
||||
futures = "0.3.15"
|
||||
itertools = "0.10"
|
||||
log = "0.4"
|
||||
once_cell = "1.7.2"
|
||||
pretty_env_logger = "0.4"
|
||||
rand = "0.6.5"
|
||||
reqwest = "0.11.9"
|
||||
|
||||
@@ -57,7 +57,7 @@ pub struct NetworkConfig {
|
||||
|
||||
// Additional user provided validators.
|
||||
// It is an option for the purpuse of file serialization.
|
||||
validator_urls: Option<Vec<ValidatorUrl>>,
|
||||
validator_urls: Option<Vec<ValidatorConfigEntry>>,
|
||||
}
|
||||
|
||||
impl Default for Base {
|
||||
@@ -89,7 +89,7 @@ impl Default for NetworkConfig {
|
||||
}
|
||||
|
||||
impl NetworkConfig {
|
||||
fn validators(&self) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
fn validators(&self) -> impl Iterator<Item = &ValidatorConfigEntry> {
|
||||
self.validator_urls.iter().flat_map(|v| v.iter())
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@ impl Config {
|
||||
pub fn get_base_validators(
|
||||
&self,
|
||||
network: WalletNetwork,
|
||||
) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
) -> impl Iterator<Item = ValidatorConfigEntry> + '_ {
|
||||
self.base.networks.validators(network.into()).map(|v| {
|
||||
v.clone()
|
||||
.try_into()
|
||||
@@ -203,7 +203,7 @@ impl Config {
|
||||
pub fn get_configured_validators(
|
||||
&self,
|
||||
network: WalletNetwork,
|
||||
) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
) -> impl Iterator<Item = ValidatorConfigEntry> + '_ {
|
||||
self
|
||||
.networks
|
||||
.get(&network.as_key())
|
||||
@@ -272,7 +272,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_validator_nymd_url(&self, network: &WalletNetwork) -> Option<Url> {
|
||||
pub fn get_selected_validator_nymd_url(&self, network: WalletNetwork) -> Option<Url> {
|
||||
self
|
||||
.networks
|
||||
.get(&network.as_key())
|
||||
@@ -286,7 +286,7 @@ impl Config {
|
||||
.and_then(|config| config.selected_api_url.clone())
|
||||
}
|
||||
|
||||
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
|
||||
pub fn add_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) {
|
||||
if let Some(network_config) = self.networks.get_mut(&network.as_key()) {
|
||||
if let Some(ref mut urls) = network_config.validator_urls {
|
||||
urls.push(url);
|
||||
@@ -304,7 +304,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
|
||||
pub fn remove_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) {
|
||||
if let Some(network_config) = self.networks.get_mut(&network.as_key()) {
|
||||
if let Some(ref mut urls) = network_config.validator_urls {
|
||||
// Removes duplicates too if there are any
|
||||
@@ -325,17 +325,19 @@ where
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct ValidatorUrl {
|
||||
pub struct ValidatorConfigEntry {
|
||||
pub nymd_url: Url,
|
||||
pub nymd_name: Option<String>,
|
||||
pub api_url: Option<Url>,
|
||||
}
|
||||
|
||||
impl TryFrom<ValidatorDetails> for ValidatorUrl {
|
||||
impl TryFrom<ValidatorDetails> for ValidatorConfigEntry {
|
||||
type Error = BackendError;
|
||||
|
||||
fn try_from(validator: ValidatorDetails) -> Result<Self, Self::Error> {
|
||||
Ok(ValidatorUrl {
|
||||
Ok(ValidatorConfigEntry {
|
||||
nymd_url: validator.nymd_url.parse()?,
|
||||
nymd_name: None,
|
||||
api_url: match &validator.api_url {
|
||||
Some(url) => Some(url.parse()?),
|
||||
None => None,
|
||||
@@ -344,12 +346,13 @@ impl TryFrom<ValidatorDetails> for ValidatorUrl {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<network_config::Validator> for ValidatorUrl {
|
||||
impl TryFrom<network_config::Validator> for ValidatorConfigEntry {
|
||||
type Error = BackendError;
|
||||
|
||||
fn try_from(validator: network_config::Validator) -> Result<Self, Self::Error> {
|
||||
Ok(ValidatorUrl {
|
||||
Ok(ValidatorConfigEntry {
|
||||
nymd_url: validator.nymd_url.parse()?,
|
||||
nymd_name: validator.nymd_name,
|
||||
api_url: match &validator.api_url {
|
||||
Some(url) => Some(url.parse()?),
|
||||
None => None,
|
||||
@@ -358,14 +361,21 @@ impl TryFrom<network_config::Validator> for ValidatorUrl {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidatorUrl {
|
||||
impl fmt::Display for ValidatorConfigEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s1 = format!("nymd_url: {}", self.nymd_url);
|
||||
let name = self.nymd_name.as_ref().map(|name| format!(" ({})", name));
|
||||
let s2 = self
|
||||
.api_url
|
||||
.as_ref()
|
||||
.map(|url| format!(", api_url: {}", url));
|
||||
write!(f, " {}{},", s1, s2.unwrap_or_default())
|
||||
write!(
|
||||
f,
|
||||
" {}{}{},",
|
||||
s1,
|
||||
name.unwrap_or_default(),
|
||||
s2.unwrap_or_default()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,13 +384,13 @@ impl fmt::Display for ValidatorUrl {
|
||||
pub struct OptionalValidators {
|
||||
// User supplied additional validator urls in addition to the hardcoded ones.
|
||||
// These are separate fields, rather than a map, to force the serialization order.
|
||||
mainnet: Option<Vec<ValidatorUrl>>,
|
||||
sandbox: Option<Vec<ValidatorUrl>>,
|
||||
qa: Option<Vec<ValidatorUrl>>,
|
||||
mainnet: Option<Vec<ValidatorConfigEntry>>,
|
||||
sandbox: Option<Vec<ValidatorConfigEntry>>,
|
||||
qa: Option<Vec<ValidatorConfigEntry>>,
|
||||
}
|
||||
|
||||
impl OptionalValidators {
|
||||
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorConfigEntry> {
|
||||
match network {
|
||||
WalletNetwork::MAINNET => self.mainnet.as_ref(),
|
||||
WalletNetwork::SANDBOX => self.sandbox.as_ref(),
|
||||
@@ -422,16 +432,19 @@ mod tests {
|
||||
selected_api_url: Some("https://my_api_url.com".parse().unwrap()),
|
||||
|
||||
validator_urls: Some(vec![
|
||||
ValidatorUrl {
|
||||
ValidatorConfigEntry {
|
||||
nymd_url: "https://foo".parse().unwrap(),
|
||||
nymd_name: Some("FooName".to_string()),
|
||||
api_url: None,
|
||||
},
|
||||
ValidatorUrl {
|
||||
ValidatorConfigEntry {
|
||||
nymd_url: "https://bar".parse().unwrap(),
|
||||
nymd_name: None,
|
||||
api_url: Some("https://bar/api".parse().unwrap()),
|
||||
},
|
||||
ValidatorUrl {
|
||||
ValidatorConfigEntry {
|
||||
nymd_url: "https://baz".parse().unwrap(),
|
||||
nymd_name: None,
|
||||
api_url: Some("https://baz/api".parse().unwrap()),
|
||||
},
|
||||
]),
|
||||
@@ -458,6 +471,7 @@ selected_api_url = 'https://my_api_url.com/'
|
||||
|
||||
[[validator_urls]]
|
||||
nymd_url = 'https://foo/'
|
||||
nymd_name = 'FooName'
|
||||
|
||||
[[validator_urls]]
|
||||
nymd_url = 'https://bar/'
|
||||
@@ -469,6 +483,39 @@ api_url = 'https://baz/api'
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_to_json() {
|
||||
let config = test_config();
|
||||
let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()];
|
||||
println!("{}", serde_json::to_string_pretty(netconfig).unwrap());
|
||||
assert_eq!(
|
||||
serde_json::to_string_pretty(netconfig).unwrap(),
|
||||
r#"{
|
||||
"version": 1,
|
||||
"selected_nymd_url": null,
|
||||
"selected_api_url": "https://my_api_url.com/",
|
||||
"validator_urls": [
|
||||
{
|
||||
"nymd_url": "https://foo/",
|
||||
"nymd_name": "FooName",
|
||||
"api_url": null
|
||||
},
|
||||
{
|
||||
"nymd_url": "https://bar/",
|
||||
"nymd_name": null,
|
||||
"api_url": "https://bar/api"
|
||||
},
|
||||
{
|
||||
"nymd_url": "https://baz/",
|
||||
"nymd_name": null,
|
||||
"api_url": "https://baz/api"
|
||||
}
|
||||
]
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_and_deserialize_to_toml() {
|
||||
let config = test_config();
|
||||
@@ -513,6 +560,6 @@ api_url = 'https://baz/api'
|
||||
.next()
|
||||
.and_then(|v| v.api_url)
|
||||
.unwrap();
|
||||
assert_eq!(api_url.as_ref(), "https://api.nyx.nodes.guru/",);
|
||||
assert_eq!(api_url.as_ref(), "https://validator.nymtech.net/api/",);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,12 +83,22 @@ pub enum BackendError {
|
||||
WalletFileAlreadyExists,
|
||||
#[error("The wallet file is not found")]
|
||||
WalletFileNotFound,
|
||||
#[error("Account ID not found in wallet")]
|
||||
NoSuchIdInWallet,
|
||||
#[error("Account ID already found in wallet")]
|
||||
IdAlreadyExistsInWallet,
|
||||
#[error("Login ID not found in wallet")]
|
||||
WalletNoSuchLoginId,
|
||||
#[error("Account ID not found in wallet login")]
|
||||
WalletNoSuchAccountIdInWalletLogin,
|
||||
#[error("Login ID already found in wallet")]
|
||||
WalletLoginIdAlreadyExists,
|
||||
#[error("Account ID already found in wallet login")]
|
||||
WalletAccountIdAlreadyExistsInWalletLogin,
|
||||
#[error("Adding a different password to the wallet not currently supported")]
|
||||
WalletDifferentPasswordDetected,
|
||||
#[error("Unexpted mnemonic account for login")]
|
||||
WalletUnexpectedMnemonicAccount,
|
||||
#[error("Unexpted multiple account entry for login")]
|
||||
WalletUnexpectedMultipleAccounts,
|
||||
#[error("Failed to derive address from mnemonic")]
|
||||
FailedToDeriveAddress,
|
||||
}
|
||||
|
||||
impl Serialize for BackendError {
|
||||
|
||||
@@ -35,15 +35,20 @@ fn main() {
|
||||
tauri::Builder::default()
|
||||
.manage(Arc::new(RwLock::new(State::default())))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
mixnet::account::add_account_for_password,
|
||||
mixnet::account::connect_with_mnemonic,
|
||||
mixnet::account::create_new_account,
|
||||
mixnet::account::create_new_mnemonic,
|
||||
mixnet::account::create_password,
|
||||
mixnet::account::does_password_file_exist,
|
||||
mixnet::account::get_balance,
|
||||
mixnet::account::list_accounts,
|
||||
mixnet::account::logout,
|
||||
mixnet::account::remove_account_for_password,
|
||||
mixnet::account::remove_password,
|
||||
mixnet::account::show_mnemonic_for_account_in_password,
|
||||
mixnet::account::sign_in_with_password,
|
||||
mixnet::account::sign_in_with_password_and_account_id,
|
||||
mixnet::account::switch_network,
|
||||
mixnet::account::validate_mnemonic,
|
||||
mixnet::admin::get_contract_settings,
|
||||
|
||||
@@ -9,18 +9,30 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
// When the UI queries validator urls we use this type
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ValidatorUrls {
|
||||
pub urls: Vec<String>,
|
||||
pub urls: Vec<ValidatorUrl>,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurl.ts"))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ValidatorUrl {
|
||||
pub url: String,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
// The type used when adding or removing validators, effectively the input.
|
||||
// NOTE: we should consider if we want to split this up
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Validator {
|
||||
pub nymd_url: String,
|
||||
pub nymd_name: Option<String>,
|
||||
pub api_url: Option<String>,
|
||||
}
|
||||
|
||||
@@ -42,10 +54,7 @@ pub async fn get_validator_nymd_urls(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<ValidatorUrls, BackendError> {
|
||||
let state = state.read().await;
|
||||
let urls: Vec<String> = state
|
||||
.get_nymd_urls(network)
|
||||
.map(|url| url.to_string())
|
||||
.collect();
|
||||
let urls: Vec<ValidatorUrl> = state.get_nymd_urls(network).collect();
|
||||
Ok(ValidatorUrls { urls })
|
||||
}
|
||||
|
||||
@@ -55,10 +64,7 @@ pub async fn get_validator_api_urls(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<ValidatorUrls, BackendError> {
|
||||
let state = state.read().await;
|
||||
let urls: Vec<String> = state
|
||||
.get_api_urls(network)
|
||||
.map(|url| url.to_string())
|
||||
.collect();
|
||||
let urls: Vec<ValidatorUrl> = state.get_api_urls(network).collect();
|
||||
Ok(ValidatorUrls { urls })
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::error::BackendError;
|
||||
use crate::network::Network as WalletNetwork;
|
||||
use crate::network_config;
|
||||
use crate::nymd_client;
|
||||
use crate::state::State;
|
||||
use crate::wallet_storage::{self, DEFAULT_WALLET_ACCOUNT_ID};
|
||||
use crate::state::{State, WalletAccountIds};
|
||||
use crate::wallet_storage::{self, DEFAULT_LOGIN_ID};
|
||||
|
||||
use bip39::{Language, Mnemonic};
|
||||
use config::defaults::all::Network;
|
||||
@@ -21,6 +21,7 @@ use std::sync::Arc;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
use validator_client::nymd::wallet::{AccountData, DirectSecp256k1HdWallet};
|
||||
|
||||
use validator_client::{nymd::SigningNymdClient, Client};
|
||||
|
||||
@@ -51,6 +52,14 @@ pub struct CreatedAccount {
|
||||
mnemonic: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/createdaccount.ts"))]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AccountEntry {
|
||||
id: String,
|
||||
address: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/balance.ts"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -107,9 +116,8 @@ pub async fn create_new_account(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_new_mnemonic() -> Result<String, BackendError> {
|
||||
let rand_mnemonic = random_mnemonic();
|
||||
Ok(rand_mnemonic.to_string())
|
||||
pub fn create_new_mnemonic() -> String {
|
||||
random_mnemonic().to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -169,7 +177,7 @@ async fn _connect_with_mnemonic(
|
||||
for network in WalletNetwork::iter() {
|
||||
log::debug!(
|
||||
"List of validators for {network}: [\n{}\n]",
|
||||
state.get_validators(network).format(",\n")
|
||||
state.get_config_validator_entries(network).format(",\n")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,6 +229,10 @@ async fn _connect_with_mnemonic(
|
||||
};
|
||||
|
||||
// Register all the clients
|
||||
{
|
||||
let mut w_state = state.write().await;
|
||||
w_state.logout();
|
||||
}
|
||||
for client in clients {
|
||||
let network: WalletNetwork = client.network.into();
|
||||
let mut w_state = state.write().await;
|
||||
@@ -268,7 +280,7 @@ fn create_clients(
|
||||
) -> Result<Vec<Client<SigningNymdClient>>, BackendError> {
|
||||
let mut clients = Vec::new();
|
||||
for network in WalletNetwork::iter() {
|
||||
let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(&network) {
|
||||
let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(network) {
|
||||
log::debug!("Using selected nymd_url for {network}: {url}");
|
||||
url.clone()
|
||||
} else {
|
||||
@@ -353,18 +365,18 @@ pub fn does_password_file_exist() -> Result<bool, BackendError> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> {
|
||||
pub fn create_password(mnemonic: &str, password: String) -> Result<(), BackendError> {
|
||||
if does_password_file_exist()? {
|
||||
return Err(BackendError::WalletFileAlreadyExists);
|
||||
}
|
||||
log::info!("Creating password");
|
||||
|
||||
let mnemonic = Mnemonic::from_str(&mnemonic)?;
|
||||
let mnemonic = Mnemonic::from_str(mnemonic)?;
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
// Currently we only support a single, default, login id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password)
|
||||
wallet_storage::store_login_with_multiple_accounts(mnemonic, hd_path, login_id, &password)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -375,15 +387,301 @@ pub async fn sign_in_with_password(
|
||||
log::info!("Signing in with password");
|
||||
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
let stored_account = wallet_storage::load_existing_wallet_login_information(&id, &password)?;
|
||||
_connect_with_mnemonic(stored_account.mnemonic().clone(), state).await
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
|
||||
let mnemonic = extract_first_mnemonic(&stored_login)?;
|
||||
let first_login_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?;
|
||||
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
}
|
||||
|
||||
fn extract_first_mnemonic(
|
||||
stored_login: &wallet_storage::StoredLogin,
|
||||
) -> Result<Mnemonic, BackendError> {
|
||||
let mnemonic = match stored_login {
|
||||
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => {
|
||||
// Login using the first account in the list
|
||||
accounts
|
||||
.get_accounts()
|
||||
.next()
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
|
||||
.mnemonic()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sign_in_with_password_and_account_id(
|
||||
account_id: &str,
|
||||
password: &str,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<Account, BackendError> {
|
||||
log::info!("Signing in with password");
|
||||
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
|
||||
let mnemonic = extract_mnemonic(&stored_login, &account_id)?;
|
||||
let first_login_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?;
|
||||
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
}
|
||||
|
||||
fn extract_mnemonic(
|
||||
stored_login: &wallet_storage::StoredLogin,
|
||||
account_id: &wallet_storage::AccountId,
|
||||
) -> Result<Mnemonic, BackendError> {
|
||||
let mnemonic = match stored_login {
|
||||
wallet_storage::StoredLogin::Mnemonic(_) => {
|
||||
return Err(BackendError::WalletNoSuchAccountIdInWalletLogin);
|
||||
}
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => accounts
|
||||
.get_account(account_id)
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
|
||||
.mnemonic()
|
||||
.clone(),
|
||||
};
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_password() -> Result<(), BackendError> {
|
||||
log::info!("Removing password");
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
wallet_storage::remove_wallet_login_information(&id)
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
wallet_storage::remove_login(&login_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_account_for_password(
|
||||
mnemonic: &str,
|
||||
password: &str,
|
||||
account_id: &str,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<AccountEntry, BackendError> {
|
||||
log::info!("Adding account for the current password: {account_id}");
|
||||
let mnemonic = Mnemonic::from_str(mnemonic)?;
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
// Currently we only support a single, default, login id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
|
||||
wallet_storage::append_account_to_login(
|
||||
mnemonic.clone(),
|
||||
hd_path,
|
||||
login_id.clone(),
|
||||
account_id.clone(),
|
||||
&password,
|
||||
)?;
|
||||
|
||||
let address = {
|
||||
let state = state.read().await;
|
||||
let network: Network = state.current_network().into();
|
||||
derive_address(mnemonic, network.bech32_prefix())?.to_string()
|
||||
};
|
||||
|
||||
// Re-read all the acccounts from the wallet to reset the state, rather than updating it
|
||||
// incrementally
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
// NOTE: since we are appending, this id shouldn't be needed, but setting the state is supposed
|
||||
// to be a general function
|
||||
let first_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_id_when_converting, state).await?;
|
||||
|
||||
Ok(AccountEntry {
|
||||
id: account_id.to_string(),
|
||||
address,
|
||||
})
|
||||
}
|
||||
|
||||
// The first `AccoundId` when converting is the `LoginId` for the entry that was loaded.
|
||||
async fn set_state_with_all_accounts(
|
||||
stored_login: wallet_storage::StoredLogin,
|
||||
first_id_when_converting: wallet_storage::AccountId,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<(), BackendError> {
|
||||
log::trace!("Set state with accounts:");
|
||||
let all_accounts: Vec<_> = stored_login
|
||||
.unwrap_into_multiple_accounts(first_id_when_converting)
|
||||
.into_accounts()
|
||||
.collect();
|
||||
|
||||
for account in &all_accounts {
|
||||
log::trace!("account: {:?}", account.id());
|
||||
}
|
||||
|
||||
let all_account_ids: Vec<WalletAccountIds> = all_accounts
|
||||
.iter()
|
||||
.map(|account| {
|
||||
let mnemonic = account.mnemonic();
|
||||
let addresses: HashMap<WalletNetwork, cosmrs::AccountId> = WalletNetwork::iter()
|
||||
.map(|network| {
|
||||
let config_network: Network = network.into();
|
||||
(
|
||||
network,
|
||||
derive_address(mnemonic.clone(), config_network.bech32_prefix()).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
WalletAccountIds {
|
||||
id: account.id().clone(),
|
||||
addresses,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut w_state = state.write().await;
|
||||
w_state.set_all_accounts(all_account_ids);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_account_for_password(
|
||||
password: &str,
|
||||
account_id: &str,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<(), BackendError> {
|
||||
log::info!("Removing account: {account_id}");
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
wallet_storage::remove_account_from_login(&login_id, &account_id, &password)?;
|
||||
|
||||
// Load to reset the internal state
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
// NOTE: Since we removed from a multi-account login, this id shouldn't be needed, but setting
|
||||
// the state is supposed to be a general function
|
||||
let first_account_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_account_id_when_converting, state).await
|
||||
}
|
||||
|
||||
fn derive_address(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
prefix: &str,
|
||||
) -> Result<cosmrs::AccountId, BackendError> {
|
||||
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)?
|
||||
.try_derive_accounts()?
|
||||
.first()
|
||||
.map(AccountData::address)
|
||||
.cloned()
|
||||
.ok_or(BackendError::FailedToDeriveAddress)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_accounts(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<Vec<AccountEntry>, BackendError> {
|
||||
log::trace!("Listing accounts");
|
||||
let state = state.read().await;
|
||||
let network = state.current_network();
|
||||
|
||||
let all_accounts = state
|
||||
.get_all_accounts()
|
||||
.map(|account| AccountEntry {
|
||||
id: account.id.to_string(),
|
||||
address: account.addresses[&network].to_string(),
|
||||
})
|
||||
.map(|account| {
|
||||
log::trace!("{:?}", account);
|
||||
account
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(all_accounts)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn show_mnemonic_for_account_in_password(
|
||||
account_id: String,
|
||||
password: String,
|
||||
) -> Result<String, BackendError> {
|
||||
log::info!("Getting mnemonic for: {account_id}");
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id);
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
let mnemonic = _show_mnemonic_for_account_in_password(&login_id, &account_id, &password)?;
|
||||
Ok(mnemonic.to_string())
|
||||
}
|
||||
|
||||
fn _show_mnemonic_for_account_in_password(
|
||||
login_id: &wallet_storage::LoginId,
|
||||
account_id: &wallet_storage::AccountId,
|
||||
password: &wallet_storage::UserPassword,
|
||||
) -> Result<bip39::Mnemonic, BackendError> {
|
||||
let stored_account = wallet_storage::load_existing_login(login_id, password)?;
|
||||
let mnemonic = match stored_account {
|
||||
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => {
|
||||
for account in accounts.get_accounts() {
|
||||
log::debug!("{:?}", account);
|
||||
}
|
||||
accounts
|
||||
.get_account(account_id)
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
|
||||
.mnemonic()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::wallet_storage::{
|
||||
self,
|
||||
account_data::{MnemonicAccount, WalletAccount},
|
||||
};
|
||||
|
||||
// This decryptes a stored wallet file using the same procedure as when signing in. Most tests
|
||||
// related to the encryped wallet storage is in `wallet_storage`.
|
||||
#[test]
|
||||
fn decrypt_stored_wallet_for_sign_in() {
|
||||
const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json";
|
||||
let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET);
|
||||
let login_id = wallet_storage::LoginId::new("first".to_string());
|
||||
let account_id = wallet_storage::AccountId::new("first".to_string());
|
||||
let password = wallet_storage::UserPassword::new("password".to_string());
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
|
||||
let stored_login =
|
||||
wallet_storage::load_existing_login_at_file(&wallet_file, &login_id, &password).unwrap();
|
||||
let mnemonic = extract_first_mnemonic(&stored_login).unwrap();
|
||||
|
||||
let expected_mnemonic = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap();
|
||||
assert_eq!(mnemonic, expected_mnemonic);
|
||||
|
||||
let all_accounts: Vec<_> = stored_login
|
||||
.unwrap_into_multiple_accounts(account_id.clone())
|
||||
.into_accounts()
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
all_accounts,
|
||||
vec![WalletAccount::new(
|
||||
account_id,
|
||||
MnemonicAccount::new(expected_mnemonic, hd_path),
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_stored_wallet_multiple_for_sign_in() {
|
||||
// WIP(JON): same as above but with file containing multiple accounts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ use crate::state::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tendermint_rpc::endpoint::broadcast::tx_commit::Response;
|
||||
use tokio::sync::RwLock;
|
||||
use validator_client::nymd::{AccountId, CosmosCoin};
|
||||
use validator_client::nymd::{AccountId, CosmosCoin, TxResponse};
|
||||
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/tauritxresult.ts"))]
|
||||
@@ -34,13 +33,13 @@ pub struct TransactionDetails {
|
||||
}
|
||||
|
||||
impl TauriTxResult {
|
||||
fn new(t: Response, details: TransactionDetails) -> TauriTxResult {
|
||||
fn new(t: TxResponse, details: TransactionDetails) -> TauriTxResult {
|
||||
TauriTxResult {
|
||||
block_height: t.height.value(),
|
||||
code: t.check_tx.code.value(),
|
||||
code: t.tx_result.code.value(),
|
||||
details,
|
||||
gas_used: t.check_tx.gas_used.value(),
|
||||
gas_wanted: t.check_tx.gas_wanted.value(),
|
||||
gas_used: t.tx_result.gas_used.value(),
|
||||
gas_wanted: t.tx_result.gas_wanted.value(),
|
||||
tx_hash: t.hash.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::config::{Config, OptionalValidators, ValidatorUrl};
|
||||
use crate::error::BackendError;
|
||||
use crate::network::Network;
|
||||
use crate::{config, network_config};
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
use validator_client::nymd::SigningNymdClient;
|
||||
use validator_client::Client;
|
||||
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
@@ -14,6 +15,16 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
// Some hardcoded metadata overrides
|
||||
static METADATA_OVERRIDES: Lazy<Vec<(Url, ValidatorMetadata)>> = Lazy::new(|| {
|
||||
vec![(
|
||||
"https://rpc.nyx.nodes.guru/".parse().unwrap(),
|
||||
ValidatorMetadata {
|
||||
name: Some("Nodes.Guru".to_string()),
|
||||
},
|
||||
)]
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_config_from_files(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
@@ -31,12 +42,26 @@ pub async fn save_config_to_files(
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
config: Config,
|
||||
config: config::Config,
|
||||
signing_clients: HashMap<Network, Client<SigningNymdClient>>,
|
||||
current_network: Network,
|
||||
|
||||
// All the accounts the we get from decrypting the wallet. We hold on to these for being able to
|
||||
// switch accounts on-the-fly
|
||||
all_accounts: Vec<WalletAccountIds>,
|
||||
|
||||
/// Validators that have been fetched dynamically, probably during startup.
|
||||
fetched_validators: OptionalValidators,
|
||||
fetched_validators: config::OptionalValidators,
|
||||
|
||||
/// We fetch (and cache) some metadata, such as names, when available
|
||||
validator_metadata: HashMap<Url, ValidatorMetadata>,
|
||||
}
|
||||
|
||||
pub(crate) struct WalletAccountIds {
|
||||
// The wallet account id
|
||||
pub id: crate::wallet_storage::AccountId,
|
||||
// The set of corresponding network identities derived from the mnemonic
|
||||
pub addresses: HashMap<Network, cosmrs::AccountId>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -72,13 +97,13 @@ impl State {
|
||||
.ok_or(BackendError::ClientNotInitialized)
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &Config {
|
||||
pub fn config(&self) -> &config::Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Load configuration from files. If unsuccessful we just log it and move on.
|
||||
pub fn load_config_files(&mut self) {
|
||||
self.config = Config::load_from_files();
|
||||
self.config = config::Config::load_from_files();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -98,6 +123,14 @@ impl State {
|
||||
self.current_network
|
||||
}
|
||||
|
||||
pub(crate) fn set_all_accounts(&mut self, all_accounts: Vec<WalletAccountIds>) {
|
||||
self.all_accounts = all_accounts
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_accounts(&self) -> impl Iterator<Item = &WalletAccountIds> {
|
||||
self.all_accounts.iter()
|
||||
}
|
||||
|
||||
pub fn logout(&mut self) {
|
||||
self.signing_clients = HashMap::new();
|
||||
}
|
||||
@@ -106,37 +139,98 @@ impl State {
|
||||
/// 1. from the configuration file
|
||||
/// 2. provided remotely
|
||||
/// 3. hardcoded fallback
|
||||
pub fn get_validators(&self, network: Network) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
/// The format is the config backend format, which is flat due to serialization preference.
|
||||
pub fn get_config_validator_entries(
|
||||
&self,
|
||||
network: Network,
|
||||
) -> impl Iterator<Item = config::ValidatorConfigEntry> + '_ {
|
||||
let validators_in_config = self.config.get_configured_validators(network);
|
||||
let fetched_validators = self.fetched_validators.validators(network).cloned();
|
||||
let default_validators = self.config.get_base_validators(network);
|
||||
|
||||
validators_in_config
|
||||
// All the validators, in decending list of priority
|
||||
let validators = validators_in_config
|
||||
.chain(fetched_validators)
|
||||
.chain(default_validators)
|
||||
.unique()
|
||||
.unique_by(|v| (v.nymd_url.clone(), v.api_url.clone()));
|
||||
|
||||
// Annotate with dynamic metadata
|
||||
validators.map(|v| {
|
||||
let metadata = self.validator_metadata.get(&v.nymd_url);
|
||||
let name = v
|
||||
.nymd_name
|
||||
.or_else(|| metadata.and_then(|m| m.name.clone()));
|
||||
config::ValidatorConfigEntry {
|
||||
nymd_url: v.nymd_url,
|
||||
nymd_name: name,
|
||||
api_url: v.api_url,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_nymd_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self.get_validators(network).into_iter().map(|v| v.nymd_url)
|
||||
}
|
||||
|
||||
pub fn get_api_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
pub fn get_nymd_urls_only(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self
|
||||
.get_validators(network)
|
||||
.get_config_validator_entries(network)
|
||||
.into_iter()
|
||||
.map(|v| v.nymd_url)
|
||||
}
|
||||
|
||||
pub fn get_api_urls_only(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self
|
||||
.get_config_validator_entries(network)
|
||||
.into_iter()
|
||||
.filter_map(|v| v.api_url)
|
||||
}
|
||||
|
||||
/// Get the list of validator nymd urls in the network config format, suitable for passing on to
|
||||
/// the UI
|
||||
pub fn get_nymd_urls(
|
||||
&self,
|
||||
network: Network,
|
||||
) -> impl Iterator<Item = network_config::ValidatorUrl> + '_ {
|
||||
self
|
||||
.get_config_validator_entries(network)
|
||||
.into_iter()
|
||||
.map(|v| network_config::ValidatorUrl {
|
||||
url: v.nymd_url.to_string(),
|
||||
name: v.nymd_name,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the list of validator-api urls in the network config format, suitable for passing on to
|
||||
/// the UI
|
||||
pub fn get_api_urls(
|
||||
&self,
|
||||
network: Network,
|
||||
) -> impl Iterator<Item = network_config::ValidatorUrl> + '_ {
|
||||
self
|
||||
.get_config_validator_entries(network)
|
||||
.into_iter()
|
||||
.filter_map(|v| {
|
||||
v.api_url.map(|u| network_config::ValidatorUrl {
|
||||
url: u.to_string(),
|
||||
name: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_all_nymd_urls(&self) -> HashMap<Network, Vec<Url>> {
|
||||
Network::iter()
|
||||
.flat_map(|network| self.get_nymd_urls(network).map(move |url| (network, url)))
|
||||
.flat_map(|network| {
|
||||
self
|
||||
.get_nymd_urls_only(network)
|
||||
.map(move |url| (network, url))
|
||||
})
|
||||
.into_group_map()
|
||||
}
|
||||
|
||||
pub fn get_all_api_urls(&self) -> HashMap<Network, Vec<Url>> {
|
||||
Network::iter()
|
||||
.flat_map(|network| self.get_api_urls(network).map(move |url| (network, url)))
|
||||
.flat_map(|network| {
|
||||
self
|
||||
.get_api_urls_only(network)
|
||||
.map(move |url| (network, url))
|
||||
})
|
||||
.into_group_map()
|
||||
}
|
||||
|
||||
@@ -154,11 +248,71 @@ impl State {
|
||||
.get(crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
self.fetched_validators = serde_json::from_str(&response.text().await?)?;
|
||||
log::debug!("Received validator urls: \n{}", self.fetched_validators);
|
||||
|
||||
self.refresh_validator_status().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh_validator_status(&mut self) -> Result<(), BackendError> {
|
||||
log::debug!("Refreshing validator status");
|
||||
|
||||
// All urls for all networks
|
||||
let nymd_urls = self
|
||||
.get_all_nymd_urls()
|
||||
.into_iter()
|
||||
.flat_map(|(_, urls)| urls.into_iter());
|
||||
|
||||
// Fetch status for all urls
|
||||
let responses = fetch_status_for_urls(nymd_urls).await?;
|
||||
|
||||
// Update the stored metadata
|
||||
self.apply_responses(responses)?;
|
||||
|
||||
// Override some overrides for usability
|
||||
self.apply_metadata_override(METADATA_OVERRIDES.to_vec());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_responses(
|
||||
&mut self,
|
||||
responses: Vec<Result<(Url, String), reqwest::Error>>,
|
||||
) -> Result<(), BackendError> {
|
||||
for response in responses.into_iter().flatten() {
|
||||
let json: serde_json::Value = serde_json::from_str(&response.1)?;
|
||||
let moniker = &json["result"]["node_info"]["moniker"];
|
||||
log::debug!("Fetched moniker for: {}: {}", response.0, moniker);
|
||||
|
||||
// Insert into metadata map
|
||||
if let Some(ref mut m) = self.validator_metadata.get_mut(&response.0) {
|
||||
m.name = Some(moniker.to_string());
|
||||
} else {
|
||||
self.validator_metadata.insert(
|
||||
response.0,
|
||||
ValidatorMetadata {
|
||||
name: Some(moniker.to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_metadata_override(&mut self, metadata_overrides: Vec<(Url, ValidatorMetadata)>) {
|
||||
for (url, metadata) in metadata_overrides {
|
||||
log::debug!("Overriding (some) metadata for: {url}");
|
||||
if let Some(m) = self.validator_metadata.get_mut(&url) {
|
||||
m.name = metadata.name;
|
||||
} else {
|
||||
self.validator_metadata.insert(url, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_validator_nymd_url(
|
||||
&mut self,
|
||||
url: &str,
|
||||
@@ -183,15 +337,41 @@ impl State {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: Network) {
|
||||
pub fn add_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) {
|
||||
self.config.add_validator_url(url, network);
|
||||
}
|
||||
|
||||
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: Network) {
|
||||
pub fn remove_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) {
|
||||
self.config.remove_validator_url(url, network)
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_status_for_urls(
|
||||
nymd_urls: impl Iterator<Item = Url>,
|
||||
) -> Result<Vec<Result<(Url, String), reqwest::Error>>, BackendError> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()?;
|
||||
|
||||
let responses = futures::future::join_all(nymd_urls.into_iter().map(|url| {
|
||||
let client = &client;
|
||||
let status_url = url.join("status").unwrap_or_else(|_| url.clone());
|
||||
async move {
|
||||
let resp = client.get(status_url).send().await?;
|
||||
resp.text().await.map(|text| (url, text))
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
|
||||
Ok(responses)
|
||||
}
|
||||
|
||||
// Validator metadata that can by dynamically populated
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ValidatorMetadata {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! client {
|
||||
($state:ident) => {
|
||||
@@ -223,31 +403,36 @@ mod tests {
|
||||
let _api_urls = state.get_api_urls(Network::MAINNET).collect::<Vec<_>>();
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
config::ValidatorConfigEntry {
|
||||
nymd_url: "http://nymd_url.com".parse().unwrap(),
|
||||
nymd_name: Some("NymdUrl".to_string()),
|
||||
api_url: Some("http://nymd_url.com/api".parse().unwrap()),
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
config::ValidatorConfigEntry {
|
||||
nymd_url: "http://foo.com".parse().unwrap(),
|
||||
nymd_name: None,
|
||||
api_url: None,
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
config::ValidatorConfigEntry {
|
||||
nymd_url: "http://bar.com".parse().unwrap(),
|
||||
nymd_name: None,
|
||||
api_url: None,
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
state.get_nymd_urls(Network::MAINNET).collect::<Vec<_>>(),
|
||||
state
|
||||
.get_nymd_urls_only(Network::MAINNET)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"http://nymd_url.com/".parse().unwrap(),
|
||||
"http://foo.com".parse().unwrap(),
|
||||
@@ -256,10 +441,12 @@ mod tests {
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_api_urls(Network::MAINNET).collect::<Vec<_>>(),
|
||||
state
|
||||
.get_api_urls_only(Network::MAINNET)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"http://nymd_url.com/api".parse().unwrap(),
|
||||
"https://api.nyx.nodes.guru".parse().unwrap(),
|
||||
"https://validator.nymtech.net/api/".parse().unwrap(),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// The wallet storage is a single json file, containing multiple entries. These are referred to as
|
||||
// Logins, and has a plaintext id tag attached.
|
||||
//
|
||||
// Each encrypted login contains either a single account, or a list of multiple accounts.
|
||||
//
|
||||
// NOTE: A not insignificant amount of complexity comes from being able to handle both these cases,
|
||||
// instead of, for example, converting a single account to a list of multiple accounts with a single
|
||||
// entry. This also avoids resaving the wallet file when opening a file created with an earlier
|
||||
// version of the wallet.
|
||||
//
|
||||
// In the future we might want to simplify by dropping the support for a single account entry,
|
||||
// instead treating as muliple accounts with one entry.
|
||||
|
||||
use cosmrs::bip32::DerivationPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
@@ -8,15 +21,16 @@ use zeroize::Zeroize;
|
||||
use crate::error::BackendError;
|
||||
|
||||
use super::encryption::EncryptedData;
|
||||
use super::password::WalletAccountId;
|
||||
use super::password::{AccountId, LoginId};
|
||||
use super::UserPassword;
|
||||
|
||||
const CURRENT_WALLET_FILE_VERSION: u32 = 1;
|
||||
|
||||
/// The wallet, stored as a serialized json file.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct StoredWallet {
|
||||
version: u32,
|
||||
accounts: Vec<EncryptedAccount>,
|
||||
accounts: Vec<EncryptedLogin>,
|
||||
}
|
||||
|
||||
impl StoredWallet {
|
||||
@@ -25,16 +39,52 @@ impl StoredWallet {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.accounts.len()
|
||||
}
|
||||
|
||||
pub fn remove_account(&mut self, id: &WalletAccountId) -> Option<EncryptedAccount> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
pub fn add_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> {
|
||||
if self.get_encrypted_login(&new_login.id).is_ok() {
|
||||
return Err(BackendError::WalletLoginIdAlreadyExists);
|
||||
}
|
||||
self.accounts.push(new_login);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_encrypted_login(&self, id: &LoginId) -> Result<&EncryptedData<StoredLogin>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| &account.id == id)
|
||||
.map(|account| &account.account)
|
||||
.ok_or(BackendError::WalletNoSuchLoginId)
|
||||
}
|
||||
|
||||
fn get_encrypted_login_mut(&mut self, id: &LoginId) -> Result<&mut EncryptedLogin, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter_mut()
|
||||
.find(|account| &account.id == id)
|
||||
.ok_or(BackendError::WalletNoSuchLoginId)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_encrypted_login_by_index(&self, index: usize) -> Option<&EncryptedLogin> {
|
||||
self.accounts.get(index)
|
||||
}
|
||||
|
||||
pub fn replace_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> {
|
||||
let login = self.get_encrypted_login_mut(&new_login.id)?;
|
||||
*login = new_login;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_encrypted_login(&mut self, id: &LoginId) -> Option<EncryptedLogin> {
|
||||
if let Some(index) = self.accounts.iter().position(|account| &account.id == id) {
|
||||
log::info!("Removing from wallet file: {id}");
|
||||
Some(self.accounts.remove(index))
|
||||
@@ -44,43 +94,15 @@ impl StoredWallet {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
|
||||
self.accounts.get(index)
|
||||
}
|
||||
|
||||
fn encrypted_account(
|
||||
pub fn decrypt_login(
|
||||
&self,
|
||||
id: &WalletAccountId,
|
||||
) -> Result<&EncryptedData<StoredAccount>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| &account.id == id)
|
||||
.map(|account| &account.account)
|
||||
.ok_or(BackendError::NoSuchIdInWallet)
|
||||
}
|
||||
|
||||
pub fn add_encrypted_account(
|
||||
&mut self,
|
||||
new_account: EncryptedAccount,
|
||||
) -> Result<(), BackendError> {
|
||||
if self.encrypted_account(&new_account.id).is_ok() {
|
||||
return Err(BackendError::IdAlreadyExistsInWallet);
|
||||
}
|
||||
self.accounts.push(new_account);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decrypt_account(
|
||||
&self,
|
||||
id: &WalletAccountId,
|
||||
id: &LoginId,
|
||||
password: &UserPassword,
|
||||
) -> Result<StoredAccount, BackendError> {
|
||||
self.encrypted_account(id)?.decrypt_struct(password)
|
||||
) -> Result<StoredLogin, BackendError> {
|
||||
self.get_encrypted_login(id)?.decrypt_struct(password)
|
||||
}
|
||||
|
||||
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredAccount>, BackendError> {
|
||||
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredLogin>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
@@ -102,39 +124,176 @@ impl Default for StoredWallet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Each entry in the stored wallet file. An id field in plaintext and an encrypted stored login.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct EncryptedAccount {
|
||||
pub id: WalletAccountId,
|
||||
pub account: EncryptedData<StoredAccount>,
|
||||
pub(crate) struct EncryptedLogin {
|
||||
pub id: LoginId,
|
||||
pub account: EncryptedData<StoredLogin>,
|
||||
}
|
||||
|
||||
// future-proofing
|
||||
impl EncryptedLogin {
|
||||
pub(crate) fn encrypt(
|
||||
id: LoginId,
|
||||
login: &StoredLogin,
|
||||
password: &UserPassword,
|
||||
) -> Result<Self, BackendError> {
|
||||
Ok(EncryptedLogin {
|
||||
id,
|
||||
account: super::encryption::encrypt_struct(login, password)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A stored login is either a account, such as a mnemonic, or a list of multiple accounts where
|
||||
/// each has an inner id. Future proofed for having private key backed accounts.
|
||||
#[derive(Serialize, Deserialize, Debug, Zeroize)]
|
||||
#[serde(untagged)]
|
||||
#[zeroize(drop)]
|
||||
pub(crate) enum StoredAccount {
|
||||
pub(crate) enum StoredLogin {
|
||||
Mnemonic(MnemonicAccount),
|
||||
// PrivateKey(PrivateKeyAccount)
|
||||
Multiple(MultipleAccounts),
|
||||
}
|
||||
|
||||
impl StoredAccount {
|
||||
pub(crate) fn new_mnemonic_backed_account(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
) -> StoredAccount {
|
||||
StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path })
|
||||
impl StoredLogin {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn as_mnemonic_account(&self) -> Option<&MnemonicAccount> {
|
||||
match self {
|
||||
StoredLogin::Mnemonic(mn) => Some(mn),
|
||||
StoredLogin::Multiple(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
// If we add accounts backed by something that is not a mnemonic, this should probably be changed
|
||||
// to return `Option<..>`.
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn as_multiple_accounts(&self) -> Option<&MultipleAccounts> {
|
||||
match self {
|
||||
StoredAccount::Mnemonic(account) => account.mnemonic(),
|
||||
StoredLogin::Mnemonic(_) => None,
|
||||
StoredLogin::Multiple(accounts) => Some(accounts),
|
||||
}
|
||||
}
|
||||
|
||||
// Return the login as multiple accounts, and if there is only a single mnemonic backed account,
|
||||
// return a set containing only the single account paired with the account id passed as function
|
||||
// argument.
|
||||
pub(crate) fn unwrap_into_multiple_accounts(self, id: AccountId) -> MultipleAccounts {
|
||||
match self {
|
||||
StoredLogin::Mnemonic(ref account) => vec![WalletAccount::new(id, account.clone())].into(),
|
||||
StoredLogin::Multiple(ref accounts) => accounts.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
/// Multiple stored accounts, each entry having an id and a data field.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
|
||||
pub(crate) struct MultipleAccounts {
|
||||
accounts: Vec<WalletAccount>,
|
||||
}
|
||||
|
||||
impl MultipleAccounts {
|
||||
pub(crate) fn new() -> Self {
|
||||
MultipleAccounts {
|
||||
accounts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_accounts(&self) -> impl Iterator<Item = &WalletAccount> {
|
||||
self.accounts.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn get_account(&self, id: &AccountId) -> Option<&WalletAccount> {
|
||||
self.accounts.iter().find(|account| &account.id == id)
|
||||
}
|
||||
|
||||
pub(crate) fn into_accounts(self) -> impl Iterator<Item = WalletAccount> {
|
||||
self.accounts.into_iter()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.accounts.len()
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn add(
|
||||
&mut self,
|
||||
id: AccountId,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
) -> Result<(), BackendError> {
|
||||
if self.get_account(&id).is_some() {
|
||||
Err(BackendError::WalletAccountIdAlreadyExistsInWalletLogin)
|
||||
} else {
|
||||
self.accounts.push(WalletAccount::new(
|
||||
id,
|
||||
MnemonicAccount::new(mnemonic, hd_path),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, id: &AccountId) -> Result<(), BackendError> {
|
||||
if self.get_account(id).is_none() {
|
||||
return Err(BackendError::WalletNoSuchAccountIdInWalletLogin);
|
||||
}
|
||||
self.accounts.retain(|accounts| &accounts.id != id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<WalletAccount>> for MultipleAccounts {
|
||||
fn from(accounts: Vec<WalletAccount>) -> MultipleAccounts {
|
||||
Self { accounts }
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry in the list of stored accounts
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
|
||||
pub(crate) struct WalletAccount {
|
||||
id: AccountId,
|
||||
account: AccountData,
|
||||
}
|
||||
|
||||
impl WalletAccount {
|
||||
pub(crate) fn new(id: AccountId, mnemonic_account: MnemonicAccount) -> Self {
|
||||
Self {
|
||||
id,
|
||||
account: AccountData::Mnemonic(mnemonic_account),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> &AccountId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
match self.account {
|
||||
AccountData::Mnemonic(ref account) => account.mnemonic(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn hd_path(&self) -> &DerivationPath {
|
||||
match self.account {
|
||||
AccountData::Mnemonic(ref account) => account.hd_path(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An account usually is a mnemonic account, but in the future it might be backed by a private
|
||||
/// key.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
#[zeroize(drop)]
|
||||
enum AccountData {
|
||||
Mnemonic(MnemonicAccount),
|
||||
// PrivateKey(PrivateKeyAccount)
|
||||
}
|
||||
|
||||
/// An account backed by a unique mnemonic.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct MnemonicAccount {
|
||||
mnemonic: bip39::Mnemonic,
|
||||
#[serde(with = "display_hd_path")]
|
||||
@@ -142,11 +301,15 @@ pub(crate) struct MnemonicAccount {
|
||||
}
|
||||
|
||||
impl MnemonicAccount {
|
||||
pub(crate) fn new(mnemonic: bip39::Mnemonic, hd_path: DerivationPath) -> Self {
|
||||
Self { mnemonic, hd_path }
|
||||
}
|
||||
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
&self.mnemonic
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn hd_path(&self) -> &DerivationPath {
|
||||
&self.hd_path
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user