Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 922941f2db | |||
| f334acab99 | |||
| 001c98e544 | |||
| d3eb238d5a | |||
| 23f0212a16 | |||
| cb58a62ff7 | |||
| cc8be3bce2 | |||
| a2c6abd3dd | |||
| d289c46e87 | |||
| a6aba3defd | |||
| 6557be3738 | |||
| 7134755073 | |||
| dd1420a65a | |||
| df1bc60464 | |||
| 865e809342 | |||
| 51f9c1ca29 | |||
| 303b014a59 | |||
| e1e20fb13e | |||
| 0c3c13ae88 | |||
| 8c8b7d71d0 | |||
| 3163c5f054 | |||
| 4a1794b2f1 | |||
| 1898b8ed96 | |||
| a23471859d | |||
| 9d8c9edf22 | |||
| 5ea7b24efc | |||
| a43a24faa8 | |||
| 39ee215005 | |||
| ef7961f58e | |||
| e628338b33 | |||
| 1bb137f87f |
@@ -0,0 +1,50 @@
|
||||
name: Publish Nym CLI binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
NETWORK: mainnet
|
||||
|
||||
jobs:
|
||||
publish-nym-cli:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Check the release tag starts with `nym-cli-`
|
||||
if: startsWith(github.ref, 'refs/tags/nym-cli-') == false && github.event_name != 'workflow_dispatch'
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
script: |
|
||||
core.setFailed('Release tag did not start with nym-cli-...')
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Build binary
|
||||
run: make build-nym-cli
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: nym-cli-${{ matrix.platform }}
|
||||
path: |
|
||||
target/release/nym-cli*
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/nym-cli
|
||||
@@ -1,5 +1,7 @@
|
||||
name: Publish Nym binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
@@ -18,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Check the release tag starts with `nym-binaries-`
|
||||
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false
|
||||
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false && github.event_name != 'workflow_dispatch'
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
script: |
|
||||
@@ -35,8 +37,24 @@ jobs:
|
||||
command: build
|
||||
args: --workspace --release
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
target/release/nym-client
|
||||
target/release/nym-gateway
|
||||
target/release/nym-mixnode
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-validator-api
|
||||
target/release/nym-network-requester
|
||||
target/release/nym-network-statistics
|
||||
target/release/nym-cli
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/nym-client
|
||||
@@ -45,4 +63,5 @@ jobs:
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-validator-api
|
||||
target/release/nym-network-requester
|
||||
target/release/nym-network-statistics
|
||||
target/release/nym-network-statistics
|
||||
target/release/nym-cli
|
||||
|
||||
+13
-1
@@ -4,11 +4,19 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558])
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- validator-client: made `fee` argument optional for `execute` and `execute_multiple` ([#1541])
|
||||
- wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585])
|
||||
|
||||
[#1541]: https://github.com/nymtech/nym/pull/1541
|
||||
[#1558]: https://github.com/nymtech/nym/pull/1558
|
||||
[#1585]: https://github.com/nymtech/nym/pull/1585
|
||||
|
||||
|
||||
## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2)
|
||||
@@ -27,6 +35,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
- validator-api: add Swagger to document the REST API ([#1249]).
|
||||
- validator-api: Added new endpoints for coconut spending flow and communications with coconut & multisig contracts ([#1261])
|
||||
- validator-api: add `uptime`, `estimated_operator_apy`, `estimated_delegators_apy` to `/mixnodes/detailed` endpoint ([#1393])
|
||||
- validator-api: add node info cache storing simulated active set inclusion probabilities
|
||||
- network-statistics: a new mixnet service that aggregates and exposes anonymized data about mixnet services ([#1328])
|
||||
- mixnode: Added basic mixnode hardware reporting to the HTTP API ([#1308]).
|
||||
- validator-api: endpoint, in coconut mode, for returning the validator-api cosmos address ([#1404]).
|
||||
@@ -46,7 +55,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
- validator: fixed local docker-compose setup to work on Apple M1 ([#1329])
|
||||
- explorer-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1482]).
|
||||
- network-requester: fix filter for suffix-only domains ([#1487])
|
||||
- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1496]).
|
||||
- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service; cleaner shutdown, without panics ([#1496], [#1573]).
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -63,6 +72,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
- gateway, network-statistics: include gateway id in the sent statistical data ([#1478])
|
||||
- network explorer: tweak how active set probability is shown ([#1503])
|
||||
- validator-api: rewarder set update fails without panicking on possible nymd queries ([#1520])
|
||||
- network-requester, socks5 client (nym-connect): send and receive respectively a message error to be displayed about filter check failure ([#1576])
|
||||
|
||||
|
||||
[#1249]: https://github.com/nymtech/nym/pull/1249
|
||||
@@ -92,6 +102,8 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
[#1496]: https://github.com/nymtech/nym/pull/1496
|
||||
[#1503]: https://github.com/nymtech/nym/pull/1503
|
||||
[#1520]: https://github.com/nymtech/nym/pull/1520
|
||||
[#1573]: https://github.com/nymtech/nym/pull/1573
|
||||
[#1576]: https://github.com/nymtech/nym/pull/1576
|
||||
|
||||
## [v1.0.1](https://github.com/nymtech/nym/tree/v1.0.1) (2022-05-04)
|
||||
|
||||
|
||||
Generated
+107
-56
@@ -894,6 +894,7 @@ dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"clap 3.2.8",
|
||||
"coconut-interface",
|
||||
"config",
|
||||
"credential-storage",
|
||||
"credentials",
|
||||
"crypto",
|
||||
@@ -1127,7 +1128,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8"
|
||||
dependencies = [
|
||||
"sct",
|
||||
"sct 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1903,10 +1904,6 @@ name = "futures-timer"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
|
||||
dependencies = [
|
||||
"gloo-timers",
|
||||
"send_wrapper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
@@ -1957,8 +1954,8 @@ dependencies = [
|
||||
"secp256k1",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tungstenite",
|
||||
"tokio-tungstenite 0.17.2",
|
||||
"tungstenite 0.17.3",
|
||||
"url",
|
||||
"validator-client",
|
||||
"wasm-bindgen",
|
||||
@@ -1983,7 +1980,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tungstenite",
|
||||
"tungstenite 0.17.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2091,18 +2088,6 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.10.0"
|
||||
@@ -2395,7 +2380,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki",
|
||||
"webpki 0.21.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2408,12 +2393,12 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hyper",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls 0.19.1",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"webpki",
|
||||
"webpki-roots",
|
||||
"webpki 0.21.4",
|
||||
"webpki-roots 0.21.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2500,6 +2485,7 @@ dependencies = [
|
||||
name = "inclusion-probability"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -2751,9 +2737,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.16"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
@@ -3087,7 +3073,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sled",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.17.2",
|
||||
"topology",
|
||||
"url",
|
||||
"validator-client",
|
||||
@@ -3134,7 +3120,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.17.2",
|
||||
"tokio-util 0.7.3",
|
||||
"url",
|
||||
"validator-api-requests",
|
||||
@@ -3208,7 +3194,7 @@ dependencies = [
|
||||
"statistics-common",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.14.0",
|
||||
"websocket-requests",
|
||||
]
|
||||
|
||||
@@ -3225,7 +3211,7 @@ dependencies = [
|
||||
"statistics-common",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3314,6 +3300,7 @@ dependencies = [
|
||||
"gateway-client",
|
||||
"getset",
|
||||
"humantime-serde",
|
||||
"inclusion-probability",
|
||||
"log",
|
||||
"mixnet-contract-common",
|
||||
"multisig-contract-common",
|
||||
@@ -3333,6 +3320,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tap",
|
||||
"task",
|
||||
"thiserror",
|
||||
"time 0.3.9",
|
||||
@@ -4726,8 +4714,20 @@ dependencies = [
|
||||
"base64",
|
||||
"log",
|
||||
"ring",
|
||||
"sct",
|
||||
"webpki",
|
||||
"sct 0.6.1",
|
||||
"webpki 0.21.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.20.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"sct 0.7.0",
|
||||
"webpki 0.22.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4737,7 +4737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls",
|
||||
"rustls 0.19.1",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
@@ -4829,6 +4829,16 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.2.1"
|
||||
@@ -4922,12 +4932,6 @@ dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "send_wrapper"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.136"
|
||||
@@ -4989,9 +4993,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.79"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
|
||||
dependencies = [
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
@@ -5207,6 +5211,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"nymsphinx-addressing",
|
||||
"ordered-buffer",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5325,7 +5330,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls 0.19.1",
|
||||
"sha2 0.9.9",
|
||||
"smallvec 1.8.0",
|
||||
"sqlformat",
|
||||
@@ -5334,8 +5339,8 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"webpki",
|
||||
"webpki-roots",
|
||||
"webpki 0.21.4",
|
||||
"webpki-roots 0.21.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5794,9 +5799,9 @@ version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls 0.19.1",
|
||||
"tokio",
|
||||
"webpki",
|
||||
"webpki 0.21.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5833,7 +5838,19 @@ dependencies = [
|
||||
"log",
|
||||
"pin-project",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
"tungstenite 0.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite 0.17.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6101,6 +6118,28 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.17.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rustls 0.20.6",
|
||||
"sha-1 0.10.0",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
"webpki 0.22.0",
|
||||
"webpki-roots 0.22.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
@@ -6413,8 +6452,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
@@ -6480,7 +6517,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"js-sys",
|
||||
"tungstenite",
|
||||
"tungstenite 0.17.3",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
@@ -6510,15 +6547,12 @@ dependencies = [
|
||||
"ethereum-types",
|
||||
"futures",
|
||||
"futures-timer",
|
||||
"getrandom 0.2.6",
|
||||
"headers",
|
||||
"hex",
|
||||
"js-sys",
|
||||
"jsonrpc-core",
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
"pin-project",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"rlp",
|
||||
"secp256k1",
|
||||
@@ -6530,8 +6564,6 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-util 0.6.9",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web3-async-native-tls",
|
||||
]
|
||||
|
||||
@@ -6557,13 +6589,32 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
|
||||
dependencies = [
|
||||
"webpki",
|
||||
"webpki 0.21.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
|
||||
dependencies = [
|
||||
"webpki 0.22.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
-1
@@ -133,7 +133,6 @@ where
|
||||
let prepared_fragment = self
|
||||
.message_preparer
|
||||
.prepare_chunk_for_sending(chunk_clone, topology, &self.ack_key, &recipient)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
real_messages.push(RealMessage::new(
|
||||
|
||||
-1
@@ -83,7 +83,6 @@ where
|
||||
let prepared_fragment = self
|
||||
.message_preparer
|
||||
.prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, packet_recipient)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// if we have the ONLY strong reference to the ack data, it means it was removed from the
|
||||
|
||||
@@ -18,6 +18,7 @@ url = "2.2"
|
||||
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal", "macros"] } # async runtime
|
||||
|
||||
coconut-interface = { path = "../../common/coconut-interface" }
|
||||
config = { path = "../../common/config" }
|
||||
credentials = { path = "../../common/credentials" }
|
||||
credential-storage = { path = "../../common/credential-storage" }
|
||||
crypto = { path = "../../common/crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] }
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::{MNEMONIC, NYMD_URL};
|
||||
use bip39::Mnemonic;
|
||||
use network_defaults::{NymNetworkDetails, VOUCHER_INFO};
|
||||
use std::str::FromStr;
|
||||
@@ -17,9 +16,9 @@ pub(crate) struct Client {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
let nymd_url = Url::from_str(NYMD_URL).unwrap();
|
||||
let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap();
|
||||
pub fn new(nymd_url: &str, mnemonic: &str) -> Self {
|
||||
let nymd_url = Url::from_str(nymd_url).unwrap();
|
||||
let mnemonic = Mnemonic::from_str(mnemonic).unwrap();
|
||||
let network_details = NymNetworkDetails::new_from_env();
|
||||
let config = nymd::Config::try_from_nym_network_details(&network_details)
|
||||
.expect("failed to construct valid validator client config with the provided network");
|
||||
|
||||
@@ -6,7 +6,6 @@ use clap::{Args, Subcommand};
|
||||
use pickledb::PickleDb;
|
||||
use rand::rngs::OsRng;
|
||||
use std::str::FromStr;
|
||||
use url::Url;
|
||||
|
||||
use coconut_interface::{Attribute, Base58, BlindSignRequest, Bytable, Parameters};
|
||||
use credential_storage::storage::Storage;
|
||||
@@ -20,7 +19,6 @@ use validator_client::nymd::tx::Hash;
|
||||
use crate::client::Client;
|
||||
use crate::error::{CredentialClientError, Result};
|
||||
use crate::state::{KeyPair, RequestData, State};
|
||||
use crate::SIGNER_AUTHORITIES;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub(crate) enum Commands {
|
||||
@@ -39,6 +37,12 @@ pub(crate) trait Execute {
|
||||
|
||||
#[derive(Args, Clone)]
|
||||
pub(crate) struct Deposit {
|
||||
/// The nymd URL that should be used
|
||||
#[clap(long)]
|
||||
nymd_url: String,
|
||||
/// A mnemonic for the account that does the deposit
|
||||
#[clap(long)]
|
||||
mnemonic: String,
|
||||
/// The amount that needs to be deposited
|
||||
#[clap(long)]
|
||||
amount: u64,
|
||||
@@ -51,7 +55,7 @@ impl Execute for Deposit {
|
||||
let signing_keypair = KeyPair::from(identity::KeyPair::new(&mut rng));
|
||||
let encryption_keypair = KeyPair::from(encryption::KeyPair::new(&mut rng));
|
||||
|
||||
let client = Client::new();
|
||||
let client = Client::new(&self.nymd_url, &self.mnemonic);
|
||||
let tx_hash = client
|
||||
.deposit(
|
||||
self.amount,
|
||||
@@ -96,6 +100,10 @@ pub(crate) struct GetCredential {
|
||||
/// The hash of a successful deposit transaction
|
||||
#[clap(long)]
|
||||
tx_hash: String,
|
||||
/// The URLs to the validator-api endpoints the are run as coconut signer authorities, separated
|
||||
/// by comma (,)
|
||||
#[clap(long)]
|
||||
signer_authorities: String,
|
||||
/// If we want to get the signature without attaching a blind sign request; it is expected that
|
||||
/// there is already a signature stored on the signer
|
||||
#[clap(long, parse(from_flag))]
|
||||
@@ -108,7 +116,8 @@ impl Execute for GetCredential {
|
||||
let mut state = db
|
||||
.get::<State>(&self.tx_hash)
|
||||
.ok_or(CredentialClientError::NoDeposit)?;
|
||||
let urls = SIGNER_AUTHORITIES.map(|addr| Url::from_str(addr).unwrap());
|
||||
|
||||
let urls = config::parse_validators(&self.signer_authorities);
|
||||
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
|
||||
let bandwidth_credential_attributes = if self.__no_request {
|
||||
|
||||
@@ -11,20 +11,24 @@ cfg_if::cfg_if! {
|
||||
|
||||
use commands::{Commands, Execute};
|
||||
use error::Result;
|
||||
use network_defaults::setup_env;
|
||||
|
||||
use clap::Parser;
|
||||
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
|
||||
|
||||
pub const MNEMONIC: &str = "jazz fatigue diagram account outer wrist slide cherry mother grid network pause wolf pig round answer mail junior better hair dismiss toward access end";
|
||||
pub const NYMD_URL: &str = "http://127.0.0.1:26657";
|
||||
pub const CONTRACT_ADDRESS: &str = "nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s";
|
||||
pub const SIGNER_AUTHORITIES: [&str; 1] = [
|
||||
"http://127.0.0.1:8080",
|
||||
];
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author = "Nymtech", version, about)]
|
||||
struct Cli {
|
||||
/// Path pointing to an env file that configures the client.
|
||||
#[clap(long)]
|
||||
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
||||
|
||||
/// Path where the sqlite credental database will be located.
|
||||
/// It should point to a $HOME/$CLIENT_ID/data/db.sqlite file of
|
||||
/// the client that is supposed to use the credential.
|
||||
#[clap(long)]
|
||||
pub(crate) credential_db_path: std::path::PathBuf,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
@@ -32,8 +36,9 @@ cfg_if::cfg_if! {
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Cli::parse();
|
||||
setup_env(args.config_env_file.clone());
|
||||
|
||||
let shared_storage = credential_storage::initialise_storage(std::path::PathBuf::from("/tmp/credential.db")).await;
|
||||
let shared_storage = credential_storage::initialise_storage(args.credential_db_path.clone()).await;
|
||||
let mut db = match PickleDb::load(
|
||||
"credential.db",
|
||||
PickleDbDumpPolicy::AutoDump,
|
||||
|
||||
@@ -28,7 +28,7 @@ rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits +
|
||||
serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization
|
||||
sled = "0.34" # for storage of replySURB decryption keys
|
||||
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] } # async runtime
|
||||
tokio-tungstenite = "0.14" # websocket
|
||||
tokio-tungstenite = "0.17.2" # websocket
|
||||
|
||||
## internal
|
||||
client-core = { path = "../client-core" }
|
||||
|
||||
@@ -50,6 +50,10 @@ impl NymConfig for Config {
|
||||
.join("clients")
|
||||
}
|
||||
|
||||
fn try_default_root_directory() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|path| path.join(".nym").join("clients"))
|
||||
}
|
||||
|
||||
fn root_directory(&self) -> PathBuf {
|
||||
self.base.get_nym_root_directory()
|
||||
}
|
||||
|
||||
@@ -199,9 +199,9 @@ impl NymClient {
|
||||
Some(bandwidth_controller),
|
||||
);
|
||||
|
||||
if self.config.get_base().get_disabled_credentials_mode() {
|
||||
gateway_client.set_disabled_credentials_mode(true)
|
||||
}
|
||||
gateway_client
|
||||
.set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode());
|
||||
|
||||
gateway_client
|
||||
.authenticate_and_start()
|
||||
.await
|
||||
|
||||
@@ -46,10 +46,9 @@ pub(crate) struct Init {
|
||||
fastmode: bool,
|
||||
|
||||
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
|
||||
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
|
||||
/// --eth-private_key don't need to be set.
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
|
||||
/// with bandwidth credential requirement.
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
#[clap(long)]
|
||||
enabled_credentials_mode: bool,
|
||||
|
||||
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
|
||||
@@ -79,7 +78,7 @@ impl From<Init> for OverrideConfig {
|
||||
port: init_config.port,
|
||||
fastmode: init_config.fastmode,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: init_config.enabled_credentials_mode,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
|
||||
@@ -78,7 +78,7 @@ pub(crate) struct OverrideConfig {
|
||||
port: Option<u16>,
|
||||
fastmode: bool,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: bool,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
@@ -126,12 +126,15 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
|
||||
.get_base_mut()
|
||||
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
{
|
||||
if args.enabled_credentials_mode {
|
||||
config.get_base_mut().with_disabled_credentials(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
{
|
||||
if let Some(eth_endpoint) = args.eth_endpoint {
|
||||
config.get_base_mut().with_eth_endpoint(eth_endpoint);
|
||||
}
|
||||
|
||||
@@ -35,10 +35,9 @@ pub(crate) struct Run {
|
||||
port: Option<u16>,
|
||||
|
||||
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
|
||||
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
|
||||
/// --eth-private-key don't need to be set.
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
|
||||
/// with bandwidth credential requirement.
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
#[clap(long)]
|
||||
enabled_credentials_mode: bool,
|
||||
|
||||
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
|
||||
@@ -62,7 +61,7 @@ impl From<Run> for OverrideConfig {
|
||||
port: run_config.port,
|
||||
fastmode: false,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: run_config.enabled_credentials_mode,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
|
||||
@@ -33,6 +33,10 @@ impl NymConfig for Config {
|
||||
.join("socks5-clients")
|
||||
}
|
||||
|
||||
fn try_default_root_directory() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|path| path.join(".nym").join("socks5-clients"))
|
||||
}
|
||||
|
||||
fn root_directory(&self) -> PathBuf {
|
||||
self.base.get_nym_root_directory()
|
||||
}
|
||||
|
||||
@@ -198,9 +198,9 @@ impl NymClient {
|
||||
Some(bandwidth_controller),
|
||||
);
|
||||
|
||||
if self.config.get_base().get_disabled_credentials_mode() {
|
||||
gateway_client.set_disabled_credentials_mode(true)
|
||||
}
|
||||
gateway_client
|
||||
.set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode());
|
||||
|
||||
gateway_client
|
||||
.authenticate_and_start()
|
||||
.await
|
||||
|
||||
@@ -46,10 +46,9 @@ pub(crate) struct Init {
|
||||
fastmode: bool,
|
||||
|
||||
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
|
||||
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
|
||||
/// --eth-private_key don't need to be set.
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
|
||||
/// with bandwidth credential requirement.
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
#[clap(long)]
|
||||
enabled_credentials_mode: bool,
|
||||
|
||||
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
|
||||
@@ -78,7 +77,7 @@ impl From<Init> for OverrideConfig {
|
||||
port: init_config.port,
|
||||
fastmode: init_config.fastmode,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: init_config.enabled_credentials_mode,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
|
||||
@@ -78,7 +78,7 @@ pub(crate) struct OverrideConfig {
|
||||
port: Option<u16>,
|
||||
fastmode: bool,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: bool,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
@@ -121,11 +121,14 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
|
||||
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
{
|
||||
if args.enabled_credentials_mode {
|
||||
config.get_base_mut().with_disabled_credentials(false)
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
{
|
||||
if let Some(eth_endpoint) = args.eth_endpoint {
|
||||
config.get_base_mut().with_eth_endpoint(eth_endpoint);
|
||||
}
|
||||
|
||||
@@ -39,10 +39,9 @@ pub(crate) struct Run {
|
||||
port: Option<u16>,
|
||||
|
||||
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
|
||||
/// with bandwidth credential requirement. If this value is set, --eth-endpoint and
|
||||
/// --eth-private-key don't need to be set.
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])]
|
||||
/// with bandwidth credential requirement.
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
#[clap(long)]
|
||||
enabled_credentials_mode: bool,
|
||||
|
||||
/// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20
|
||||
@@ -65,7 +64,7 @@ impl From<Run> for OverrideConfig {
|
||||
port: run_config.port,
|
||||
fastmode: false,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: run_config.enabled_credentials_mode,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
|
||||
@@ -54,6 +54,13 @@ impl MixnetResponseListener {
|
||||
return;
|
||||
}
|
||||
Ok(Message::Response(data)) => data,
|
||||
Ok(Message::NetworkRequesterResponse(r)) => {
|
||||
error!(
|
||||
"Network requester failed on connection id {} with error: {}",
|
||||
r.connection_id, r.network_requester_error
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.controller_sender
|
||||
|
||||
@@ -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.1"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
|
||||
license = "Apache-2.0"
|
||||
@@ -32,7 +32,7 @@ credentials = { path = "../../common/credentials", optional = true }
|
||||
crypto = { path = "../../common/crypto" }
|
||||
nymsphinx = { path = "../../common/nymsphinx" }
|
||||
topology = { path = "../../common/topology" }
|
||||
gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm"] }
|
||||
gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] }
|
||||
validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
|
||||
wasm-utils = { path = "../../common/wasm-utils" }
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
@@ -25,7 +25,7 @@ async function main() {
|
||||
set_panic_hook();
|
||||
|
||||
// validator server we will use to get topology from
|
||||
const validator = "https://sandbox-validator.nymtech.net/api"; //"http://localhost:8081";
|
||||
const validator = "https://validator.nymtech.net/api"; //"http://localhost:8081";
|
||||
|
||||
client = new NymClient(validator);
|
||||
|
||||
|
||||
@@ -132,9 +132,7 @@ impl NymClient {
|
||||
bandwidth_controller,
|
||||
);
|
||||
|
||||
if disabled_credentials_mode {
|
||||
gateway_client.set_disabled_credentials_mode(true)
|
||||
}
|
||||
gateway_client.set_disabled_credentials_mode(disabled_credentials_mode);
|
||||
|
||||
gateway_client
|
||||
.authenticate_and_start()
|
||||
@@ -199,7 +197,6 @@ impl NymClient {
|
||||
// don't bother with acks etc. for time being
|
||||
let prepared_fragment = message_preparer
|
||||
.prepare_chunk_for_sending(message_chunk, topology, &self.ack_key, &recipient)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
console_warn!("packet is going to have round trip time of {:?}, but we're not going to do anything for acks anyway ", prepared_fragment.total_delay);
|
||||
@@ -214,7 +211,7 @@ impl NymClient {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn choose_gateway(&self) -> &gateway::Node {
|
||||
pub(crate) fn choose_gateway(&self) -> gateway::Node {
|
||||
let topology = self
|
||||
.topology
|
||||
.as_ref()
|
||||
@@ -222,7 +219,21 @@ impl NymClient {
|
||||
|
||||
// choose the first one available
|
||||
assert!(!topology.gateways().is_empty());
|
||||
topology.gateways().first().unwrap()
|
||||
topology.gateways().first().unwrap();
|
||||
|
||||
console_log!("picking nym gateway");
|
||||
|
||||
gateway::Node {
|
||||
owner: "n1kymvkx6vsq7pvn6hfurkpg06h3j4gxj4em7tlg".to_string(),
|
||||
stake: 100000000,
|
||||
location: "PRIVACY HQ".to_string(),
|
||||
host: "gateway1.nymtech.net".parse().unwrap(),
|
||||
mix_host: "213.219.38.119:1789".parse().unwrap(),
|
||||
clients_port: 443,
|
||||
identity_key: identity::PublicKey::from_base58_string("E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM").unwrap(),
|
||||
sphinx_key: encryption::PublicKey::from_base58_string("CYcrjoJ8GT7Dp54zViUyyRUfegeRCyPifWQZHRgMZrfX").unwrap(),
|
||||
version: "1.0.0-rc.2".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Right now it's impossible to have async exported functions to take `&mut self` rather than mut self
|
||||
|
||||
@@ -15,8 +15,8 @@ log = "0.4"
|
||||
thiserror = "1.0"
|
||||
url = "2.2"
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
secp256k1 = "0.20.3"
|
||||
web3 = { version = "0.17.0", default-features = false }
|
||||
secp256k1 = { version = "0.20.3", optional = true }
|
||||
web3 = { version = "0.17.0", default-features = false, optional = true }
|
||||
async-trait = { version = "0.1.51" }
|
||||
|
||||
# internal
|
||||
@@ -30,8 +30,8 @@ network-defaults = { path = "../../network-defaults" }
|
||||
validator-client = { path = "../validator-client", optional = true }
|
||||
|
||||
[dependencies.tungstenite]
|
||||
version = "0.13"
|
||||
default-features = false
|
||||
version = "0.17.3"
|
||||
features = ["rustls-tls-webpki-roots"]
|
||||
|
||||
# non-wasm-only dependencies
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
@@ -39,7 +39,7 @@ version = "1.19.1"
|
||||
features = ["macros", "rt", "net", "sync", "time"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
|
||||
version = "0.14"
|
||||
version = "0.17.2"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.credential-storage]
|
||||
path = "../../credential-storage"
|
||||
@@ -73,5 +73,5 @@ features = ["js"]
|
||||
|
||||
[features]
|
||||
coconut = ["gateway-requests/coconut", "coconut-interface", "validator-client", "credentials/coconut"]
|
||||
wasm = ["web3/wasm", "web3/http", "web3/signing"]
|
||||
default = ["web3/default"]
|
||||
wasm = []
|
||||
default = ["web3/default", "secp256k1"]
|
||||
|
||||
@@ -23,7 +23,7 @@ use {
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
use {
|
||||
credentials::token::bandwidth::TokenCredential,
|
||||
crypto::asymmetric::identity,
|
||||
@@ -45,7 +45,7 @@ use {
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
pub fn eth_contract(web3: Web3<Http>) -> Contract<Http> {
|
||||
Contract::from_json(
|
||||
web3.eth(),
|
||||
@@ -58,7 +58,7 @@ pub fn eth_contract(web3: Web3<Http>) -> Contract<Http> {
|
||||
.expect("Invalid json abi")
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
pub fn eth_erc20_contract(web3: Web3<Http>) -> Contract<Http> {
|
||||
Contract::from_json(
|
||||
web3.eth(),
|
||||
@@ -76,11 +76,11 @@ pub struct BandwidthController<St: Storage> {
|
||||
storage: St,
|
||||
#[cfg(feature = "coconut")]
|
||||
validator_endpoints: Vec<url::Url>,
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
contract: Contract<Http>,
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
erc20_contract: Contract<Http>,
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
eth_private_key: SecretKey,
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
pub fn new(
|
||||
storage: St,
|
||||
eth_endpoint: String,
|
||||
@@ -120,7 +120,7 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
async fn backup_keypair(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> {
|
||||
self.storage
|
||||
.insert_erc20_credential(
|
||||
@@ -132,7 +132,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
async fn restore_keypair(&self) -> Result<identity::KeyPair, GatewayClientError> {
|
||||
let data = self.storage.get_next_erc20_credential().await?;
|
||||
let public_key = identity::PublicKey::from_base58_string(data.public_key).unwrap();
|
||||
@@ -141,7 +141,7 @@ where
|
||||
Ok(identity::KeyPair::from_keys(private_key, public_key))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
async fn mark_keypair_as_spent(
|
||||
&self,
|
||||
keypair: &identity::KeyPair,
|
||||
@@ -180,7 +180,7 @@ where
|
||||
)?)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
pub async fn prepare_token_credential(
|
||||
&self,
|
||||
gateway_identity: identity::PublicKey,
|
||||
@@ -219,7 +219,7 @@ where
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
pub async fn buy_token_credential(
|
||||
&self,
|
||||
verification_key: identity::PublicKey,
|
||||
@@ -348,7 +348,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(feature = "coconut")))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use network_defaults::ETH_EVENT_NAME;
|
||||
|
||||
@@ -30,7 +30,7 @@ use rand::rngs::OsRng;
|
||||
use std::convert::TryFrom;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tungstenite::protocol::Message;
|
||||
use tungstenite::Message;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::connect_async;
|
||||
@@ -449,7 +449,7 @@ impl GatewayClient {
|
||||
.derive_destination_address();
|
||||
let encrypted_address = EncryptedAddressBytes::new(&self_address, shared_key, &iv);
|
||||
|
||||
let msg =
|
||||
let msg: tungstenite::Message =
|
||||
ClientControlRequest::new_authenticate(self_address, encrypted_address, iv).into();
|
||||
|
||||
match self.send_websocket_message(msg).await? {
|
||||
@@ -733,6 +733,8 @@ impl GatewayClient {
|
||||
}
|
||||
|
||||
pub async fn authenticate_and_start(&mut self) -> Result<Arc<SharedKeys>, GatewayClientError> {
|
||||
println!("About to establish connection to {}", self.gateway_address);
|
||||
|
||||
if !self.connection.is_established() {
|
||||
self.establish_connection().await?;
|
||||
}
|
||||
|
||||
@@ -162,12 +162,10 @@ impl PartiallyDelegated {
|
||||
.expect("stream sender was somehow dropped without sending anything!");
|
||||
|
||||
if let Some(res) = receive_res {
|
||||
if let Err(err) = res {
|
||||
// the receiver got an error. most likely a network one.
|
||||
return Err(err);
|
||||
} else {
|
||||
panic!("This should have NEVER happened - returned a stream before receiving notification")
|
||||
}
|
||||
let _res = res?;
|
||||
panic!(
|
||||
"This should have NEVER happened - returned a stream before receiving notification"
|
||||
)
|
||||
}
|
||||
|
||||
// this call failing is incredibly unlikely, but not impossible.
|
||||
|
||||
@@ -24,7 +24,7 @@ use mixnet_contract_common::{
|
||||
PagedMixDelegationsResponse, PagedMixnodeResponse, PagedRewardedSetResponse, QueryMsg,
|
||||
RewardedSetUpdateDetails,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryInto;
|
||||
use std::time::SystemTime;
|
||||
use vesting_contract_common::ExecuteMsg as VestingExecuteMsg;
|
||||
@@ -282,6 +282,30 @@ impl<C> NymdClient<C> {
|
||||
self.simulated_gas_multiplier = multiplier;
|
||||
}
|
||||
|
||||
pub async fn query_contract_smart<M, T>(
|
||||
&self,
|
||||
contract: &AccountId,
|
||||
query_msg: &M,
|
||||
) -> Result<T, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
M: ?Sized + Serialize + Sync,
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
self.client.query_contract_smart(contract, query_msg).await
|
||||
}
|
||||
|
||||
pub async fn query_contract_raw(
|
||||
&self,
|
||||
contract: &AccountId,
|
||||
query_data: Vec<u8>,
|
||||
) -> Result<Vec<u8>, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
self.client.query_contract_raw(contract, query_data).await
|
||||
}
|
||||
|
||||
pub fn wrap_contract_execute_message<M>(
|
||||
&self,
|
||||
contract_address: &AccountId,
|
||||
|
||||
@@ -7,9 +7,11 @@ use crate::nymd::error::NymdError;
|
||||
use crate::nymd::NymdClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp};
|
||||
use mixnet_contract_common::IdentityKey;
|
||||
use vesting_contract::vesting::Account;
|
||||
use vesting_contract_common::{
|
||||
messages::QueryMsg as VestingQueryMsg, OriginalVestingResponse, Period, PledgeData,
|
||||
messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse,
|
||||
OriginalVestingResponse, Period, PledgeData, VestingDelegation,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
@@ -70,6 +72,37 @@ pub trait VestingQueryClient {
|
||||
&self,
|
||||
vesting_account_address: &str,
|
||||
) -> Result<Period, NymdError>;
|
||||
|
||||
async fn get_delegation_timestamps(
|
||||
&self,
|
||||
address: &str,
|
||||
mix_identity: String,
|
||||
) -> Result<DelegationTimesResponse, NymdError>;
|
||||
|
||||
async fn get_all_vesting_delegations_paged(
|
||||
&self,
|
||||
start_after: Option<(u32, IdentityKey, u64)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllDelegationsResponse, NymdError>;
|
||||
|
||||
async fn get_all_vesting_delegations(&self) -> Result<Vec<VestingDelegation>, NymdError> {
|
||||
let mut delegations = Vec::new();
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let mut paged_response = self
|
||||
.get_all_vesting_delegations_paged(start_after.take(), None)
|
||||
.await?;
|
||||
delegations.append(&mut paged_response.delegations);
|
||||
|
||||
if let Some(start_after_res) = paged_response.start_next_after {
|
||||
start_after = Some(start_after_res)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(delegations)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -232,4 +265,29 @@ impl<C: CosmWasmClient + Sync + Send> VestingQueryClient for NymdClient<C> {
|
||||
.query_contract_smart(self.vesting_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_delegation_timestamps(
|
||||
&self,
|
||||
address: &str,
|
||||
mix_identity: String,
|
||||
) -> Result<DelegationTimesResponse, NymdError> {
|
||||
let request = VestingQueryMsg::GetDelegationTimes {
|
||||
address: address.to_string(),
|
||||
mix_identity,
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.vesting_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_vesting_delegations_paged(
|
||||
&self,
|
||||
start_after: Option<(u32, IdentityKey, u64)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllDelegationsResponse, NymdError> {
|
||||
let request = VestingQueryMsg::GetAllDelegations { start_after, limit };
|
||||
self.client
|
||||
.query_contract_smart(self.vesting_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,30 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
Self::default_config_directory(id).join(Self::config_file_name())
|
||||
}
|
||||
|
||||
// We provide a second set of functions that tries to not panic.
|
||||
|
||||
fn try_default_root_directory() -> Option<PathBuf>;
|
||||
|
||||
fn try_default_config_directory(id: Option<&str>) -> Option<PathBuf> {
|
||||
if let Some(id) = id {
|
||||
Self::try_default_root_directory().map(|d| d.join(id).join("config"))
|
||||
} else {
|
||||
Self::try_default_root_directory().map(|d| d.join("config"))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_default_data_directory(id: Option<&str>) -> Option<PathBuf> {
|
||||
if let Some(id) = id {
|
||||
Self::try_default_root_directory().map(|d| d.join(id).join("data"))
|
||||
} else {
|
||||
Self::try_default_root_directory().map(|d| d.join("data"))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_default_config_file_path(id: Option<&str>) -> Option<PathBuf> {
|
||||
Self::try_default_config_directory(id).map(|d| d.join(Self::config_file_name()))
|
||||
}
|
||||
|
||||
fn root_directory(&self) -> PathBuf;
|
||||
fn config_directory(&self) -> PathBuf;
|
||||
fn data_directory(&self) -> PathBuf;
|
||||
|
||||
@@ -217,7 +217,7 @@ pub enum QueryMsg {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {
|
||||
pub mixnet_denom: String,
|
||||
nodes_to_remove: Option<Vec<NodeToRemove>>,
|
||||
pub nodes_to_remove: Option<Vec<NodeToRemove>>,
|
||||
}
|
||||
|
||||
impl MigrateMsg {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
use cosmwasm_std::{Coin, Timestamp};
|
||||
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg};
|
||||
use mixnet_contract_common::IdentityKey;
|
||||
|
||||
pub mod events;
|
||||
pub mod messages;
|
||||
@@ -73,3 +74,35 @@ impl OriginalVestingResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)]
|
||||
pub struct VestingDelegation {
|
||||
pub account_id: u32,
|
||||
pub mix_identity: IdentityKey,
|
||||
pub block_timestamp: u64,
|
||||
pub amount: Uint128,
|
||||
}
|
||||
|
||||
impl VestingDelegation {
|
||||
pub fn storage_key(&self) -> (u32, IdentityKey, u64) {
|
||||
(
|
||||
self.account_id,
|
||||
self.mix_identity.clone(),
|
||||
self.block_timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)]
|
||||
pub struct DelegationTimesResponse {
|
||||
pub owner: Addr,
|
||||
pub account_id: u32,
|
||||
pub mix_identity: IdentityKey,
|
||||
pub delegation_timestamps: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)]
|
||||
pub struct AllDelegationsResponse {
|
||||
pub delegations: Vec<VestingDelegation>,
|
||||
pub start_next_after: Option<(u32, IdentityKey, u64)>,
|
||||
}
|
||||
|
||||
@@ -170,4 +170,12 @@ pub enum QueryMsg {
|
||||
address: String,
|
||||
},
|
||||
GetLockedPledgeCap {},
|
||||
GetDelegationTimes {
|
||||
address: String,
|
||||
mix_identity: IdentityKey,
|
||||
},
|
||||
GetAllDelegations {
|
||||
start_after: Option<(u32, IdentityKey, u64)>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.17"
|
||||
rand = "0.8.5"
|
||||
thiserror = "1.0.32"
|
||||
|
||||
@@ -4,6 +4,8 @@ pub enum Error {
|
||||
EmptyListCumulStake,
|
||||
#[error("Sample point was unexpectedly out of bounds")]
|
||||
SamplePointOutOfBounds,
|
||||
#[error("Norm computation failed on different size arrarys")]
|
||||
#[error("Norm computation failed on different size arrays")]
|
||||
NormDifferenceSizeArrays,
|
||||
#[error("Computed probabilities are fewer than input number of nodes")]
|
||||
ResultsShorterThanInput,
|
||||
}
|
||||
|
||||
@@ -1,26 +1,50 @@
|
||||
//! Active set inclusion probability simulator
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use error::Error;
|
||||
use rand::Rng;
|
||||
|
||||
mod error;
|
||||
|
||||
const TOLERANCE_L2_NORM: f64 = 1e-4;
|
||||
const TOLERANCE_MAX_NORM: f64 = 1e-3;
|
||||
const TOLERANCE_MAX_NORM: f64 = 1e-4;
|
||||
|
||||
pub struct SelectionProbability {
|
||||
pub active_set_probability: Vec<f64>,
|
||||
pub reserve_set_probability: Vec<f64>,
|
||||
pub samples: u32,
|
||||
pub samples: u64,
|
||||
pub time: Duration,
|
||||
pub delta_l2: f64,
|
||||
pub delta_max: f64,
|
||||
}
|
||||
|
||||
pub fn simulate_selection_probability_mixnodes(
|
||||
list_stake_for_mixnodes: &[u64],
|
||||
pub fn simulate_selection_probability_mixnodes<R>(
|
||||
list_stake_for_mixnodes: &[u128],
|
||||
active_set_size: usize,
|
||||
reserve_set_size: usize,
|
||||
max_samples: u32,
|
||||
) -> Result<SelectionProbability, Error> {
|
||||
max_samples: u64,
|
||||
max_time: Duration,
|
||||
rng: &mut R,
|
||||
) -> Result<SelectionProbability, Error>
|
||||
where
|
||||
R: Rng + ?Sized,
|
||||
{
|
||||
log::trace!("Simulating mixnode active set selection probability");
|
||||
|
||||
// In case the active set size is larger than the number of bonded mixnodes, they all have 100%
|
||||
// chance we don't have to go through with the simulation
|
||||
if list_stake_for_mixnodes.len() <= active_set_size {
|
||||
return Ok(SelectionProbability {
|
||||
active_set_probability: vec![1.0; list_stake_for_mixnodes.len()],
|
||||
reserve_set_probability: vec![0.0; list_stake_for_mixnodes.len()],
|
||||
samples: 0,
|
||||
time: Duration::ZERO,
|
||||
delta_l2: 0.0,
|
||||
delta_max: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
// Total number of existing (registered) nodes
|
||||
let num_mixnodes = list_stake_for_mixnodes.len();
|
||||
|
||||
@@ -35,7 +59,9 @@ pub fn simulate_selection_probability_mixnodes(
|
||||
let mut samples = 0;
|
||||
let mut delta_l2;
|
||||
let mut delta_max;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// Make sure we bound the time we allow it to run
|
||||
let start_time = Instant::now();
|
||||
|
||||
loop {
|
||||
samples += 1;
|
||||
@@ -46,8 +72,10 @@ pub fn simulate_selection_probability_mixnodes(
|
||||
let active_set_probability_previous = active_set_probability.clone();
|
||||
|
||||
// Select the active nodes for the epoch (hour)
|
||||
while sample_active_mixnodes.len() < active_set_size {
|
||||
let candidate = sample_candidate(&list_cumul_temp, &mut rng)?;
|
||||
while sample_active_mixnodes.len() < active_set_size
|
||||
&& sample_active_mixnodes.len() < list_cumul_temp.len()
|
||||
{
|
||||
let candidate = sample_candidate(&list_cumul_temp, rng)?;
|
||||
|
||||
if !sample_active_mixnodes.contains(&candidate) {
|
||||
sample_active_mixnodes.push(candidate);
|
||||
@@ -56,8 +84,10 @@ pub fn simulate_selection_probability_mixnodes(
|
||||
}
|
||||
|
||||
// Select the reserve nodes for the epoch (hour)
|
||||
while sample_reserve_mixnodes.len() < reserve_set_size {
|
||||
let candidate = sample_candidate(&list_cumul_temp, &mut rng)?;
|
||||
while sample_reserve_mixnodes.len() < reserve_set_size
|
||||
&& sample_reserve_mixnodes.len() + sample_active_mixnodes.len() < list_cumul_temp.len()
|
||||
{
|
||||
let candidate = sample_candidate(&list_cumul_temp, rng)?;
|
||||
|
||||
if !sample_reserve_mixnodes.contains(&candidate)
|
||||
&& !sample_active_mixnodes.contains(&candidate)
|
||||
@@ -78,35 +108,49 @@ pub fn simulate_selection_probability_mixnodes(
|
||||
// Convergence critera only on active set.
|
||||
// We devide by samples to get the average, that is not really part of the delta
|
||||
// computation.
|
||||
delta_l2 = l2_diff(&active_set_probability, &active_set_probability_previous)?
|
||||
/ f64::from(samples);
|
||||
delta_max = max_diff(&active_set_probability, &active_set_probability_previous)?
|
||||
/ f64::from(samples);
|
||||
delta_l2 =
|
||||
l2_diff(&active_set_probability, &active_set_probability_previous)? / (samples as f64);
|
||||
delta_max =
|
||||
max_diff(&active_set_probability, &active_set_probability_previous)? / (samples as f64);
|
||||
if samples > 10 && delta_l2 < TOLERANCE_L2_NORM && delta_max < TOLERANCE_MAX_NORM
|
||||
|| samples >= max_samples
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Stop if we run out of time
|
||||
if start_time.elapsed() > max_time {
|
||||
log::debug!("Simulation ran out of time, stopping");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Divide occurrences with the number of samples once we're done to get the probabilities.
|
||||
active_set_probability
|
||||
.iter_mut()
|
||||
.for_each(|x| *x /= f64::from(samples));
|
||||
.for_each(|x| *x /= samples as f64);
|
||||
reserve_set_probability
|
||||
.iter_mut()
|
||||
.for_each(|x| *x /= f64::from(samples));
|
||||
.for_each(|x| *x /= samples as f64);
|
||||
|
||||
// Some sanity checks of the output
|
||||
if active_set_probability.len() != num_mixnodes || reserve_set_probability.len() != num_mixnodes
|
||||
{
|
||||
return Err(Error::ResultsShorterThanInput);
|
||||
}
|
||||
|
||||
Ok(SelectionProbability {
|
||||
active_set_probability,
|
||||
reserve_set_probability,
|
||||
samples,
|
||||
time: start_time.elapsed(),
|
||||
delta_l2,
|
||||
delta_max,
|
||||
})
|
||||
}
|
||||
|
||||
// Compute the cumulative sum
|
||||
fn cumul_sum<'a>(list: impl IntoIterator<Item = &'a u64>) -> Vec<u64> {
|
||||
fn cumul_sum<'a>(list: impl IntoIterator<Item = &'a u128>) -> Vec<u128> {
|
||||
let mut list_cumul = Vec::new();
|
||||
let mut cumul = 0;
|
||||
for entry in list {
|
||||
@@ -116,7 +160,10 @@ fn cumul_sum<'a>(list: impl IntoIterator<Item = &'a u64>) -> Vec<u64> {
|
||||
list_cumul
|
||||
}
|
||||
|
||||
fn sample_candidate(list_cumul: &[u64], rng: &mut rand::rngs::ThreadRng) -> Result<usize, Error> {
|
||||
fn sample_candidate<R>(list_cumul: &[u128], rng: &mut R) -> Result<usize, Error>
|
||||
where
|
||||
R: Rng + ?Sized,
|
||||
{
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
let uniform = Uniform::from(0..*list_cumul.last().ok_or(Error::EmptyListCumulStake)?);
|
||||
let r = uniform.sample(rng);
|
||||
@@ -132,7 +179,7 @@ fn sample_candidate(list_cumul: &[u64], rng: &mut rand::rngs::ThreadRng) -> Resu
|
||||
}
|
||||
|
||||
// Update list of cumulative stake to reflect eliminating the picked node
|
||||
fn remove_mixnode_from_cumul_stake(candidate: usize, list_cumul_stake: &mut [u64]) {
|
||||
fn remove_mixnode_from_cumul_stake(candidate: usize, list_cumul_stake: &mut [u128]) {
|
||||
let prob_candidate = if candidate == 0 {
|
||||
list_cumul_stake[0]
|
||||
} else {
|
||||
@@ -171,8 +218,14 @@ fn max_diff(v1: &[f64], v2: &[f64]) -> Result<f64, Error> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_rng() -> StdRng {
|
||||
StdRng::seed_from_u64(42)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_cumul_sum() {
|
||||
let v = cumul_sum(&vec![1, 2, 3]);
|
||||
@@ -212,11 +265,14 @@ mod tests {
|
||||
];
|
||||
|
||||
let max_samples = 100_000;
|
||||
let max_time = Duration::from_secs(10);
|
||||
let mut rng = test_rng();
|
||||
|
||||
let SelectionProbability {
|
||||
active_set_probability,
|
||||
reserve_set_probability,
|
||||
samples,
|
||||
time,
|
||||
delta_l2,
|
||||
delta_max,
|
||||
} = simulate_selection_probability_mixnodes(
|
||||
@@ -224,9 +280,15 @@ mod tests {
|
||||
active_set_size,
|
||||
standby_set_size,
|
||||
max_samples,
|
||||
max_time,
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that any possible test failure wasn't because we ran it on 1970s hardware, and the
|
||||
// sampling aborted prematurely due to hitting `max_time`.
|
||||
assert!(time < max_time);
|
||||
|
||||
// These values comes from running the python simulator for a very long time
|
||||
let expected_active_set_probability = vec![
|
||||
0.025_070_8,
|
||||
@@ -271,7 +333,93 @@ mod tests {
|
||||
);
|
||||
|
||||
// We converge around 20_000, add another 500 for some slack due to random values
|
||||
assert!(samples < 20_500);
|
||||
assert_eq!(samples, 20_001);
|
||||
assert!(delta_l2 < TOLERANCE_L2_NORM);
|
||||
assert!(delta_max < TOLERANCE_MAX_NORM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fewer_nodes_than_active_set_size() {
|
||||
let active_set_size = 10;
|
||||
let standby_set_size = 3;
|
||||
let list_mix = vec![100, 100, 3000];
|
||||
let max_samples = 100_000;
|
||||
let max_time = Duration::from_secs(10);
|
||||
let mut rng = test_rng();
|
||||
|
||||
let SelectionProbability {
|
||||
active_set_probability,
|
||||
reserve_set_probability,
|
||||
samples,
|
||||
time: _,
|
||||
delta_l2,
|
||||
delta_max,
|
||||
} = simulate_selection_probability_mixnodes(
|
||||
&list_mix,
|
||||
active_set_size,
|
||||
standby_set_size,
|
||||
max_samples,
|
||||
max_time,
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// These values comes from running the python simulator for a very long time
|
||||
let expected_active_set_probability = vec![1.0, 1.0, 1.0];
|
||||
let expected_reserve_set_probability = vec![0.0, 0.0, 0.0];
|
||||
assert!(
|
||||
max_diff(&active_set_probability, &expected_active_set_probability).unwrap()
|
||||
< 1e1 * f64::EPSILON
|
||||
);
|
||||
assert!(
|
||||
max_diff(&reserve_set_probability, &expected_reserve_set_probability).unwrap()
|
||||
< 1e1 * f64::EPSILON
|
||||
);
|
||||
|
||||
// We converge around 20_000, add another 500 for some slack due to random values
|
||||
assert_eq!(samples, 0);
|
||||
assert!(delta_l2 < f64::EPSILON);
|
||||
assert!(delta_max < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fewer_nodes_than_reward_set_size() {
|
||||
let active_set_size = 4;
|
||||
let standby_set_size = 3;
|
||||
let list_mix = vec![100, 100, 3000, 342, 3_498_234];
|
||||
let max_samples = 100_000_000;
|
||||
let max_time = Duration::from_secs(10);
|
||||
let mut rng = test_rng();
|
||||
|
||||
let SelectionProbability {
|
||||
active_set_probability,
|
||||
reserve_set_probability,
|
||||
samples,
|
||||
time: _,
|
||||
delta_l2,
|
||||
delta_max,
|
||||
} = simulate_selection_probability_mixnodes(
|
||||
&list_mix,
|
||||
active_set_size,
|
||||
standby_set_size,
|
||||
max_samples,
|
||||
max_time,
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// These values comes from running the python simulator for a very long time
|
||||
let expected_active_set_probability = vec![0.546, 0.538, 0.999, 0.915, 1.0];
|
||||
let expected_reserve_set_probability = vec![0.453, 0.461, 0.0005, 0.084, 0.0];
|
||||
assert!(
|
||||
max_diff(&active_set_probability, &expected_active_set_probability).unwrap() < 1e-2,
|
||||
);
|
||||
assert!(
|
||||
max_diff(&reserve_set_probability, &expected_reserve_set_probability).unwrap() < 1e-2,
|
||||
);
|
||||
|
||||
// We converge around 20_000, add another 500 for some slack due to random values
|
||||
assert_eq!(samples, 20_001);
|
||||
assert!(delta_l2 < TOLERANCE_L2_NORM);
|
||||
assert!(delta_max < TOLERANCE_MAX_NORM);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ pub enum CoconutError {
|
||||
)]
|
||||
DeserializationMinLength { min: usize, actual: usize },
|
||||
|
||||
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {} or {modulus_target} % {modulus} == 0")]
|
||||
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {object} or {modulus_target} % {modulus} == 0")]
|
||||
DeserializationInvalidLength {
|
||||
actual: usize,
|
||||
target: usize,
|
||||
|
||||
@@ -213,7 +213,7 @@ where
|
||||
/// - compute vk_b = g^x || v_b
|
||||
/// - compute sphinx_plaintext = SURB_ACK || g^x || v_b
|
||||
/// - compute sphinx_packet = Sphinx(recipient, sphinx_plaintext)
|
||||
pub async fn prepare_chunk_for_sending(
|
||||
pub fn prepare_chunk_for_sending(
|
||||
&mut self,
|
||||
fragment: Fragment,
|
||||
topology: &NymTopology,
|
||||
@@ -222,8 +222,7 @@ where
|
||||
) -> Result<PreparedFragment, NymTopologyError> {
|
||||
// create an ack
|
||||
let (ack_delay, surb_ack_bytes) = self
|
||||
.generate_surb_ack(fragment.fragment_identifier(), topology, ack_key)
|
||||
.await?
|
||||
.generate_surb_ack(fragment.fragment_identifier(), topology, ack_key)?
|
||||
.prepare_for_sending();
|
||||
|
||||
// TODO:
|
||||
@@ -294,7 +293,7 @@ where
|
||||
}
|
||||
|
||||
/// Construct an acknowledgement SURB for the given [`FragmentIdentifier`]
|
||||
async fn generate_surb_ack(
|
||||
fn generate_surb_ack(
|
||||
&mut self,
|
||||
fragment_id: FragmentIdentifier,
|
||||
topology: &NymTopology,
|
||||
@@ -357,8 +356,7 @@ where
|
||||
// gateways could not distinguish reply packets from normal messages due to lack of said acks
|
||||
// note: the ack delay is irrelevant since we do not know the delay of actual surb
|
||||
let (_, surb_ack_bytes) = self
|
||||
.generate_surb_ack(reply_id, topology, ack_key)
|
||||
.await?
|
||||
.generate_surb_ack(reply_id, topology, ack_key)?
|
||||
.prepare_for_sending();
|
||||
|
||||
let zero_pad_len = self.packet_size.plaintext_size()
|
||||
|
||||
@@ -9,3 +9,4 @@ edition = "2021"
|
||||
[dependencies]
|
||||
nymsphinx-addressing = { path = "../../../common/nymsphinx/addressing" }
|
||||
ordered-buffer = {path = "../ordered-buffer"}
|
||||
thiserror = "1"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod msg;
|
||||
pub mod network_requester_response;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use msg::*;
|
||||
pub use network_requester_response::*;
|
||||
pub use request::*;
|
||||
pub use response::*;
|
||||
|
||||
@@ -1,36 +1,40 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::network_requester_response::{Error as NrError, NetworkRequesterResponse};
|
||||
use crate::request::{Request, RequestError};
|
||||
use crate::response::{Response, ResponseError};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MessageError {
|
||||
#[error("{0}")]
|
||||
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"),
|
||||
}
|
||||
}
|
||||
#[error("{0:?}")]
|
||||
Response(ResponseError),
|
||||
|
||||
#[error("{0}")]
|
||||
NetworkRequesterResponseError(NrError),
|
||||
|
||||
#[error("no data")]
|
||||
NoData,
|
||||
|
||||
#[error("unknown message type received")]
|
||||
UnknownMessageType,
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
Request(Request),
|
||||
Response(Response),
|
||||
NetworkRequesterResponse(NetworkRequesterResponse),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
const REQUEST_FLAG: u8 = 0;
|
||||
const RESPONSE_FLAG: u8 = 1;
|
||||
const NR_RESPONSE_FLAG: u8 = 2;
|
||||
|
||||
pub fn conn_id(&self) -> u64 {
|
||||
match self {
|
||||
@@ -39,6 +43,7 @@ impl Message {
|
||||
Request::Send(conn_id, _, _) => *conn_id,
|
||||
},
|
||||
Message::Response(resp) => resp.connection_id,
|
||||
Message::NetworkRequesterResponse(resp) => resp.connection_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +54,7 @@ impl Message {
|
||||
Request::Send(_, data, _) => data.len(),
|
||||
},
|
||||
Message::Response(resp) => resp.data.len(),
|
||||
Message::NetworkRequesterResponse(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +71,10 @@ impl Message {
|
||||
Response::try_from_bytes(&b[1..])
|
||||
.map(Message::Response)
|
||||
.map_err(MessageError::Response)
|
||||
} else if b[0] == Self::NR_RESPONSE_FLAG {
|
||||
NetworkRequesterResponse::try_from_bytes(&b[1..])
|
||||
.map(Message::NetworkRequesterResponse)
|
||||
.map_err(MessageError::NetworkRequesterResponseError)
|
||||
} else {
|
||||
Err(MessageError::UnknownMessageType)
|
||||
}
|
||||
@@ -78,6 +88,9 @@ impl Message {
|
||||
Self::Response(r) => std::iter::once(Self::RESPONSE_FLAG)
|
||||
.chain(r.into_bytes().iter().cloned())
|
||||
.collect(),
|
||||
Self::NetworkRequesterResponse(r) => std::iter::once(Self::NR_RESPONSE_FLAG)
|
||||
.chain(r.into_bytes().iter().cloned())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::ConnectionId;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NetworkRequesterResponse {
|
||||
pub connection_id: ConnectionId,
|
||||
pub network_requester_error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
#[error("no data provided")]
|
||||
NoData,
|
||||
|
||||
#[error("not enough bytes to recover the connection id")]
|
||||
ConnectionIdTooShort,
|
||||
|
||||
#[error("message is not utf8 encoded")]
|
||||
MalformedErrorMessage(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl NetworkRequesterResponse {
|
||||
pub fn new(connection_id: ConnectionId, network_requester_error: String) -> Self {
|
||||
NetworkRequesterResponse {
|
||||
connection_id,
|
||||
network_requester_error,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_bytes(b: &[u8]) -> Result<NetworkRequesterResponse, Error> {
|
||||
if b.is_empty() {
|
||||
return Err(Error::NoData);
|
||||
}
|
||||
|
||||
if b.len() < 8 {
|
||||
return Err(Error::ConnectionIdTooShort);
|
||||
}
|
||||
|
||||
let mut connection_id_bytes = b.to_vec();
|
||||
let network_requester_error_bytes = connection_id_bytes.split_off(8);
|
||||
|
||||
let connection_id = u64::from_be_bytes([
|
||||
connection_id_bytes[0],
|
||||
connection_id_bytes[1],
|
||||
connection_id_bytes[2],
|
||||
connection_id_bytes[3],
|
||||
connection_id_bytes[4],
|
||||
connection_id_bytes[5],
|
||||
connection_id_bytes[6],
|
||||
connection_id_bytes[7],
|
||||
]);
|
||||
let network_requester_error = String::from_utf8(network_requester_error_bytes)?;
|
||||
|
||||
Ok(NetworkRequesterResponse {
|
||||
connection_id,
|
||||
network_requester_error,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.connection_id
|
||||
.to_be_bytes()
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(self.network_requester_error.into_bytes().into_iter())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod network_requester_response_serde_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple_serde() {
|
||||
let conn_id = 42;
|
||||
let network_requester_error = String::from("This is a test msg");
|
||||
let response = NetworkRequesterResponse::new(conn_id, network_requester_error.clone());
|
||||
let bytes = response.into_bytes();
|
||||
let deserialized_response = NetworkRequesterResponse::try_from_bytes(&bytes).unwrap();
|
||||
|
||||
assert_eq!(conn_id, deserialized_response.connection_id);
|
||||
assert_eq!(
|
||||
network_requester_error,
|
||||
deserialized_response.network_requester_error
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_errors() {
|
||||
let err = NetworkRequesterResponse::try_from_bytes(&[]).err().unwrap();
|
||||
assert_eq!(err, Error::NoData);
|
||||
|
||||
let bytes: [u8; 5] = [1, 2, 3, 4, 5];
|
||||
let err = NetworkRequesterResponse::try_from_bytes(&bytes)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(err, Error::ConnectionIdTooShort);
|
||||
|
||||
let bytes: Vec<u8> = 42u64
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain([0, 159, 146, 150].into_iter())
|
||||
.collect();
|
||||
let err = NetworkRequesterResponse::try_from_bytes(&bytes)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert!(matches!(err, Error::MalformedErrorMessage(_)));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nymsphinx_addressing::clients::{Recipient, RecipientFormattingError};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{self};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type ConnectionId = u64;
|
||||
pub type RemoteAddress = String;
|
||||
@@ -12,39 +15,30 @@ pub enum RequestFlag {
|
||||
Send = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RequestError {
|
||||
#[error("not enough bytes to recover the length of the address")]
|
||||
AddressLengthTooShort,
|
||||
|
||||
#[error("not enough bytes to recover the address")]
|
||||
AddressTooShort,
|
||||
|
||||
#[error("not enough bytes to recover the connection id")]
|
||||
ConnectionIdTooShort,
|
||||
|
||||
#[error("no data provided")]
|
||||
NoData,
|
||||
|
||||
#[error("request of unknown type")]
|
||||
UnknownRequestFlag,
|
||||
|
||||
#[error("too short return address")]
|
||||
ReturnAddressTooShort,
|
||||
|
||||
#[error("malformed return address - {0}")]
|
||||
MalformedReturnAddress(RecipientFormattingError),
|
||||
}
|
||||
|
||||
impl fmt::Display for RequestError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RequestError::AddressLengthTooShort => {
|
||||
write!(f, "not enough bytes to recover the length of the address")
|
||||
}
|
||||
RequestError::AddressTooShort => write!(f, "not enough bytes to recover the address"),
|
||||
RequestError::ConnectionIdTooShort => {
|
||||
write!(f, "not enough bytes to recover the connection id")
|
||||
}
|
||||
RequestError::NoData => write!(f, "no data provided"),
|
||||
RequestError::UnknownRequestFlag => write!(f, "request of unknown type"),
|
||||
RequestError::ReturnAddressTooShort => write!(f, "too short return address"),
|
||||
RequestError::MalformedReturnAddress(recipient_err) => {
|
||||
write!(f, "malformed return address - {}", recipient_err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RequestError {}
|
||||
|
||||
impl RequestError {
|
||||
pub fn is_malformed_return(&self) -> bool {
|
||||
matches!(self, RequestError::MalformedReturnAddress(_))
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ConnectionId;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum ResponseError {
|
||||
#[error("not enough bytes to recover the connection id")]
|
||||
ConnectionIdTooShort,
|
||||
#[error("no data provided")]
|
||||
NoData,
|
||||
}
|
||||
/// A remote network response retrieved by the Socks5 service provider. This
|
||||
|
||||
@@ -5,13 +5,14 @@ use std::time::Duration;
|
||||
|
||||
use tokio::sync::watch::{self, error::SendError};
|
||||
|
||||
const SHUTDOWN_TIMER_SECS: u64 = 5;
|
||||
const DEFAULT_SHUTDOWN_TIMER_SECS: u64 = 5;
|
||||
|
||||
/// Used to notify other tasks to gracefully shutdown
|
||||
#[derive(Debug)]
|
||||
pub struct ShutdownNotifier {
|
||||
notify_tx: watch::Sender<()>,
|
||||
notify_rx: Option<watch::Receiver<()>>,
|
||||
shutdown_timer_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for ShutdownNotifier {
|
||||
@@ -20,11 +21,19 @@ impl Default for ShutdownNotifier {
|
||||
Self {
|
||||
notify_tx,
|
||||
notify_rx: Some(notify_rx),
|
||||
shutdown_timer_secs: DEFAULT_SHUTDOWN_TIMER_SECS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShutdownNotifier {
|
||||
pub fn new(shutdown_timer_secs: u64) -> Self {
|
||||
Self {
|
||||
shutdown_timer_secs,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> ShutdownListener {
|
||||
ShutdownListener::new(
|
||||
self.notify_rx
|
||||
@@ -50,7 +59,7 @@ impl ShutdownNotifier {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
log::info!("Forcing shutdown");
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(SHUTDOWN_TIMER_SECS)) => {
|
||||
_ = tokio::time::sleep(Duration::from_secs(self.shutdown_timer_secs)) => {
|
||||
log::info!("Timout reached, forcing shutdown");
|
||||
},
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ impl Node {
|
||||
}
|
||||
|
||||
pub fn clients_address(&self) -> String {
|
||||
format!("ws://{}:{}", self.host, self.clients_port)
|
||||
format!("wss://{}:{}", self.host, self.clients_port)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ wasm-bindgen-futures = "0.4"
|
||||
|
||||
# we don't want entire tokio-tungstenite, tungstenite itself is just fine - we just want message and error enums
|
||||
[dependencies.tungstenite]
|
||||
version = "0.13"
|
||||
version = "0.17.3"
|
||||
default-features = false
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@@ -85,6 +85,11 @@ pub struct JSWebsocket {
|
||||
|
||||
impl JSWebsocket {
|
||||
pub fn new(url: &str) -> Result<Self, JsValue> {
|
||||
console_log!(
|
||||
"Attempting to establish wasm websocket connection to {}",
|
||||
url
|
||||
);
|
||||
|
||||
let ws = WebSocket::new(url)?;
|
||||
// we don't want to ever have to deal with blobs
|
||||
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- vesting-contract: added queries for delegation timestamps and paged query for all vesting delegations in the contract ([#1569])
|
||||
|
||||
### Changed
|
||||
|
||||
- mixnet-contract: compounding delegator rewards now happens instantaneously as opposed to having to wait for the current epoch to finish ([#1571])
|
||||
|
||||
### Fixed
|
||||
|
||||
- vesting-contract: the contract now correctly stores delegations with their timestamp as opposed to using block height ([#1544])
|
||||
- mixnet-contract: compounding delegator rewards is now possible even if the associated mixnode had already unbonded ([#1571])
|
||||
|
||||
[#1544]: https://github.com/nymtech/nym/pull/1544
|
||||
[#1569]: https://github.com/nymtech/nym/pull/1569
|
||||
[#1569]: https://github.com/nymtech/nym/pull/1571
|
||||
|
||||
## [nym-contracts-v1.0.1](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.1) (2022-06-22)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -8,7 +8,6 @@ use super::storage::{
|
||||
use crate::constants;
|
||||
use crate::contract::debug_with_visibility;
|
||||
use crate::delegations::storage as delegations_storage;
|
||||
use crate::delegations::transactions::_try_delegate_to_mixnode;
|
||||
use crate::error::ContractError;
|
||||
use crate::mixnet_contract_settings::storage::mix_denom;
|
||||
use crate::mixnodes::storage::mixnodes;
|
||||
@@ -18,7 +17,7 @@ use crate::rewards::helpers;
|
||||
use crate::support::helpers::{is_authorized, operator_cost_at_epoch};
|
||||
use cosmwasm_std::{
|
||||
coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Order, Response,
|
||||
Storage, Uint128,
|
||||
StdResult, Storage, Uint128,
|
||||
};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract_common::events::{
|
||||
@@ -450,18 +449,15 @@ pub fn try_compound_delegator_reward(
|
||||
|
||||
pub fn _try_compound_delegator_reward(
|
||||
block_height: u64,
|
||||
mut deps: DepsMut<'_>,
|
||||
deps: DepsMut<'_>,
|
||||
owner_address: &str,
|
||||
mix_identity: &str,
|
||||
proxy: Option<Addr>,
|
||||
) -> Result<Uint128, ContractError> {
|
||||
let delegation_map = crate::delegations::storage::delegations();
|
||||
let mix_denom = mix_denom(deps.storage)?;
|
||||
|
||||
let key = mixnet_contract_common::delegation::generate_storage_key(
|
||||
&deps.api.addr_validate(owner_address)?,
|
||||
proxy.as_ref(),
|
||||
);
|
||||
let owner = deps.api.addr_validate(owner_address)?;
|
||||
let key = mixnet_contract_common::delegation::generate_storage_key(&owner, proxy.as_ref());
|
||||
let reward = calculate_delegator_reward(deps.storage, deps.api, key.clone(), mix_identity)?;
|
||||
let mut total_delegation_delegate = Uint128::zero();
|
||||
|
||||
@@ -469,8 +465,7 @@ pub fn _try_compound_delegator_reward(
|
||||
let delegation_heights = delegation_map
|
||||
.prefix((mix_identity.to_string(), key.clone()))
|
||||
.keys(deps.storage, None, None, cosmwasm_std::Order::Ascending)
|
||||
.filter_map(|v| v.ok())
|
||||
.collect::<Vec<u64>>();
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
for h in delegation_heights {
|
||||
let delegation =
|
||||
@@ -494,30 +489,36 @@ pub fn _try_compound_delegator_reward(
|
||||
// since we know that the target node exists and because the total_delegation bucket
|
||||
// entry is created whenever the node itself is added, the unwrap here is fine
|
||||
// as the entry MUST exist
|
||||
Ok(total_delegation
|
||||
.unwrap()
|
||||
.saturating_sub(total_delegation_delegate))
|
||||
Ok(total_delegation.unwrap() + reward)
|
||||
},
|
||||
)?;
|
||||
|
||||
_try_delegate_to_mixnode(
|
||||
deps.branch(),
|
||||
block_height,
|
||||
mix_identity,
|
||||
owner_address,
|
||||
// let's simplify the entire procedure. Rather than creating a fresh delegation on the mixnode
|
||||
// via `_try_delegate_to_mixnode` and then waiting for reconcile to happen,
|
||||
// just save it directly to the storage right now.
|
||||
// my reasoning for that is simple: `_try_delegate_to_mixnode` could fail if the node the
|
||||
// delegator has delegated to no longer exists.
|
||||
let delegation = Delegation::new(
|
||||
owner,
|
||||
mix_identity.into(),
|
||||
Coin {
|
||||
amount: compounded_delegation,
|
||||
denom: mix_denom,
|
||||
},
|
||||
block_height,
|
||||
proxy,
|
||||
);
|
||||
|
||||
delegation_map.save(
|
||||
deps.storage,
|
||||
(mix_identity.into(), key.clone(), block_height),
|
||||
&delegation,
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
if let Some(mut bond) = mixnodes().may_load(deps.storage, mix_identity)? {
|
||||
bond.accumulated_rewards = Some(bond.accumulated_rewards().saturating_sub(reward));
|
||||
mixnodes().save(deps.storage, mix_identity, &bond, block_height)?;
|
||||
}
|
||||
if let Some(mut bond) = mixnodes().may_load(deps.storage, mix_identity)? {
|
||||
bond.accumulated_rewards = Some(bond.accumulated_rewards().saturating_sub(reward));
|
||||
mixnodes().save(deps.storage, mix_identity, &bond, block_height)?;
|
||||
}
|
||||
|
||||
DELEGATOR_REWARD_CLAIMED_HEIGHT.save(
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
use crate::errors::ContractError;
|
||||
use crate::queued_migrations::migrate_config_from_env;
|
||||
use crate::storage::{
|
||||
account_from_address, locked_pledge_cap, update_locked_pledge_cap, ADMIN,
|
||||
MIXNET_CONTRACT_ADDRESS, MIX_DENOM,
|
||||
account_from_address, locked_pledge_cap, update_locked_pledge_cap, BlockTimestampSecs, ADMIN,
|
||||
DELEGATIONS, MIXNET_CONTRACT_ADDRESS, MIX_DENOM,
|
||||
};
|
||||
use crate::traits::{
|
||||
DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount,
|
||||
};
|
||||
use crate::vesting::{populate_vesting_periods, Account};
|
||||
use cosmwasm_std::{
|
||||
coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
|
||||
Response, Timestamp, Uint128,
|
||||
coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order,
|
||||
QueryResponse, Response, StdResult, Timestamp, Uint128,
|
||||
};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract_common::{Gateway, IdentityKey, MixNode};
|
||||
use vesting_contract_common::events::{
|
||||
new_ownership_transfer_event, new_periodic_vesting_account_event,
|
||||
@@ -22,7 +23,10 @@ use vesting_contract_common::events::{
|
||||
use vesting_contract_common::messages::{
|
||||
ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification,
|
||||
};
|
||||
use vesting_contract_common::{OriginalVestingResponse, Period, PledgeData};
|
||||
use vesting_contract_common::{
|
||||
AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData,
|
||||
VestingDelegation,
|
||||
};
|
||||
|
||||
pub const INITIAL_LOCKED_PLEDGE_CAP: Uint128 = Uint128::new(100_000_000_000);
|
||||
|
||||
@@ -518,6 +522,13 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
|
||||
QueryMsg::GetCurrentVestingPeriod { address } => {
|
||||
to_binary(&try_get_current_vesting_period(&address, deps, env)?)
|
||||
}
|
||||
QueryMsg::GetDelegationTimes {
|
||||
address,
|
||||
mix_identity,
|
||||
} => to_binary(&try_get_delegation_times(deps, &address, mix_identity)?),
|
||||
QueryMsg::GetAllDelegations { start_after, limit } => {
|
||||
to_binary(&try_get_all_delegations(deps, start_after, limit)?)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(query_res?)
|
||||
@@ -634,6 +645,63 @@ pub fn try_get_delegated_vesting(
|
||||
account.get_delegated_vesting(block_time, &env, deps.storage)
|
||||
}
|
||||
|
||||
pub fn try_get_delegation_times(
|
||||
deps: Deps<'_>,
|
||||
vesting_account_address: &str,
|
||||
mix_identity: String,
|
||||
) -> Result<DelegationTimesResponse, ContractError> {
|
||||
let owner = deps.api.addr_validate(vesting_account_address)?;
|
||||
let account = account_from_address(vesting_account_address, deps.storage, deps.api)?;
|
||||
|
||||
let delegation_timestamps = DELEGATIONS
|
||||
.prefix((account.storage_key(), mix_identity.clone()))
|
||||
.keys(deps.storage, None, None, Order::Ascending)
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
Ok(DelegationTimesResponse {
|
||||
owner,
|
||||
account_id: account.storage_key(),
|
||||
mix_identity,
|
||||
delegation_timestamps,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_get_all_delegations(
|
||||
deps: Deps<'_>,
|
||||
start_after: Option<(u32, IdentityKey, BlockTimestampSecs)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AllDelegationsResponse, ContractError> {
|
||||
let limit = limit.unwrap_or(100).min(200) as usize;
|
||||
|
||||
let start = start_after.map(Bound::exclusive);
|
||||
let delegations = DELEGATIONS
|
||||
.range(deps.storage, start, None, Order::Ascending)
|
||||
.map(|kv| {
|
||||
kv.map(
|
||||
|((account_id, mix_identity, block_timestamp), amount)| VestingDelegation {
|
||||
account_id,
|
||||
mix_identity,
|
||||
block_timestamp,
|
||||
amount,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = if delegations.len() < limit {
|
||||
None
|
||||
} else {
|
||||
delegations
|
||||
.last()
|
||||
.map(|delegation| delegation.storage_key())
|
||||
};
|
||||
|
||||
Ok(AllDelegationsResponse {
|
||||
delegations,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_funds(funds: &[Coin], mix_denom: String) -> Result<Coin, ContractError> {
|
||||
if funds.is_empty() || funds[0].amount.is_zero() {
|
||||
return Err(ContractError::EmptyFunds);
|
||||
|
||||
@@ -5,7 +5,7 @@ use cw_storage_plus::{Item, Map};
|
||||
use mixnet_contract_common::IdentityKey;
|
||||
use vesting_contract_common::PledgeData;
|
||||
|
||||
type BlockHeight = u64;
|
||||
pub(crate) type BlockTimestampSecs = u64;
|
||||
|
||||
pub const KEY: Item<'_, u32> = Item::new("key");
|
||||
const ACCOUNTS: Map<'_, String, Account> = Map::new("acc");
|
||||
@@ -14,7 +14,7 @@ const BALANCES: Map<'_, u32, Uint128> = Map::new("blc");
|
||||
const WITHDRAWNS: Map<'_, u32, Uint128> = Map::new("wthd");
|
||||
const BOND_PLEDGES: Map<'_, u32, PledgeData> = Map::new("bnd");
|
||||
const GATEWAY_PLEDGES: Map<'_, u32, PledgeData> = Map::new("gtw");
|
||||
pub const DELEGATIONS: Map<'_, (u32, IdentityKey, BlockHeight), Uint128> = Map::new("dlg");
|
||||
pub const DELEGATIONS: Map<'_, (u32, IdentityKey, BlockTimestampSecs), Uint128> = Map::new("dlg");
|
||||
pub const ADMIN: Item<'_, String> = Item::new("adm");
|
||||
pub const MIXNET_CONTRACT_ADDRESS: Item<'_, String> = Item::new("mix");
|
||||
pub const MIX_DENOM: Item<'_, String> = Item::new("den");
|
||||
@@ -35,7 +35,7 @@ pub fn update_locked_pledge_cap(
|
||||
}
|
||||
|
||||
pub fn save_delegation(
|
||||
key: (u32, IdentityKey, BlockHeight),
|
||||
key: (u32, IdentityKey, BlockTimestampSecs),
|
||||
amount: Uint128,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
@@ -44,7 +44,7 @@ pub fn save_delegation(
|
||||
}
|
||||
|
||||
pub fn remove_delegation(
|
||||
key: (u32, IdentityKey, BlockHeight),
|
||||
key: (u32, IdentityKey, BlockTimestampSecs),
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
DELEGATIONS.remove(storage, key);
|
||||
|
||||
@@ -88,7 +88,7 @@ impl DelegatingAccount for Account {
|
||||
vec![coin.clone()],
|
||||
)?;
|
||||
self.track_delegation(
|
||||
env.block.height,
|
||||
env.block.time.seconds(),
|
||||
mix_identity,
|
||||
current_balance,
|
||||
coin,
|
||||
@@ -129,14 +129,14 @@ impl DelegatingAccount for Account {
|
||||
|
||||
fn track_delegation(
|
||||
&self,
|
||||
block_height: u64,
|
||||
block_timestamp_secs: u64,
|
||||
mix_identity: IdentityKey,
|
||||
current_balance: Uint128,
|
||||
delegation: Coin,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
save_delegation(
|
||||
(self.storage_key(), mix_identity, block_height),
|
||||
(self.storage_key(), mix_identity, block_timestamp_secs),
|
||||
delegation.amount,
|
||||
storage,
|
||||
)?;
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::errors::ContractError;
|
||||
use crate::storage::{
|
||||
load_balance, load_bond_pledge, load_gateway_pledge, load_withdrawn, remove_bond_pledge,
|
||||
remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge,
|
||||
save_gateway_pledge, save_withdrawn, DELEGATIONS, KEY,
|
||||
save_gateway_pledge, save_withdrawn, BlockTimestampSecs, DELEGATIONS, KEY,
|
||||
};
|
||||
use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128};
|
||||
use cw_storage_plus::Bound;
|
||||
@@ -261,4 +261,19 @@ impl Account {
|
||||
.filter_map(|x| x.ok())
|
||||
.fold(Uint128::zero(), |acc, (_key, val)| acc + val))
|
||||
}
|
||||
|
||||
pub fn total_delegations_at_timestamp(
|
||||
&self,
|
||||
storage: &dyn Storage,
|
||||
start_time: BlockTimestampSecs,
|
||||
) -> Result<Uint128, ContractError> {
|
||||
Ok(DELEGATIONS
|
||||
.sub_prefix(self.storage_key())
|
||||
.range(storage, None, None, Order::Ascending)
|
||||
.filter_map(|x| x.ok())
|
||||
.filter(|((_mix, block_time), _amount)| *block_time <= start_time)
|
||||
.fold(Uint128::zero(), |acc, ((_mix, _block_time), amount)| {
|
||||
acc + amount
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::errors::ContractError;
|
||||
use crate::storage::{delete_account, save_account, DELEGATIONS, MIX_DENOM};
|
||||
use crate::storage::{delete_account, save_account, MIX_DENOM};
|
||||
use crate::traits::VestingAccount;
|
||||
use cosmwasm_std::{Addr, Coin, Env, Order, Storage, Timestamp, Uint128};
|
||||
use cosmwasm_std::{Addr, Coin, Env, Storage, Timestamp, Uint128};
|
||||
use vesting_contract_common::{OriginalVestingResponse, Period};
|
||||
|
||||
use super::Account;
|
||||
@@ -16,13 +16,6 @@ impl VestingAccount for Account {
|
||||
+ self.get_pledged_vesting(None, env, storage)?.amount)
|
||||
}
|
||||
|
||||
fn track_reward(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
let current_balance = self.load_balance(storage)?;
|
||||
let new_balance = current_balance + amount.amount;
|
||||
self.save_balance(new_balance, storage)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn locked_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
@@ -141,14 +134,7 @@ impl VestingAccount for Account {
|
||||
Period::In(idx) => self.periods[idx as usize].start_time,
|
||||
};
|
||||
|
||||
let coin = DELEGATIONS
|
||||
.sub_prefix(self.storage_key())
|
||||
.range(storage, None, None, Order::Ascending)
|
||||
.filter_map(|x| x.ok())
|
||||
.filter(|((_mix, block_time), _amount)| *block_time < start_time)
|
||||
.fold(Uint128::zero(), |acc, ((_mix, _block_time), amount)| {
|
||||
acc + amount
|
||||
});
|
||||
let coin = self.total_delegations_at_timestamp(storage, start_time)?;
|
||||
|
||||
let amount = Uint128::new(coin.u128().min(max_available.u128()));
|
||||
|
||||
@@ -158,6 +144,7 @@ impl VestingAccount for Account {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: why do we allow querying for block times in the past? - just use env.block.time all the time
|
||||
fn get_delegated_vesting(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
@@ -166,9 +153,18 @@ impl VestingAccount for Account {
|
||||
) -> Result<Coin, ContractError> {
|
||||
let block_time = block_time.unwrap_or(env.block.time);
|
||||
let delegated_free = self.get_delegated_free(Some(block_time), env, storage)?;
|
||||
let total_delegations = self.total_delegations(storage)?;
|
||||
|
||||
let amount = total_delegations - delegated_free.amount;
|
||||
let period = self.get_current_vesting_period(block_time);
|
||||
let start_time = match period {
|
||||
Period::Before => 0,
|
||||
Period::After => u64::MAX,
|
||||
Period::In(idx) => self.periods[idx as usize].start_time,
|
||||
};
|
||||
|
||||
let delegations_before_start_time =
|
||||
self.total_delegations_at_timestamp(storage, start_time)?;
|
||||
|
||||
let amount = delegations_before_start_time - delegated_free.amount;
|
||||
|
||||
Ok(Coin {
|
||||
amount,
|
||||
@@ -261,4 +257,11 @@ impl VestingAccount for Account {
|
||||
save_account(self, storage)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn track_reward(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
let current_balance = self.load_balance(storage)?;
|
||||
let new_balance = current_balance + amount.amount;
|
||||
self.save_balance(new_balance, storage)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,11 @@ mod tests {
|
||||
use crate::traits::DelegatingAccount;
|
||||
use crate::traits::VestingAccount;
|
||||
use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount};
|
||||
use crate::vesting::{populate_vesting_periods, Account};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{coins, Addr, Coin, Timestamp, Uint128};
|
||||
use mixnet_contract_common::{Gateway, MixNode};
|
||||
use vesting_contract_common::messages::ExecuteMsg;
|
||||
use vesting_contract_common::messages::{ExecuteMsg, VestingSpecification};
|
||||
use vesting_contract_common::Period;
|
||||
|
||||
#[test]
|
||||
@@ -757,4 +758,171 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(Uint128::zero(), bonded_vesting.amount);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegated_free() {
|
||||
let mut deps = init_contract();
|
||||
let mut env = mock_env();
|
||||
|
||||
let vesting_period_length_secs = 3600;
|
||||
|
||||
let account_creation_timestamp = 1650000000;
|
||||
let account_creation_blockheight = 12345;
|
||||
|
||||
// this value is completely arbitrary, I just wanted to keep consistent
|
||||
// (and make sure that if block timestamp increases so does the block height)
|
||||
let blocks_per_period = 100;
|
||||
|
||||
env.block.height = account_creation_blockheight;
|
||||
env.block.time = Timestamp::from_seconds(account_creation_timestamp);
|
||||
|
||||
// lets define some helper timestamps
|
||||
|
||||
// our account is set to be created after 2 vesting periods already passed
|
||||
let vesting_start_blockheight = account_creation_blockheight - 2 * blocks_per_period;
|
||||
let vesting_start_timestamp = account_creation_timestamp - 2 * vesting_period_length_secs;
|
||||
|
||||
let vesting_period2_start_blockheight = vesting_start_blockheight + blocks_per_period;
|
||||
let vesting_period2_start_timestamp = vesting_start_timestamp + vesting_period_length_secs;
|
||||
|
||||
// this vesting period is currently in progress!
|
||||
let vesting_period3_start_blockheight =
|
||||
vesting_period2_start_blockheight + blocks_per_period;
|
||||
let vesting_period3_start_timestamp =
|
||||
vesting_period2_start_timestamp + vesting_period_length_secs;
|
||||
|
||||
// and this one is in the future! (in relation to account creation)
|
||||
let vesting_period4_start_blockheight =
|
||||
vesting_period3_start_blockheight + blocks_per_period;
|
||||
let vesting_period4_start_timestamp =
|
||||
vesting_period3_start_timestamp + vesting_period_length_secs;
|
||||
|
||||
// lets create our vesting account
|
||||
let periods = populate_vesting_periods(
|
||||
vesting_start_timestamp,
|
||||
VestingSpecification::new(None, Some(vesting_period_length_secs), None),
|
||||
);
|
||||
|
||||
let vesting_account = Account::new(
|
||||
Addr::unchecked("owner"),
|
||||
Some(Addr::unchecked("staking")),
|
||||
Coin {
|
||||
amount: Uint128::new(1_000_000_000_000),
|
||||
denom: TEST_COIN_DENOM.to_string(),
|
||||
},
|
||||
Timestamp::from_seconds(account_creation_timestamp),
|
||||
periods,
|
||||
deps.as_mut().storage,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// time for some delegations
|
||||
|
||||
let mix_identity = "alice".to_string();
|
||||
|
||||
let delegation = Coin {
|
||||
amount: Uint128::new(90_000_000_000),
|
||||
denom: TEST_COIN_DENOM.to_string(),
|
||||
};
|
||||
|
||||
// delegate explicitly at the time the account was created
|
||||
// (i.e. after 2 vesting periods already elapsed)
|
||||
env.block.height = account_creation_blockheight;
|
||||
env.block.time = Timestamp::from_seconds(account_creation_timestamp);
|
||||
let ok = vesting_account.try_delegate_to_mixnode(
|
||||
mix_identity.clone(),
|
||||
delegation.clone(),
|
||||
&env,
|
||||
&mut deps.storage,
|
||||
);
|
||||
assert!(ok.is_ok());
|
||||
|
||||
let vested_coins = vesting_account
|
||||
.get_vested_coins(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
let vesting_coins = vesting_account
|
||||
.get_vesting_coins(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
assert_eq!(vested_coins.amount, Uint128::new(250_000_000_000));
|
||||
assert_eq!(vesting_coins.amount, Uint128::new(750_000_000_000));
|
||||
let delegated_free = vesting_account
|
||||
.get_delegated_free(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
let delegated_vesting = vesting_account
|
||||
.get_delegated_vesting(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
|
||||
// all good so far
|
||||
assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000));
|
||||
assert_eq!(delegated_vesting.amount, Uint128::zero());
|
||||
|
||||
// some time passes, and we're now into the next vesting period, more of our coins got unlocked!
|
||||
env.block.height = vesting_period4_start_blockheight;
|
||||
env.block.time = Timestamp::from_seconds(vesting_period4_start_timestamp);
|
||||
|
||||
let vested_coins = vesting_account
|
||||
.get_vested_coins(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
let vesting_coins = vesting_account
|
||||
.get_vesting_coins(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
assert_eq!(vested_coins.amount, Uint128::new(375_000_000_000));
|
||||
assert_eq!(vesting_coins.amount, Uint128::new(625_000_000_000));
|
||||
|
||||
// and nothing about our existing delegation changed
|
||||
let delegated_free = vesting_account
|
||||
.get_delegated_free(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
let delegated_vesting = vesting_account
|
||||
.get_delegated_vesting(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000));
|
||||
assert_eq!(delegated_vesting.amount, Uint128::zero());
|
||||
|
||||
// however, create a new delegation now in this brand new vesting period
|
||||
let delegation = Coin {
|
||||
amount: Uint128::new(50_000_000_000),
|
||||
denom: TEST_COIN_DENOM.to_string(),
|
||||
};
|
||||
let ok = vesting_account.try_delegate_to_mixnode(
|
||||
mix_identity.clone(),
|
||||
delegation.clone(),
|
||||
&env,
|
||||
&mut deps.storage,
|
||||
);
|
||||
assert!(ok.is_ok());
|
||||
|
||||
// we're still good here, we have delegated in total 140M from our vested tokens!
|
||||
let delegated_free = vesting_account
|
||||
.get_delegated_free(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
let delegated_vesting = vesting_account
|
||||
.get_delegated_vesting(None, &env, &deps.storage)
|
||||
.unwrap();
|
||||
assert_eq!(delegated_free.amount, Uint128::new(140_000_000_000));
|
||||
assert_eq!(delegated_vesting.amount, Uint128::zero());
|
||||
|
||||
// but let's ask now a different question:
|
||||
// how many vested tokens have I had delegated during vesting period3? (i.e. after account creation)
|
||||
let delegated_free = vesting_account
|
||||
.get_delegated_free(
|
||||
Some(Timestamp::from_seconds(vesting_period3_start_timestamp)),
|
||||
&env,
|
||||
&deps.storage,
|
||||
)
|
||||
.unwrap();
|
||||
let delegated_vesting = vesting_account
|
||||
.get_delegated_vesting(
|
||||
Some(Timestamp::from_seconds(vesting_period3_start_timestamp)),
|
||||
&env,
|
||||
&deps.storage,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// returns 90M as the 50M delegation didn't exist at this point of time
|
||||
assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000));
|
||||
|
||||
// the 50M delegation wasn't a thing here for VESTING tokens either
|
||||
assert_eq!(delegated_vesting.amount, Uint128::zero());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
|
||||
thiserror = "1"
|
||||
tokio = { version = "1.19.1", features = [ "rt-multi-thread", "net", "signal", "fs" ] }
|
||||
tokio-stream = { version = "0.1.9", features = [ "fs" ] }
|
||||
tokio-tungstenite = "0.14"
|
||||
tokio-tungstenite = "0.17"
|
||||
tokio-util = { version = "0.7.3", features = [ "codec" ] }
|
||||
url = { version = "2.2", features = [ "serde" ] }
|
||||
web3 = "0.17.0"
|
||||
|
||||
@@ -29,6 +29,6 @@ credentials = { path = "../../common/credentials" }
|
||||
coconut = ["coconut-interface", "credentials/coconut"]
|
||||
|
||||
[dependencies.tungstenite]
|
||||
version = "0.13.0"
|
||||
version = "0.17.3"
|
||||
default-features = false
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ pub use self::shared_key::{SharedKeySize, SharedKeys};
|
||||
use crypto::asymmetric::identity;
|
||||
use futures::{Sink, Stream};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use tungstenite::{Error as WsError, Message as WsMessage};
|
||||
use tungstenite::error::Error as WsError;
|
||||
use tungstenite::protocol::Message as WsMessage;
|
||||
|
||||
pub(crate) type WsItem = Result<WsMessage, WsError>;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt::{self, Error, Formatter},
|
||||
};
|
||||
use tungstenite::protocol::Message;
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::Credential;
|
||||
@@ -192,12 +191,12 @@ impl ClientControlRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientControlRequest> for Message {
|
||||
impl From<ClientControlRequest> for tungstenite::Message {
|
||||
fn from(req: ClientControlRequest) -> Self {
|
||||
// it should be safe to call `unwrap` here as the message is generated by the server
|
||||
// so if it fails (and consequently panics) it's a bug that should be resolved
|
||||
let str_req = serde_json::to_string(&req).unwrap();
|
||||
Message::Text(str_req)
|
||||
tungstenite::Message::Text(str_req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,12 +257,12 @@ impl ServerResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerResponse> for Message {
|
||||
impl From<ServerResponse> for tungstenite::Message {
|
||||
fn from(res: ServerResponse) -> Self {
|
||||
// it should be safe to call `unwrap` here as the message is generated by the server
|
||||
// so if it fails (and consequently panics) it's a bug that should be resolved
|
||||
let str_res = serde_json::to_string(&res).unwrap();
|
||||
Message::Text(str_res)
|
||||
tungstenite::Message::Text(str_res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,8 +313,8 @@ impl BinaryRequest {
|
||||
BinaryRequest::ForwardSphinx(mix_packet)
|
||||
}
|
||||
|
||||
pub fn into_ws_message(self, shared_key: &SharedKeys) -> Message {
|
||||
Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
|
||||
pub fn into_ws_message(self, shared_key: &SharedKeys) -> tungstenite::Message {
|
||||
tungstenite::Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,8 +366,8 @@ impl BinaryResponse {
|
||||
BinaryResponse::PushedMixMessage(msg)
|
||||
}
|
||||
|
||||
pub fn into_ws_message(self, shared_key: &SharedKeys) -> Message {
|
||||
Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
|
||||
pub fn into_ws_message(self, shared_key: &SharedKeys) -> tungstenite::Message {
|
||||
tungstenite::Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct Init {
|
||||
mnemonic: Option<String>,
|
||||
|
||||
/// Set this gateway to work in a enabled credentials mode that would disallow clients to bypass bandwidth credential requirement
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
#[clap(long)]
|
||||
enabled_credentials_mode: Option<bool>,
|
||||
|
||||
@@ -83,7 +83,7 @@ impl From<Init> for OverrideConfig {
|
||||
validators: init_config.validators,
|
||||
mnemonic: init_config.mnemonic,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: init_config.enabled_credentials_mode,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
@@ -177,7 +177,7 @@ mod tests {
|
||||
mnemonic: None,
|
||||
statistics_service_url: None,
|
||||
enabled_statistics: None,
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: None,
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
eth_endpoint: "".to_string(),
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) struct OverrideConfig {
|
||||
validators: Option<String>,
|
||||
mnemonic: Option<String>,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: Option<bool>,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
@@ -118,8 +118,8 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
|
||||
config = config.with_custom_validator_apis(parse_validators(&raw_validators))
|
||||
}
|
||||
|
||||
if let Some(raw_validators) = args.validators {
|
||||
config = config.with_custom_validator_nymd(parse_validators(&raw_validators));
|
||||
if let Some(ref raw_validators) = args.validators {
|
||||
config = config.with_custom_validator_nymd(parse_validators(raw_validators));
|
||||
} else if std::env::var(CONFIGURED).is_ok() {
|
||||
let raw_validators = std::env::var(NYMD_VALIDATOR).expect("nymd validator not set");
|
||||
config = config.with_custom_validator_nymd(parse_validators(&raw_validators))
|
||||
@@ -146,18 +146,15 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
|
||||
config = config.with_eth_endpoint(String::from(DEFAULT_ETH_ENDPOINT));
|
||||
}
|
||||
|
||||
// We set the disabled credentials mode flag if we either compile without 'eth', or if there is a flag we
|
||||
// can read from, which is when we build with 'eth' (and without 'coconut').
|
||||
if cfg!(not(feature = "eth")) {
|
||||
config = config.with_disabled_credentials_mode(true);
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
{
|
||||
if let Some(enabled_credentials_mode) = args.enabled_credentials_mode {
|
||||
config = config.with_disabled_credentials_mode(!enabled_credentials_mode);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
{
|
||||
if let Some(enabled_credentials_mode) = args.enabled_credentials_mode {
|
||||
config = config.with_disabled_credentials_mode(enabled_credentials_mode);
|
||||
}
|
||||
|
||||
if let Some(raw_validators) = args.validators {
|
||||
config = config.with_custom_validator_nymd(parse_validators(&raw_validators));
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct Run {
|
||||
mnemonic: Option<String>,
|
||||
|
||||
/// Set this gateway to work in a enabled credentials mode that would disallow clients to bypass bandwidth credential requirement
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
#[clap(long)]
|
||||
enabled_credentials_mode: Option<bool>,
|
||||
|
||||
@@ -83,7 +83,7 @@ impl From<Run> for OverrideConfig {
|
||||
validators: run_config.validators,
|
||||
mnemonic: run_config.mnemonic,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
enabled_credentials_mode: run_config.enabled_credentials_mode,
|
||||
|
||||
#[cfg(all(feature = "eth", not(feature = "coconut")))]
|
||||
|
||||
@@ -66,6 +66,10 @@ impl NymConfig for Config {
|
||||
.join("gateways")
|
||||
}
|
||||
|
||||
fn try_default_root_directory() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|path| path.join(".nym").join("gateways"))
|
||||
}
|
||||
|
||||
fn root_directory(&self) -> PathBuf {
|
||||
self.gateway.nym_root_directory.clone()
|
||||
}
|
||||
@@ -123,6 +127,7 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "eth", feature = "coconut"))]
|
||||
pub fn with_disabled_credentials_mode(mut self, disabled_credentials_mode: bool) -> Self {
|
||||
self.gateway.disabled_credentials_mode = disabled_credentials_mode;
|
||||
self
|
||||
|
||||
@@ -96,6 +96,10 @@ impl NymConfig for Config {
|
||||
.join("mixnodes")
|
||||
}
|
||||
|
||||
fn try_default_root_directory() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|path| path.join(".nym").join("mixnodes"))
|
||||
}
|
||||
|
||||
fn root_directory(&self) -> PathBuf {
|
||||
self.mixnode.nym_root_directory.clone()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
## [nym-connect-v1.0.2](https://github.com/nymtech/nym/tree/nym-connect-v1.0.2) (2022-08-18)
|
||||
|
||||
### Changed
|
||||
|
||||
- nym-connect: "load balance" the service providers by picking a random Service Provider for each Service and storing in local storage so it remains sticky for the user ([#1540])
|
||||
- nym-connect: the ServiceProviderSelector only displays the available Services, and picks a random Service Provider for Services the user has never used before ([#1540])
|
||||
- nym-connect: add `local-forage` for storing user settings ([#1540])
|
||||
|
||||
[#1540]: https://github.com/nymtech/nym/pull/1540
|
||||
|
||||
|
||||
## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22)
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+2
-1
@@ -3404,7 +3404,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-connect"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
"client-core",
|
||||
@@ -5227,6 +5227,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"nymsphinx-addressing",
|
||||
"ordered-buffer",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"react-hook-form": "^7.14.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"semver": "^6.3.0",
|
||||
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-connect"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
description = "nym-connect"
|
||||
authors = ["Nym Technologies SA"]
|
||||
license = ""
|
||||
|
||||
@@ -37,9 +37,7 @@ pub async fn get_config_file_location(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<String> {
|
||||
let id = get_config_id(state).await?;
|
||||
Ok(Config::config_file_location(&id)
|
||||
.to_string_lossy()
|
||||
.to_string())
|
||||
Config::config_file_location(&id).map(|d| d.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -94,8 +92,9 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn config_file_location(id: &str) -> PathBuf {
|
||||
Socks5Config::default_config_file_path(Some(id))
|
||||
pub fn config_file_location(id: &str) -> Result<PathBuf> {
|
||||
Socks5Config::try_default_config_file_path(Some(id))
|
||||
.ok_or(BackendError::CouldNotGetFilename)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +106,9 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
|
||||
|
||||
log::debug!(
|
||||
"Attempting to use config file location: {}",
|
||||
Config::config_file_location(&id).to_string_lossy(),
|
||||
Config::config_file_location(&id)?.to_string_lossy(),
|
||||
);
|
||||
let already_init = Config::config_file_location(&id).exists();
|
||||
let already_init = Config::config_file_location(&id)?.exists();
|
||||
if already_init {
|
||||
log::info!(
|
||||
"SOCKS5 client \"{}\" was already initialised before! \
|
||||
@@ -147,12 +146,12 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
|
||||
Some(&chosen_gateway_id),
|
||||
config.get_socks5(),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
config.get_base_mut().with_gateway_endpoint(gateway);
|
||||
|
||||
let config_save_location = config.get_socks5().get_config_file_save_location();
|
||||
config.get_socks5().save_to_file(None).tap_err(|_| {
|
||||
log::warn!("Failed to save the config file");
|
||||
log::error!("Failed to save the config file");
|
||||
})?;
|
||||
|
||||
log::info!("Saved configuration file to {:?}", config_save_location);
|
||||
@@ -183,7 +182,7 @@ async fn setup_gateway(
|
||||
register: bool,
|
||||
user_chosen_gateway_id: Option<&str>,
|
||||
config: &Socks5Config,
|
||||
) -> GatewayEndpoint {
|
||||
) -> Result<GatewayEndpoint> {
|
||||
if register {
|
||||
// Get the gateway details by querying the validator-api. Either pick one at random or use
|
||||
// the chosen one if it's among the available ones.
|
||||
@@ -201,7 +200,7 @@ async fn setup_gateway(
|
||||
.await;
|
||||
println!("Saved all generated keys");
|
||||
|
||||
gateway.into()
|
||||
Ok(gateway.into())
|
||||
} else if user_chosen_gateway_id.is_some() {
|
||||
// Just set the config, don't register or create any keys
|
||||
// This assumes that the user knows what they are doing, and that the existing keys are
|
||||
@@ -213,19 +212,20 @@ async fn setup_gateway(
|
||||
)
|
||||
.await;
|
||||
log::debug!("Querying gateway gives: {}", gateway);
|
||||
gateway.into()
|
||||
Ok(gateway.into())
|
||||
} else {
|
||||
println!("Not registering gateway, will reuse existing config and keys");
|
||||
match Socks5Config::load_from_file(Some(id)) {
|
||||
Ok(existing_config) => existing_config.get_base().get_gateway_endpoint().clone(),
|
||||
Ok(existing_config) => Ok(existing_config.get_base().get_gateway_endpoint().clone()),
|
||||
Err(err) => {
|
||||
panic!(
|
||||
log::error!(
|
||||
"Unable to configure gateway: {err}. \n
|
||||
Seems like the client was already initialized but it was not possible to read \
|
||||
the existing configuration file. \n
|
||||
CAUTION: Consider backing up your gateway keys and try force gateway registration, or \
|
||||
removing the existing configuration and starting over."
|
||||
)
|
||||
);
|
||||
Err(BackendError::CouldNotLoadExistingGatewayConfiguration(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ pub enum BackendError {
|
||||
CouldNotInitWithoutServiceProvider,
|
||||
#[error("Could not get file name")]
|
||||
CouldNotGetFilename,
|
||||
#[error("Could not get config file location")]
|
||||
CouldNotGetConfigFilename,
|
||||
#[error("Could not load existing gateway configuration")]
|
||||
CouldNotLoadExistingGatewayConfiguration(std::io::Error),
|
||||
}
|
||||
|
||||
impl Serialize for BackendError {
|
||||
|
||||
@@ -101,7 +101,7 @@ impl State {
|
||||
|
||||
// Setup configuration by writing to file
|
||||
if let Err(err) = self.init_config().await {
|
||||
log::warn!("Failed to initialize: {}", err);
|
||||
log::error!("Failed to initialize: {}", err);
|
||||
|
||||
// Wait a little to give the user some rudimentary feedback that the click actually
|
||||
// registered.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "nym-connect",
|
||||
"version": "1.0.1"
|
||||
"version": "1.0.2"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
@@ -65,9 +65,9 @@
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Nym Connect",
|
||||
"title": "NymConnect",
|
||||
"width": 240,
|
||||
"height": 480,
|
||||
"height": 500,
|
||||
"resizable": false
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,6 +1,56 @@
|
||||
import React from 'react';
|
||||
import { ConnectionStatusKind } from '../types';
|
||||
|
||||
const getBusyFillColor = (color: string): string => {
|
||||
if (color === '#60D6EF') {
|
||||
return '#21D072';
|
||||
}
|
||||
return '#60D6EF';
|
||||
};
|
||||
|
||||
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
|
||||
if (isError && hover) {
|
||||
return '#21D072';
|
||||
}
|
||||
if (isError) {
|
||||
return '#40475C';
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatusKind.disconnected:
|
||||
if (hover) {
|
||||
return '#21D072';
|
||||
}
|
||||
return '#60D6EF';
|
||||
case ConnectionStatusKind.connecting:
|
||||
case ConnectionStatusKind.disconnecting:
|
||||
return '#60D6EF';
|
||||
default:
|
||||
// connected
|
||||
if (hover) {
|
||||
return '#DA465B';
|
||||
}
|
||||
return '#21D072';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: ConnectionStatusKind, hover: boolean): string => {
|
||||
switch (status) {
|
||||
case ConnectionStatusKind.disconnected:
|
||||
return 'Connect';
|
||||
case ConnectionStatusKind.connecting:
|
||||
return 'Connecting';
|
||||
case ConnectionStatusKind.disconnecting:
|
||||
return 'Connected';
|
||||
default:
|
||||
// connected
|
||||
if (hover) {
|
||||
return 'Disconnect';
|
||||
}
|
||||
return 'Connected';
|
||||
}
|
||||
};
|
||||
|
||||
export const ConnectionButton: React.FC<{
|
||||
status: ConnectionStatusKind;
|
||||
disabled?: boolean;
|
||||
@@ -130,53 +180,3 @@ export const ConnectionButton: React.FC<{
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const getBusyFillColor = (color: string): string => {
|
||||
if (color === '#60D6EF') {
|
||||
return '#21D072';
|
||||
}
|
||||
return '#60D6EF';
|
||||
};
|
||||
|
||||
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
|
||||
if (isError && hover) {
|
||||
return '#21D072';
|
||||
}
|
||||
if (isError) {
|
||||
return '#40475C';
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatusKind.disconnected:
|
||||
if (hover) {
|
||||
return '#21D072';
|
||||
}
|
||||
return '#60D6EF';
|
||||
case ConnectionStatusKind.connecting:
|
||||
case ConnectionStatusKind.disconnecting:
|
||||
return '#60D6EF';
|
||||
default:
|
||||
// connected
|
||||
if (hover) {
|
||||
return '#DA465B';
|
||||
}
|
||||
return '#21D072';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: ConnectionStatusKind, hover: boolean): string => {
|
||||
switch (status) {
|
||||
case ConnectionStatusKind.disconnected:
|
||||
return 'Connect';
|
||||
case ConnectionStatusKind.connecting:
|
||||
return 'Connecting';
|
||||
case ConnectionStatusKind.disconnecting:
|
||||
return 'Connected';
|
||||
default:
|
||||
// connected
|
||||
if (hover) {
|
||||
return 'Disconnect';
|
||||
}
|
||||
return 'Connected';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,24 +1,53 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle';
|
||||
import { Box, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { ServiceProvider, Services } from '../types/directory';
|
||||
import { ServiceProvider, Service, Services } from '../types/directory';
|
||||
|
||||
type ServiceWithRandomSp = {
|
||||
id: string;
|
||||
description: string;
|
||||
sp: ServiceProvider;
|
||||
};
|
||||
|
||||
export const ServiceProviderSelector: React.FC<{
|
||||
onChange?: (serviceProvider: ServiceProvider) => void;
|
||||
services?: Services;
|
||||
}> = ({ services, onChange }) => {
|
||||
const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>();
|
||||
currentSp?: ServiceProvider;
|
||||
}> = ({ services, currentSp, onChange }) => {
|
||||
const [service, setService] = React.useState<Service>();
|
||||
const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>(currentSp);
|
||||
const textEl = React.useRef<null | HTMLElement>(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
useEffect(() => {
|
||||
if (!serviceProvider && currentSp) {
|
||||
setServiceProvider(currentSp);
|
||||
}
|
||||
}, [currentSp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (services && serviceProvider) {
|
||||
// retrieve the service corresponding to this service provider
|
||||
setService(
|
||||
services.find((s) =>
|
||||
s.items.some(
|
||||
({ id, address, gateway }) =>
|
||||
id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [serviceProvider, services]);
|
||||
|
||||
const handleClick = () => {
|
||||
setAnchorEl(textEl.current);
|
||||
};
|
||||
const handleClose = (newServiceProvider?: ServiceProvider) => {
|
||||
if (newServiceProvider) {
|
||||
if (newServiceProvider && newServiceProvider !== currentSp) {
|
||||
setServiceProvider(newServiceProvider);
|
||||
onChange?.(newServiceProvider);
|
||||
}
|
||||
@@ -39,6 +68,16 @@ export const ServiceProviderSelector: React.FC<{
|
||||
);
|
||||
}
|
||||
|
||||
const servicesWithRandomSp: ServiceWithRandomSp[] = useMemo(
|
||||
() =>
|
||||
services.map(({ id, items, description }) => ({
|
||||
id,
|
||||
description,
|
||||
sp: items[Math.floor(Math.random() * items.length)],
|
||||
})),
|
||||
[services],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" sx={{ mt: 3 }}>
|
||||
@@ -48,7 +87,7 @@ export const ServiceProviderSelector: React.FC<{
|
||||
fontWeight={700}
|
||||
color={(theme) => (serviceProvider ? undefined : theme.palette.primary.main)}
|
||||
>
|
||||
{serviceProvider ? serviceProvider.description : 'Select a service'}
|
||||
{service ? service.description : 'Select a service'}
|
||||
</Typography>
|
||||
<IconButton
|
||||
id="service-provider-button"
|
||||
@@ -65,44 +104,46 @@ export const ServiceProviderSelector: React.FC<{
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={() => handleClose()}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'service-provider-button',
|
||||
sx: {
|
||||
minWidth: 160,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<>
|
||||
<MenuItem disabled dense sx={{ fontSize: 'small', fontWeight: 'bold', mb: -1 }}>
|
||||
{service.description}
|
||||
</MenuItem>
|
||||
{service.items.map((sp) => (
|
||||
<MenuItem dense sx={{ fontSize: 'small', ml: 2, height: 'auto' }} onClick={() => handleClose(sp)}>
|
||||
<Tooltip
|
||||
title={
|
||||
<Stack direction="column">
|
||||
<Typography fontSize="inherit">
|
||||
<code>{sp.id}</code>
|
||||
</Typography>
|
||||
<Typography fontSize="inherit" fontWeight={700}>
|
||||
{sp.description}
|
||||
</Typography>
|
||||
<Typography fontSize="inherit">
|
||||
Gateway <code>{sp.gateway.slice(0, 10)}...</code>
|
||||
</Typography>
|
||||
<Typography fontSize="inherit">
|
||||
Provider <code>{sp.address.slice(0, 10)}...</code>
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography fontSize="inherit" noWrap>
|
||||
{servicesWithRandomSp.map(({ id, description, sp }) => (
|
||||
<MenuItem dense key={id} sx={{ fontSize: 'small', fontWeight: 'bold' }} onClick={() => handleClose(sp)}>
|
||||
<Tooltip
|
||||
title={
|
||||
<Stack direction="column">
|
||||
<Typography fontSize="inherit">
|
||||
<code>{sp.id}</code>
|
||||
</Typography>
|
||||
<Typography fontSize="inherit" fontWeight={700}>
|
||||
{sp.description}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
))}
|
||||
</>
|
||||
<Typography fontSize="inherit">
|
||||
Gateway <code>{sp.gateway.slice(0, 10)}...</code>
|
||||
</Typography>
|
||||
<Typography fontSize="inherit">
|
||||
Provider <code>{sp.address.slice(0, 10)}...</code>
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Typography>{description}</Typography>
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { forage } from '@tauri-apps/tauri-forage';
|
||||
import { ConnectionStatusKind } from '../types';
|
||||
import { ConnectionStatsItem } from '../components/ConnectionStats';
|
||||
import { ServiceProvider, Services } from '../types/directory';
|
||||
@@ -36,7 +37,7 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatusKind>(ConnectionStatusKind.disconnected);
|
||||
const [connectionStats, setConnectionStats] = useState<ConnectionStatsItem[]>();
|
||||
const [connectedSince, setConnectedSince] = useState<DateTime>();
|
||||
const [services, setServices] = React.useState<Services>();
|
||||
const [services, setServices] = React.useState<Services>([]);
|
||||
const [serviceProvider, setRawServiceProvider] = React.useState<ServiceProvider>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,33 +73,71 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
await invoke('start_disconnecting');
|
||||
}, []);
|
||||
|
||||
const setSpInStorage = async (sp: ServiceProvider) => {
|
||||
await forage.setItem({
|
||||
key: 'nym-connect-sp',
|
||||
value: sp,
|
||||
} as any)();
|
||||
};
|
||||
|
||||
const setServiceProvider = useCallback(async (newServiceProvider: ServiceProvider) => {
|
||||
await invoke('set_gateway', { gateway: newServiceProvider.gateway });
|
||||
await invoke('set_service_provider', { serviceProvider: newServiceProvider.address });
|
||||
await setSpInStorage(newServiceProvider);
|
||||
setRawServiceProvider(newServiceProvider);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ClientContext.Provider
|
||||
value={{
|
||||
mode,
|
||||
setMode,
|
||||
connectionStatus,
|
||||
setConnectionStatus,
|
||||
connectionStats,
|
||||
setConnectionStats,
|
||||
connectedSince,
|
||||
setConnectedSince,
|
||||
startConnecting,
|
||||
startDisconnecting,
|
||||
services,
|
||||
serviceProvider,
|
||||
setServiceProvider,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ClientContext.Provider>
|
||||
const getSpFromStorage = async () => {
|
||||
try {
|
||||
const spFromStorage = await forage.getItem({ key: 'nym-connect-sp' })();
|
||||
if (spFromStorage) {
|
||||
setRawServiceProvider(spFromStorage);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const validityCheck = async () => {
|
||||
if (services.length > 0 && serviceProvider) {
|
||||
const isValid = services.some(({ items }) => items.some(({ id }) => id === serviceProvider.id));
|
||||
if (!isValid) {
|
||||
console.warn('invalid SP, cleaning local storage');
|
||||
await forage.removeItem({
|
||||
key: 'nym-connect-sp',
|
||||
})();
|
||||
setRawServiceProvider(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
validityCheck();
|
||||
}, [services, serviceProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
getSpFromStorage();
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
mode,
|
||||
setMode,
|
||||
connectionStatus,
|
||||
setConnectionStatus,
|
||||
connectionStats,
|
||||
setConnectionStats,
|
||||
connectedSince,
|
||||
setConnectedSince,
|
||||
startConnecting,
|
||||
startDisconnecting,
|
||||
services,
|
||||
serviceProvider,
|
||||
setServiceProvider,
|
||||
}),
|
||||
[mode, connectedSince, connectionStatus, connectionStats, connectedSince, services, serviceProvider],
|
||||
);
|
||||
|
||||
return <ClientContext.Provider value={contextValue}>{children}</ClientContext.Provider>;
|
||||
};
|
||||
|
||||
export const useClientContext = () => useContext(ClientContext);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ConnectionStatusKind } from '../types';
|
||||
import { NeedHelp } from '../components/NeedHelp';
|
||||
import { ServiceProviderSelector } from '../components/ServiceProviderSelector';
|
||||
import { ServiceProvider, Services } from '../types/directory';
|
||||
import { useClientContext } from '../context/main';
|
||||
|
||||
export const DefaultLayout: React.FC<{
|
||||
status: ConnectionStatusKind;
|
||||
@@ -20,6 +21,8 @@ export const DefaultLayout: React.FC<{
|
||||
setServiceProvider(newServiceProvider);
|
||||
onServiceProviderChange?.(newServiceProvider);
|
||||
};
|
||||
const { serviceProvider: currentSp } = useClientContext();
|
||||
|
||||
return (
|
||||
<AppWindowFrame>
|
||||
<Typography fontWeight="400" fontSize="12px" textAlign="center" sx={{ opacity: 0.6 }}>
|
||||
@@ -31,10 +34,10 @@ export const DefaultLayout: React.FC<{
|
||||
<br />
|
||||
Nym mixnet for privacy.
|
||||
</Typography>
|
||||
<ServiceProviderSelector services={services} onChange={handleServiceProviderChange} />
|
||||
<ServiceProviderSelector services={services} onChange={handleServiceProviderChange} currentSp={currentSp} />
|
||||
<ConnectionButton
|
||||
status={status}
|
||||
disabled={serviceProvider === undefined}
|
||||
disabled={serviceProvider === undefined && currentSp === undefined}
|
||||
busy={busy}
|
||||
isError={isError}
|
||||
onClick={onConnectClick}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Specify filenames and other platform specific constants to respect platform conventions, or at
|
||||
// least, something popular on each respective platform.
|
||||
|
||||
pub const CONFIG_DIR_NAME: &str = "nym-wallet";
|
||||
pub const CONFIG_FILENAME: &str = "config.toml";
|
||||
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests';
|
||||
import { TBondGatewayArgs } from 'src/types';
|
||||
import { BondGatewayForm } from '../forms/BondGatewayForm';
|
||||
|
||||
const defaultMixnodeValues: GatewayData = {
|
||||
const defaultGatewayValues: GatewayData = {
|
||||
identityKey: '',
|
||||
sphinxKey: '',
|
||||
ownerSignature: '',
|
||||
@@ -19,7 +19,7 @@ const defaultMixnodeValues: GatewayData = {
|
||||
host: '',
|
||||
version: '',
|
||||
mixPort: 1789,
|
||||
clientsPort: 1790,
|
||||
clientsPort: 9000,
|
||||
};
|
||||
|
||||
const defaultAmountValues = (denom: CurrencyDenom) => ({
|
||||
@@ -43,7 +43,7 @@ export const BondGatewayModal = ({
|
||||
onError: (e: string) => void;
|
||||
}) => {
|
||||
const [step, setStep] = useState<1 | 2 | 3>(1);
|
||||
const [gatewayData, setGatewayData] = useState<GatewayData>(defaultMixnodeValues);
|
||||
const [gatewayData, setGatewayData] = useState<GatewayData>(defaultGatewayValues);
|
||||
const [amountData, setAmountData] = useState<GatewayAmount>(defaultAmountValues(denom));
|
||||
|
||||
const { fee, getFee, resetFeeState, feeError } = useGetFee();
|
||||
|
||||
@@ -118,7 +118,6 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
stakeSaturation: 0,
|
||||
numberOfDelegators: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const statusResponse = await getMixnodeStatus(identityKey);
|
||||
additionalDetails.status = statusResponse.status;
|
||||
@@ -163,7 +162,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
try {
|
||||
operatorRewards = await getOperatorRewards(clientDetails?.client_address);
|
||||
} catch (e) {
|
||||
console.warn(`get_operator_rewards request failed: ${e}`);
|
||||
Console.warn(`get_operator_rewards request failed: ${e}`);
|
||||
}
|
||||
if (data) {
|
||||
const { status, stakeSaturation, numberOfDelegators } = await getAdditionalMixnodeDetails(
|
||||
@@ -186,7 +185,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
} as TBondedMixnode);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn(e);
|
||||
Console.warn(e);
|
||||
setError(`While fetching current bond state, an error occurred: ${e}`);
|
||||
}
|
||||
}
|
||||
@@ -207,6 +206,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
} as TBondedGateway);
|
||||
}
|
||||
} catch (e: any) {
|
||||
Console.warn(e);
|
||||
setError(`While fetching current bond state, an error occurred: ${e}`);
|
||||
}
|
||||
}
|
||||
@@ -235,6 +235,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
}
|
||||
return tx;
|
||||
} catch (e: any) {
|
||||
Console.warn(e);
|
||||
setError(`an error occurred: ${e}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -256,6 +257,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
}
|
||||
return tx;
|
||||
} catch (e: any) {
|
||||
Console.warn(e);
|
||||
setError(`an error occurred: ${e}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -272,6 +274,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
if (bondedNode && isGateway(bondedNode) && bondedNode.proxy) tx = await vestingUnbondGateway(fee?.fee);
|
||||
if (bondedNode && isGateway(bondedNode) && !bondedNode.proxy) tx = await unbondGatewayRequest(fee?.fee);
|
||||
} catch (e) {
|
||||
Console.warn(e);
|
||||
setError(`an error occurred: ${e as string}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -286,6 +289,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
if (bondedNode?.proxy) tx = await updateMixnodeVestingRequest(pm, fee?.fee);
|
||||
else tx = await updateMixnodeRequest(pm, fee?.fee);
|
||||
} catch (e: any) {
|
||||
Console.warn(e);
|
||||
setError(`an error occurred: ${e}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { FeeDetails } from '@nymproject/types';
|
||||
import { TPoolOption } from 'src/components';
|
||||
import { Bond } from 'src/components/Bonding/Bond';
|
||||
@@ -16,9 +16,8 @@ import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/ty
|
||||
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
|
||||
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
|
||||
import { CompoundRewardsModal } from 'src/components/Bonding/modals/CompoundRewardsModal';
|
||||
import { PageLayout } from '../../layouts';
|
||||
import { BondingContextProvider, useBondingContext } from '../../context';
|
||||
import { Box } from '@mui/material';
|
||||
import { BondingContextProvider, useBondingContext } from '../../context';
|
||||
|
||||
const Bonding = () => {
|
||||
const [showModal, setShowModal] = useState<
|
||||
@@ -42,19 +41,31 @@ const Bonding = () => {
|
||||
compoundRewards,
|
||||
isLoading,
|
||||
checkOwnership,
|
||||
error,
|
||||
} = useBondingContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setShowModal(undefined);
|
||||
setConfirmationDetails({
|
||||
status: 'error',
|
||||
title: 'An error occurred',
|
||||
subtitle: error,
|
||||
});
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const handleCloseModal = async () => {
|
||||
setShowModal(undefined);
|
||||
await checkOwnership();
|
||||
};
|
||||
|
||||
const handleError = (error: string) => {
|
||||
const handleError = (e: string) => {
|
||||
setShowModal(undefined);
|
||||
setConfirmationDetails({
|
||||
status: 'error',
|
||||
title: 'An error occurred',
|
||||
subtitle: error,
|
||||
subtitle: e,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -30,14 +30,18 @@ const DataField = ({ title, info, Indicator }: { title: string; info: string; In
|
||||
);
|
||||
|
||||
const colorMap: { [key in SelectionChance]: string } = {
|
||||
VeryLow: 'error.main',
|
||||
Low: 'error.main',
|
||||
Moderate: 'warning.main',
|
||||
High: 'success.main',
|
||||
VeryHigh: 'success.main',
|
||||
};
|
||||
|
||||
const textMap: { [key in SelectionChance]: string } = {
|
||||
VeryLow: 'VeryLow',
|
||||
Low: 'Low',
|
||||
Moderate: 'Moderate',
|
||||
High: 'High',
|
||||
VeryHigh: 'Very high',
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::receiver::ReconstructedMessage;
|
||||
use proxy_helpers::connection_controller::{Controller, ControllerCommand, ControllerSender};
|
||||
use socks5_requests::{ConnectionId, Message as Socks5Message, Request, Response};
|
||||
use socks5_requests::{
|
||||
ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request, Response,
|
||||
};
|
||||
use statistics_common::collector::StatisticsSender;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
@@ -192,7 +194,16 @@ impl ServiceProvider {
|
||||
return_address: Recipient,
|
||||
) {
|
||||
if !self.open_proxy && !self.outbound_request_filter.check(&remote_addr) {
|
||||
log::info!("Domain {:?} failed filter check", remote_addr);
|
||||
let log_msg = format!("Domain {:?} failed filter check", remote_addr);
|
||||
log::info!("{}", log_msg);
|
||||
mix_input_sender
|
||||
.unbounded_send((
|
||||
Socks5Message::NetworkRequesterResponse(NetworkRequesterResponse::new(
|
||||
conn_id, log_msg,
|
||||
)),
|
||||
return_address,
|
||||
))
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,7 +286,7 @@ impl ServiceProvider {
|
||||
self.handle_proxy_send(controller_sender, conn_id, data, closed)
|
||||
}
|
||||
},
|
||||
Socks5Message::Response(_) => {}
|
||||
Socks5Message::Response(_) | Socks5Message::NetworkRequesterResponse(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type SelectionChance = 'VeryHigh' | 'Moderate' | 'Low';
|
||||
export type SelectionChance = 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow';
|
||||
|
||||
@@ -16,7 +16,9 @@ rust-version = "1.56"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.52"
|
||||
cfg-if = "1.0"
|
||||
clap = "2.33.0"
|
||||
console-subscriber = { version = "0.1.1", optional = true} # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
|
||||
dirs = "4.0"
|
||||
dotenv = "0.15.0"
|
||||
futures = "0.3"
|
||||
@@ -31,6 +33,7 @@ rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
||||
rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
tap = "1.0.1"
|
||||
thiserror = "1"
|
||||
time = { version = "0.3", features = ["serde-human-readable", "parsing"]}
|
||||
tokio = { version = "1.19.1", features = ["rt-multi-thread", "macros", "signal", "time"] }
|
||||
@@ -51,25 +54,23 @@ schemars = { version = "0.8", features = ["preserve_order"] }
|
||||
|
||||
## internal
|
||||
coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" }
|
||||
coconut-interface = { path = "../common/coconut-interface", optional = true }
|
||||
config = { path = "../common/config" }
|
||||
cosmwasm-std = "1.0.0"
|
||||
credential-storage = { path = "../common/credential-storage" }
|
||||
credentials = { path = "../common/credentials", optional = true }
|
||||
crypto = { path="../common/crypto" }
|
||||
gateway-client = { path="../common/client-libs/gateway-client" }
|
||||
inclusion-probability = { path = "../common/inclusion-probability" }
|
||||
mixnet-contract-common = { path= "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nymsphinx = { path="../common/nymsphinx" }
|
||||
nymcoconut = { path = "../common/nymcoconut", optional = true }
|
||||
nymsphinx = { path="../common/nymsphinx" }
|
||||
task = { path = "../common/task" }
|
||||
topology = { path="../common/topology" }
|
||||
validator-api-requests = { path = "validator-api-requests" }
|
||||
validator-client = { path="../common/client-libs/validator-client", features = ["nymd-client"] }
|
||||
version-checker = { path="../common/version-checker" }
|
||||
coconut-interface = { path = "../common/coconut-interface", optional = true }
|
||||
credentials = { path = "../common/credentials", optional = true }
|
||||
credential-storage = { path = "../common/credential-storage" }
|
||||
# validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
|
||||
console-subscriber = { version = "0.1.1", optional = true}
|
||||
cfg-if = "1.0"
|
||||
|
||||
[features]
|
||||
coconut = ["coconut-interface", "credentials", "gateway-client/coconut", "credentials/coconut", "validator-api-requests/coconut", "nymcoconut"]
|
||||
|
||||
@@ -72,6 +72,10 @@ impl NymConfig for Config {
|
||||
.join("validator-api")
|
||||
}
|
||||
|
||||
fn try_default_root_directory() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|path| path.join(".nym").join("validator-api"))
|
||||
}
|
||||
|
||||
fn root_directory(&self) -> PathBuf {
|
||||
Self::default_root_directory()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{watch, RwLock};
|
||||
use tokio::time;
|
||||
use validator_api_requests::models::{MixNodeBondAnnotated, MixnodeStatus};
|
||||
use validator_client::nymd::CosmWasmClient;
|
||||
@@ -31,6 +31,13 @@ use validator_client::nymd::CosmWasmClient;
|
||||
pub(crate) mod reward_estimate;
|
||||
pub(crate) mod routes;
|
||||
|
||||
// The cache can emit notifications to listeners about the current state
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum CacheNotification {
|
||||
Start,
|
||||
Updated,
|
||||
}
|
||||
|
||||
pub struct ValidatorCacheRefresher<C> {
|
||||
nymd_client: Client<C>,
|
||||
cache: ValidatorCache,
|
||||
@@ -38,6 +45,9 @@ pub struct ValidatorCacheRefresher<C> {
|
||||
|
||||
// Readonly: some of the quantities cached depends on values from the storage.
|
||||
storage: Option<ValidatorApiStorage>,
|
||||
|
||||
// Notify listeners that the cache has been updated
|
||||
update_notifier: watch::Sender<CacheNotification>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -73,14 +83,14 @@ pub struct Cache<T> {
|
||||
}
|
||||
|
||||
impl<T: Clone> Cache<T> {
|
||||
fn new(value: T) -> Self {
|
||||
pub(super) fn new(value: T) -> Self {
|
||||
Cache {
|
||||
value,
|
||||
as_at: current_unix_timestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, value: T) {
|
||||
pub(super) fn update(&mut self, value: T) {
|
||||
self.value = value;
|
||||
self.as_at = current_unix_timestamp()
|
||||
}
|
||||
@@ -101,11 +111,13 @@ impl<C> ValidatorCacheRefresher<C> {
|
||||
cache: ValidatorCache,
|
||||
storage: Option<ValidatorApiStorage>,
|
||||
) -> Self {
|
||||
let (tx, _) = watch::channel(CacheNotification::Start);
|
||||
ValidatorCacheRefresher {
|
||||
nymd_client,
|
||||
cache,
|
||||
caching_interval,
|
||||
storage,
|
||||
update_notifier: tx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +129,10 @@ impl<C> ValidatorCacheRefresher<C> {
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> watch::Receiver<CacheNotification> {
|
||||
self.update_notifier.subscribe()
|
||||
}
|
||||
|
||||
async fn annotate_bond_with_details(
|
||||
&self,
|
||||
mixnodes: Vec<MixNodeBond>,
|
||||
@@ -250,6 +266,10 @@ impl<C> ValidatorCacheRefresher<C> {
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = self.update_notifier.send(CacheNotification::Updated) {
|
||||
warn!("Failed to notify validator cache refresh: {}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -261,17 +281,25 @@ impl<C> ValidatorCacheRefresher<C> {
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
if let Err(err) = self.refresh_cache().await {
|
||||
error!("Failed to refresh validator cache - {}", err);
|
||||
} else {
|
||||
// relaxed memory ordering is fine here. worst case scenario network monitor
|
||||
// will just have to wait for an additional backoff to see the change.
|
||||
// And so this will not really incur any performance penalties by setting it every loop iteration
|
||||
self.cache.initialised.store(true, Ordering::Relaxed)
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
trace!("ValidatorCacheRefresher: Received shutdown");
|
||||
}
|
||||
ret = self.refresh_cache() => {
|
||||
if let Err(err) = ret {
|
||||
error!("Failed to refresh validator cache - {}", err);
|
||||
} else {
|
||||
// relaxed memory ordering is fine here. worst case scenario network monitor
|
||||
// will just have to wait for an additional backoff to see the change.
|
||||
// And so this will not really incur any performance penalties by setting it every loop iteration
|
||||
self.cache.initialised.store(true, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
trace!("ValidatorCacheRefresher: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use anyhow::Result;
|
||||
use clap::{crate_version, App, Arg, ArgMatches};
|
||||
use contract_cache::ValidatorCache;
|
||||
use log::{info, warn};
|
||||
use node_status_api::NodeStatusCache;
|
||||
use okapi::openapi3::OpenApi;
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::http::Method;
|
||||
@@ -470,7 +471,8 @@ async fn setup_rocket(
|
||||
.mount("/swagger", make_swagger_ui(&swagger::get_docs()))
|
||||
.attach(setup_cors()?)
|
||||
.attach(setup_liftoff_notify(liftoff_notify))
|
||||
.attach(ValidatorCache::stage());
|
||||
.attach(ValidatorCache::stage())
|
||||
.attach(NodeStatusCache::stage());
|
||||
|
||||
// This is not a very nice approach. A lazy value would be more suitable, but that's still
|
||||
// a nightly feature: https://github.com/rust-lang/rust/issues/74465
|
||||
@@ -566,7 +568,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
let signing_nymd_client = Client::new_signing(&config);
|
||||
|
||||
let liftoff_notify = Arc::new(Notify::new());
|
||||
let shutdown = ShutdownNotifier::default();
|
||||
// We need a bigger timeout
|
||||
let shutdown = ShutdownNotifier::new(10);
|
||||
|
||||
// let's build our rocket!
|
||||
let rocket = setup_rocket(
|
||||
@@ -579,10 +582,11 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
let monitor_builder = setup_network_monitor(&config, system_version, &rocket);
|
||||
|
||||
let validator_cache = rocket.state::<ValidatorCache>().unwrap().clone();
|
||||
let node_status_cache = rocket.state::<NodeStatusCache>().unwrap().clone();
|
||||
|
||||
// if network monitor is disabled, we're not going to be sending any rewarding hence
|
||||
// we're not starting signing client
|
||||
if config.get_network_monitor_enabled() {
|
||||
let validator_cache_listener = if config.get_network_monitor_enabled() {
|
||||
// Main storage
|
||||
let storage = rocket.state::<ValidatorApiStorage>().unwrap().clone();
|
||||
|
||||
@@ -592,33 +596,53 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { uptime_updater.run(shutdown_listener).await });
|
||||
|
||||
// spawn the cache refresher
|
||||
// spawn the validator cache refresher
|
||||
let validator_cache_refresher = ValidatorCacheRefresher::new(
|
||||
signing_nymd_client.clone(),
|
||||
config.get_caching_interval(),
|
||||
validator_cache.clone(),
|
||||
Some(storage.clone()),
|
||||
);
|
||||
let validator_cache_listener = validator_cache_refresher.subscribe();
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
|
||||
|
||||
// spawn rewarded set updater
|
||||
let mut rewarded_set_updater =
|
||||
RewardedSetUpdater::new(signing_nymd_client, validator_cache.clone(), storage).await?;
|
||||
tokio::spawn(async move { rewarded_set_updater.run().await.unwrap() });
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await.unwrap() });
|
||||
|
||||
validator_cache_listener
|
||||
} else {
|
||||
// Spawn the validator cache refresher.
|
||||
// When the network monitor is not enabled, we spawn the validator cache refresher task
|
||||
// with just a nymd client, in contrast to a signing client.
|
||||
let nymd_client = Client::new_query(&config);
|
||||
let validator_cache_refresher = ValidatorCacheRefresher::new(
|
||||
nymd_client,
|
||||
config.get_caching_interval(),
|
||||
validator_cache,
|
||||
validator_cache.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let validator_cache_listener = validator_cache_refresher.subscribe();
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
// spawn our cacher
|
||||
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
|
||||
}
|
||||
|
||||
validator_cache_listener
|
||||
};
|
||||
|
||||
// Spawn the node status cache refresher.
|
||||
// It is primarily refreshed in-sync with the validator cache, however provide a fallback
|
||||
// caching interval that is twice the validator cache
|
||||
let mut validator_api_cache_refresher = node_status_api::NodeStatusCacheRefresher::new(
|
||||
node_status_cache,
|
||||
validator_cache,
|
||||
validator_cache_listener,
|
||||
config.get_caching_interval().saturating_mul(2),
|
||||
);
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { validator_api_cache_refresher.run(shutdown_listener).await });
|
||||
|
||||
// launch the rocket!
|
||||
// Rocket handles shutdown on it's own, but its shutdown handling should be incorporated
|
||||
|
||||
@@ -12,6 +12,7 @@ use topology::NymTopology;
|
||||
const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200);
|
||||
const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Chunker {
|
||||
rng: OsRng,
|
||||
message_preparer: MessagePreparer<OsRng>,
|
||||
@@ -30,7 +31,7 @@ impl Chunker {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn prepare_packets_from(
|
||||
pub(crate) fn prepare_packets_from(
|
||||
&mut self,
|
||||
message: Vec<u8>,
|
||||
topology: &NymTopology,
|
||||
@@ -40,10 +41,10 @@ impl Chunker {
|
||||
// but without some significant API changes in the `MessagePreparer` this was the easiest
|
||||
// way to being able to have variable sender address.
|
||||
self.message_preparer.set_sender_address(packet_sender);
|
||||
self.prepare_packets(message, topology, packet_sender).await
|
||||
self.prepare_packets(message, topology, packet_sender)
|
||||
}
|
||||
|
||||
async fn prepare_packets(
|
||||
fn prepare_packets(
|
||||
&mut self,
|
||||
message: Vec<u8>,
|
||||
topology: &NymTopology,
|
||||
@@ -62,7 +63,6 @@ impl Chunker {
|
||||
let prepared_fragment = self
|
||||
.message_preparer
|
||||
.prepare_chunk_for_sending(message_chunk, topology, &ack_key, &packet_sender)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
mix_packets.push(prepared_fragment.mix_packet);
|
||||
|
||||
@@ -7,6 +7,7 @@ use crypto::asymmetric::identity;
|
||||
use crypto::asymmetric::identity::PUBLIC_KEY_LENGTH;
|
||||
use log::{debug, info, trace, warn};
|
||||
use std::time::Duration;
|
||||
use task::ShutdownListener;
|
||||
use tokio::time::{sleep, Instant};
|
||||
|
||||
// TODO: should it perhaps be moved to config along other timeout values?
|
||||
@@ -143,10 +144,22 @@ impl GatewayPinger {
|
||||
info!("Pinging all active gateways took {:?}", time_taken);
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&self) {
|
||||
loop {
|
||||
sleep(self.pinging_interval).await;
|
||||
self.ping_and_cleanup_all_gateways().await
|
||||
pub(crate) async fn run(&self, mut shutdown: ShutdownListener) {
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = sleep(self.pinging_interval) => {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
trace!("GatewaysPinger: Received shutdown");
|
||||
}
|
||||
_ = self.ping_and_cleanup_all_gateways() => (),
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("GatewaysPinger: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,11 +122,15 @@ impl Monitor {
|
||||
|
||||
let mut packets = Vec::with_capacity(routes.len());
|
||||
for route in routes {
|
||||
packets.push(
|
||||
self.packet_preparer
|
||||
.prepare_test_route_viability_packets(route, self.route_test_packets)
|
||||
.await,
|
||||
);
|
||||
let mut packet_preparer = self.packet_preparer.clone();
|
||||
let route = route.clone();
|
||||
let route_test_packets = self.route_test_packets;
|
||||
let gateway_packets = tokio::spawn(async move {
|
||||
packet_preparer.prepare_test_route_viability_packets(&route, route_test_packets)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
packets.push(gateway_packets);
|
||||
}
|
||||
|
||||
self.received_processor.set_route_test_nonce().await;
|
||||
@@ -306,12 +310,20 @@ impl Monitor {
|
||||
.await;
|
||||
|
||||
self.packet_sender
|
||||
.spawn_gateways_pinger(self.gateway_ping_interval);
|
||||
.spawn_gateways_pinger(self.gateway_ping_interval, shutdown.clone());
|
||||
|
||||
let mut run_interval = tokio::time::interval(self.run_interval);
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = run_interval.tick() => self.test_run().await,
|
||||
_ = run_interval.tick() => {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
}
|
||||
_ = self.test_run() => (),
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ pub(crate) struct PreparedPackets {
|
||||
pub(super) invalid_gateways: Vec<InvalidNode>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PacketPreparer {
|
||||
system_version: String,
|
||||
chunker: Option<Chunker>,
|
||||
@@ -151,7 +152,7 @@ impl PacketPreparer {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wrap_test_packet(
|
||||
fn wrap_test_packet(
|
||||
&mut self,
|
||||
packet: &TestPacket,
|
||||
topology: &NymTopology,
|
||||
@@ -162,12 +163,11 @@ impl PacketPreparer {
|
||||
if self.chunker.is_none() {
|
||||
self.chunker = Some(Chunker::new(packet_recipient));
|
||||
}
|
||||
let mut mix_packets = self
|
||||
.chunker
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.prepare_packets_from(packet.to_bytes(), topology, packet_recipient)
|
||||
.await;
|
||||
let mut mix_packets = self.chunker.as_mut().unwrap().prepare_packets_from(
|
||||
packet.to_bytes(),
|
||||
topology,
|
||||
packet_recipient,
|
||||
);
|
||||
assert_eq!(
|
||||
mix_packets.len(),
|
||||
1,
|
||||
@@ -351,7 +351,7 @@ impl PacketPreparer {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn prepare_test_route_viability_packets(
|
||||
pub(crate) fn prepare_test_route_viability_packets(
|
||||
&mut self,
|
||||
route: &TestRoute,
|
||||
num: usize,
|
||||
@@ -360,9 +360,7 @@ impl PacketPreparer {
|
||||
let test_packet = route.self_test_packet();
|
||||
let recipient = self.create_packet_sender(route.gateway());
|
||||
for _ in 0..num {
|
||||
let mix_packet = self
|
||||
.wrap_test_packet(&test_packet, route.topology(), recipient)
|
||||
.await;
|
||||
let mix_packet = self.wrap_test_packet(&test_packet, route.topology(), recipient);
|
||||
mix_packets.push(mix_packet)
|
||||
}
|
||||
|
||||
@@ -451,9 +449,7 @@ impl PacketPreparer {
|
||||
let topology = test_route.substitute_mix(mixnode);
|
||||
// produce n mix packets
|
||||
for _ in 0..self.per_node_test_packets {
|
||||
let mix_packet = self
|
||||
.wrap_test_packet(&test_packet, &topology, recipient)
|
||||
.await;
|
||||
let mix_packet = self.wrap_test_packet(&test_packet, &topology, recipient);
|
||||
mix_packets.push(mix_packet);
|
||||
}
|
||||
}
|
||||
@@ -476,9 +472,7 @@ impl PacketPreparer {
|
||||
let topology = test_route.substitute_gateway(gateway);
|
||||
// produce n mix packets
|
||||
for _ in 0..self.per_node_test_packets {
|
||||
let mix_packet = self
|
||||
.wrap_test_packet(&test_packet, &topology, recipient)
|
||||
.await;
|
||||
let mix_packet = self.wrap_test_packet(&test_packet, &topology, recipient);
|
||||
gateway_mix_packets.push(mix_packet);
|
||||
}
|
||||
|
||||
|
||||
@@ -140,8 +140,7 @@ impl ReceivedProcessor {
|
||||
self.permit_changer = Some(permit_sender);
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let permit = wait_for_permit(&mut permit_receiver, &*inner).await;
|
||||
while let Some(permit) = wait_for_permit(&mut permit_receiver, &*inner).await {
|
||||
receive_or_release_permit(&mut permit_receiver, permit).await;
|
||||
}
|
||||
|
||||
@@ -151,16 +150,20 @@ impl ReceivedProcessor {
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
permit_receiver = permit_receiver.next() => match permit_receiver.unwrap() {
|
||||
LockPermit::Release => return,
|
||||
LockPermit::Free => error!("somehow we got notification that the lock is free to take while we already hold it!"),
|
||||
permit_receiver = permit_receiver.next() => match permit_receiver {
|
||||
Some(LockPermit::Release) => return,
|
||||
Some(LockPermit::Free) => error!("somehow we got notification that the lock is free to take while we already hold it!"),
|
||||
None => return,
|
||||
},
|
||||
messages = inner.packets_receiver.next() => {
|
||||
for message in messages.expect("packet receiver has died!") {
|
||||
if let Err(err) = inner.on_message(message) {
|
||||
warn!(target: "Monitor", "failed to process received gateway message - {}", err)
|
||||
messages = inner.packets_receiver.next() => match messages {
|
||||
Some(messages) => {
|
||||
for message in messages {
|
||||
if let Err(err) = inner.on_message(message) {
|
||||
warn!(target: "Monitor", "failed to process received gateway message - {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => return,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -172,14 +175,15 @@ impl ReceivedProcessor {
|
||||
async fn wait_for_permit<'a>(
|
||||
permit_receiver: &mut mpsc::Receiver<LockPermit>,
|
||||
inner: &'a Mutex<ReceivedProcessorInner>,
|
||||
) -> MutexGuard<'a, ReceivedProcessorInner> {
|
||||
) -> Option<MutexGuard<'a, ReceivedProcessorInner>> {
|
||||
loop {
|
||||
match permit_receiver.next().await.unwrap() {
|
||||
match permit_receiver.next().await {
|
||||
// we should only ever get this on the very first run
|
||||
LockPermit::Release => debug!(
|
||||
Some(LockPermit::Release) => debug!(
|
||||
"somehow got request to drop our lock permit while we do not hold it!"
|
||||
),
|
||||
LockPermit::Free => return inner.lock().await,
|
||||
Some(LockPermit::Free) => return Some(inner.lock().await),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ impl PacketReceiver {
|
||||
pub(crate) async fn run(&mut self, mut shutdown: ShutdownListener) {
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
}
|
||||
// unwrap here is fine as it can only return a `None` if the PacketSender has died
|
||||
// and if that was the case, then the entire monitor is already in an undefined state
|
||||
update = self.clients_updater.next() => self.process_gateway_update(update.unwrap()),
|
||||
@@ -68,9 +72,6 @@ impl PacketReceiver {
|
||||
Some((_gateway_id, message)) = self.gateways_reader.stream_map().next() => {
|
||||
self.process_gateway_messages(message)
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
use task::ShutdownListener;
|
||||
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
|
||||
@@ -176,7 +177,11 @@ impl PacketSender {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_gateways_pinger(&self, pinging_interval: Duration) {
|
||||
pub(crate) fn spawn_gateways_pinger(
|
||||
&self,
|
||||
pinging_interval: Duration,
|
||||
shutdown: ShutdownListener,
|
||||
) {
|
||||
let gateway_pinger = GatewayPinger::new(
|
||||
self.active_gateway_clients.clone(),
|
||||
self.fresh_gateway_client_data
|
||||
@@ -185,7 +190,7 @@ impl PacketSender {
|
||||
pinging_interval,
|
||||
);
|
||||
|
||||
tokio::spawn(async move { gateway_pinger.run().await });
|
||||
tokio::spawn(async move { gateway_pinger.run(shutdown).await });
|
||||
}
|
||||
|
||||
fn new_gateway_client_handle(
|
||||
@@ -216,9 +221,8 @@ impl PacketSender {
|
||||
Some(fresh_gateway_client_data.bandwidth_controller.clone()),
|
||||
);
|
||||
|
||||
if fresh_gateway_client_data.disabled_credentials_mode {
|
||||
gateway_client.set_disabled_credentials_mode(true)
|
||||
}
|
||||
gateway_client
|
||||
.set_disabled_credentials_mode(fresh_gateway_client_data.disabled_credentials_mode);
|
||||
|
||||
(
|
||||
GatewayClientHandle::new(gateway_client),
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use rocket::fairing::AdHoc;
|
||||
use serde::Serialize;
|
||||
use tap::TapFallible;
|
||||
use tokio::{
|
||||
sync::{watch, RwLock},
|
||||
time,
|
||||
};
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use mixnet_contract_common::{reward_params::EpochRewardParams, MixNodeBond};
|
||||
use task::ShutdownListener;
|
||||
use validator_api_requests::models::InclusionProbability;
|
||||
|
||||
use crate::contract_cache::{Cache, CacheNotification, ValidatorCache};
|
||||
|
||||
const CACHE_TIMOUT_MS: u64 = 100;
|
||||
const MAX_SIMULATION_SAMPLES: u64 = 5000;
|
||||
const MAX_SIMULATION_TIME_SEC: u64 = 15;
|
||||
|
||||
enum NodeStatusCacheError {
|
||||
SimulationFailed,
|
||||
}
|
||||
|
||||
// A node status cache suitable for caching values computed in one sweep, such as active set
|
||||
// inclusion probabilities that are computed for all mixnodes at the same time.
|
||||
//
|
||||
// The cache can be triggered to update on contract cache changes, and/or periodically on a timer.
|
||||
#[derive(Clone)]
|
||||
pub struct NodeStatusCache {
|
||||
inner: Arc<RwLock<NodeStatusCacheInner>>,
|
||||
}
|
||||
|
||||
struct NodeStatusCacheInner {
|
||||
inclusion_probabilities: Cache<InclusionProbabilities>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, schemars::JsonSchema)]
|
||||
pub(crate) struct InclusionProbabilities {
|
||||
pub inclusion_probabilities: Vec<InclusionProbability>,
|
||||
pub samples: u64,
|
||||
pub elapsed: Duration,
|
||||
pub delta_max: f64,
|
||||
pub delta_l2: f64,
|
||||
}
|
||||
|
||||
impl InclusionProbabilities {
|
||||
pub fn node(&self, id: &str) -> Option<&InclusionProbability> {
|
||||
self.inclusion_probabilities.iter().find(|x| x.id == id)
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeStatusCache {
|
||||
fn new() -> Self {
|
||||
NodeStatusCache {
|
||||
inner: Arc::new(RwLock::new(NodeStatusCacheInner::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage() -> AdHoc {
|
||||
AdHoc::on_ignite("Node Status Cache", |rocket| async {
|
||||
rocket.manage(Self::new())
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_cache(&self, inclusion_probabilities: InclusionProbabilities) {
|
||||
match time::timeout(Duration::from_millis(CACHE_TIMOUT_MS), self.inner.write()).await {
|
||||
Ok(mut cache) => {
|
||||
cache
|
||||
.inclusion_probabilities
|
||||
.update(inclusion_probabilities);
|
||||
}
|
||||
Err(e) => error!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn inclusion_probabilities(&self) -> Option<Cache<InclusionProbabilities>> {
|
||||
match time::timeout(Duration::from_millis(CACHE_TIMOUT_MS), self.inner.read()).await {
|
||||
Ok(cache) => Some(cache.inclusion_probabilities.clone()),
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeStatusCacheInner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inclusion_probabilities: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Long running task responsible of keeping the cache up-to-date.
|
||||
pub struct NodeStatusCacheRefresher {
|
||||
cache: NodeStatusCache,
|
||||
contract_cache: ValidatorCache,
|
||||
contract_cache_listener: watch::Receiver<CacheNotification>,
|
||||
fallback_caching_interval: Duration,
|
||||
}
|
||||
|
||||
impl NodeStatusCacheRefresher {
|
||||
pub(crate) fn new(
|
||||
cache: NodeStatusCache,
|
||||
contract_cache: ValidatorCache,
|
||||
contract_cache_listener: watch::Receiver<CacheNotification>,
|
||||
fallback_caching_interval: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
cache,
|
||||
contract_cache,
|
||||
contract_cache_listener,
|
||||
fallback_caching_interval,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, mut shutdown: ShutdownListener) {
|
||||
let mut fallback_interval = time::interval(self.fallback_caching_interval);
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("NodeStatusCacheRefresher: Received shutdown");
|
||||
}
|
||||
// Update node status cache when the contract cache / validator cache is updated
|
||||
Ok(_) = self.contract_cache_listener.changed() => {
|
||||
tokio::select! {
|
||||
_ = self.update_on_notify(&mut fallback_interval) => (),
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("NodeStatusCacheRefresher: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
// ... however, if we don't receive any notifications we fall back to periodic
|
||||
// refreshes
|
||||
_ = fallback_interval.tick() => {
|
||||
tokio::select! {
|
||||
_ = self.update_on_timer() => (),
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("NodeStatusCacheRefresher: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("NodeStatusCacheRefresher: Exiting");
|
||||
}
|
||||
|
||||
async fn update_on_notify(&self, fallback_interval: &mut time::Interval) {
|
||||
log::debug!(
|
||||
"Validator cache event detected: {:?}",
|
||||
&*self.contract_cache_listener.borrow(),
|
||||
);
|
||||
let _ = self.refresh_cache().await;
|
||||
fallback_interval.reset();
|
||||
}
|
||||
|
||||
async fn update_on_timer(&self) {
|
||||
log::debug!("Timed trigger for the node status cache");
|
||||
let have_contract_cache_data =
|
||||
*self.contract_cache_listener.borrow() != CacheNotification::Start;
|
||||
|
||||
if have_contract_cache_data {
|
||||
let _ = self.refresh_cache().await;
|
||||
} else {
|
||||
log::trace!(
|
||||
"Skipping updating node status cache, is the contract cache not yet available?"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_cache(&self) -> Result<(), NodeStatusCacheError> {
|
||||
log::info!("Updating node status cache");
|
||||
let mixnode_bonds = self.contract_cache.mixnodes().await;
|
||||
let params = self.contract_cache.epoch_reward_params().await.into_inner();
|
||||
let inclusion_probabilities = compute_inclusion_probabilities(&mixnode_bonds, params)
|
||||
.ok_or_else(|| {
|
||||
error!(
|
||||
"Failed to simulate selection probabilties for mixnodes, not updating cache"
|
||||
);
|
||||
NodeStatusCacheError::SimulationFailed
|
||||
})?;
|
||||
|
||||
self.cache.update_cache(inclusion_probabilities).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inclusion_probabilities(
|
||||
mixnode_bonds: &[MixNodeBond],
|
||||
params: EpochRewardParams,
|
||||
) -> Option<InclusionProbabilities> {
|
||||
let active_set_size = params
|
||||
.active_set_size()
|
||||
.try_into()
|
||||
.tap_err(|e| error!("Active set size unexpectantly large: {e}"))
|
||||
.ok()?;
|
||||
let standby_set_size = (params.rewarded_set_size() - params.active_set_size())
|
||||
.try_into()
|
||||
.tap_err(|e| error!("Active set size larger than rewarded set size, a contradiction: {e}"))
|
||||
.ok()?;
|
||||
|
||||
// Unzip list of total bonds into ids and bonds.
|
||||
// We need to go through this zip/unzip procedure to make sure we have matching identities
|
||||
// for the input to the simulator, which assumes the identity is the position in the vec
|
||||
let (ids, mixnode_total_bonds) = unzip_into_mixnode_ids_and_total_bonds(mixnode_bonds);
|
||||
|
||||
// Compute inclusion probabilitites and keep track of how long time it took.
|
||||
let mut rng = rand::thread_rng();
|
||||
let results = inclusion_probability::simulate_selection_probability_mixnodes(
|
||||
&mixnode_total_bonds,
|
||||
active_set_size,
|
||||
standby_set_size,
|
||||
MAX_SIMULATION_SAMPLES,
|
||||
Duration::from_secs(MAX_SIMULATION_TIME_SEC),
|
||||
&mut rng,
|
||||
)
|
||||
.tap_err(|err| error!("{err}"))
|
||||
.ok()?;
|
||||
|
||||
Some(InclusionProbabilities {
|
||||
inclusion_probabilities: zip_ids_together_with_results(&ids, &results),
|
||||
samples: results.samples,
|
||||
elapsed: results.time,
|
||||
delta_max: results.delta_max,
|
||||
delta_l2: results.delta_l2,
|
||||
})
|
||||
}
|
||||
|
||||
fn unzip_into_mixnode_ids_and_total_bonds(
|
||||
mixnode_bonds: &[MixNodeBond],
|
||||
) -> (Vec<&String>, Vec<u128>) {
|
||||
mixnode_bonds
|
||||
.iter()
|
||||
.filter_map(|m| m.total_bond().map(|b| (m.identity(), b)))
|
||||
.unzip()
|
||||
}
|
||||
|
||||
fn zip_ids_together_with_results(
|
||||
ids: &[&String],
|
||||
results: &inclusion_probability::SelectionProbability,
|
||||
) -> Vec<InclusionProbability> {
|
||||
ids.iter()
|
||||
.zip(results.active_set_probability.iter())
|
||||
.zip(results.reserve_set_probability.iter())
|
||||
.map(|((id, a), r)| InclusionProbability {
|
||||
id: (*id).to_string(),
|
||||
in_active: *a,
|
||||
in_reserve: *r,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) use cache::{NodeStatusCache, NodeStatusCacheRefresher};
|
||||
|
||||
use okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings};
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod local_guard;
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod routes;
|
||||
@@ -35,6 +38,7 @@ pub(crate) fn node_status_routes(
|
||||
routes::get_mixnode_inclusion_probability,
|
||||
routes::get_mixnode_avg_uptime,
|
||||
routes::get_mixnode_avg_uptimes,
|
||||
routes::get_mixnode_inclusion_probabilities,
|
||||
]
|
||||
} else {
|
||||
// in the minimal variant we would not have access to endpoints relying on existence
|
||||
@@ -43,6 +47,7 @@ pub(crate) fn node_status_routes(
|
||||
routes::get_mixnode_status,
|
||||
routes::get_mixnode_stake_saturation,
|
||||
routes::get_mixnode_inclusion_probability,
|
||||
routes::get_mixnode_inclusion_probabilities,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user