Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c1dec4b21 | |||
| 0492af4bf9 | |||
| 2e6b2b49dc | |||
| ca180ca6c2 | |||
| e5efc18912 | |||
| f01713c1ff | |||
| fd19fb529c | |||
| 66b2f1f051 | |||
| 090efa7263 | |||
| 310de00694 | |||
| 080356f46b | |||
| 7543e4b997 | |||
| f42588a985 | |||
| f8b4faf974 | |||
| 2d7b2b1fda | |||
| fdd5b55e14 |
@@ -79,9 +79,6 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Build release contracts
|
||||
run: make wasm
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-contracts-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-contracts-') && github.event_name == 'release' }}
|
||||
runs-on: [self-hosted, custom-runner-linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -19,9 +19,6 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Build release contracts
|
||||
run: make wasm
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: CI for Network Explorer API
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
NETWORK: mainnet
|
||||
|
||||
jobs:
|
||||
publish-nym:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-explorer-api-') && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check the release tag starts with `nym-explorer-api-`
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
script: |
|
||||
core.setFailed('Release tag did not start with nym-explorer-api-...')
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Build all explorer-api
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path explorer-api/Cargo.toml --workspace --release
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
target/release/explorer-api
|
||||
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/explorer-api
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Publish Nym CLI binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
NETWORK: mainnet
|
||||
|
||||
jobs:
|
||||
publish-nym-cli:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-cli-') && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Check the release tag starts with `nym-cli-`
|
||||
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
|
||||
@@ -10,7 +10,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -10,7 +10,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -16,7 +16,7 @@ env:
|
||||
|
||||
jobs:
|
||||
publish-nym:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -10,7 +10,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -9,7 +9,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -4,40 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v1.1.12] (2023-03-07)
|
||||
|
||||
- Fix generated docs for mixnet and vesting contract on docs.rs ([#3093])
|
||||
- Introduce a way of injecting topology into the client ([#3044])
|
||||
- Update mixnet TypeScript client methods #1 ([#2783])
|
||||
- Update tooltips for routing and average score ([#3133])
|
||||
- update selected service provider description style ([#3128])
|
||||
|
||||
[#3093]: https://github.com/nymtech/nym/issues/3093
|
||||
[#3044]: https://github.com/nymtech/nym/issues/3044
|
||||
[#2783]: https://github.com/nymtech/nym/issues/2783
|
||||
[#3133]: https://github.com/nymtech/nym/pull/3133
|
||||
[#3128]: https://github.com/nymtech/nym/pull/3128
|
||||
|
||||
## [v1.1.11] (2023-02-28)
|
||||
|
||||
- Fix empty dealer set loop ([#3105])
|
||||
- The nym-api db.sqlite is broken when trying to run against it it in `enabled-credentials-mode true` there is an ordering issue with migrations when using the credential binary to purchase bandwidth ([#3100])
|
||||
- Feature/latency based gateway selection ([#3081])
|
||||
- Fix the credential binary to handle transactions to sleep when in non-inProgress epochs ([#3057])
|
||||
- Publish mixnet contract to crates.io ([#1919])
|
||||
- Publish vesting contract to crates.io ([#1920])
|
||||
- Feature/update checker to use master ([#3097])
|
||||
- Feature/improve binary checks ([#3094])
|
||||
|
||||
[#3105]: https://github.com/nymtech/nym/issues/3105
|
||||
[#3100]: https://github.com/nymtech/nym/issues/3100
|
||||
[#3081]: https://github.com/nymtech/nym/pull/3081
|
||||
[#3057]: https://github.com/nymtech/nym/issues/3057
|
||||
[#1919]: https://github.com/nymtech/nym/issues/1919
|
||||
[#1920]: https://github.com/nymtech/nym/issues/1920
|
||||
[#3097]: https://github.com/nymtech/nym/pull/3097
|
||||
[#3094]: https://github.com/nymtech/nym/pull/3094
|
||||
|
||||
## [v1.1.10] (2023-02-21)
|
||||
|
||||
- Verloc listener causing mixnode unexpected shutdown ([#3038])
|
||||
|
||||
Generated
+91
-33
@@ -677,7 +677,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "client-core"
|
||||
version = "1.1.11"
|
||||
version = "1.1.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dashmap 5.4.0",
|
||||
@@ -705,8 +705,6 @@ dependencies = [
|
||||
"time 0.3.17",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite 0.14.0",
|
||||
"tungstenite 0.13.0",
|
||||
"url",
|
||||
"validator-client",
|
||||
"wasm-bindgen",
|
||||
@@ -904,6 +902,21 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-contract-testing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"cosmwasm-std",
|
||||
"cosmwasm-storage",
|
||||
"cw-storage-plus",
|
||||
"hex",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-crypto"
|
||||
version = "1.0.0"
|
||||
@@ -943,6 +956,16 @@ dependencies = [
|
||||
"uint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-storage"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d18403b07304d15d304dad11040d45bbcaf78d603b4be3fb5e2685c16f9229b5"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.5"
|
||||
@@ -1713,11 +1736,31 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "1.1.3"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401"
|
||||
checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9"
|
||||
dependencies = [
|
||||
"enum-iterator-derive",
|
||||
"enum-iterator-derive 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91a4ec26efacf4aeff80887a175a419493cb6f8b5480d26387eb0bd038976187"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1782,7 +1825,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "explorer-api"
|
||||
version = "1.1.11"
|
||||
version = "1.1.10"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap 4.1.4",
|
||||
@@ -2187,9 +2230,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.14.4"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c"
|
||||
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -2897,9 +2940,9 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.13.5+1.4.5"
|
||||
version = "0.14.2+1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba"
|
||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3261,7 +3304,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.12"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3342,7 +3385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-bin-common"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap 4.1.4",
|
||||
"clap_complete",
|
||||
@@ -3351,7 +3394,7 @@ dependencies = [
|
||||
"pretty_env_logger",
|
||||
"semver 0.11.0",
|
||||
"serde",
|
||||
"vergen",
|
||||
"vergen 7.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3371,7 +3414,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.11"
|
||||
version = "1.1.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.13.1",
|
||||
@@ -3429,7 +3472,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.11"
|
||||
version = "1.1.10"
|
||||
dependencies = [
|
||||
"clap 4.1.4",
|
||||
"client-core",
|
||||
@@ -3478,7 +3521,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-std",
|
||||
@@ -3490,7 +3533,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes 0.8.2",
|
||||
"blake3",
|
||||
@@ -3523,7 +3566,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-gateway"
|
||||
version = "1.1.11"
|
||||
version = "1.1.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3581,7 +3624,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-std",
|
||||
@@ -3600,7 +3643,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnode"
|
||||
version = "1.1.12"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
@@ -3655,7 +3698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.11"
|
||||
version = "1.1.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"clap 4.1.4",
|
||||
@@ -3693,7 +3736,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-statistics"
|
||||
version = "1.1.11"
|
||||
version = "1.1.10"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"log",
|
||||
@@ -3737,7 +3780,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pem",
|
||||
]
|
||||
@@ -3756,7 +3799,6 @@ dependencies = [
|
||||
"nym-network-defaults",
|
||||
"nym-sphinx",
|
||||
"nym-task",
|
||||
"nym-topology",
|
||||
"pretty_env_logger",
|
||||
"rand 0.7.3",
|
||||
"tap",
|
||||
@@ -3769,7 +3811,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.11"
|
||||
version = "1.1.10"
|
||||
dependencies = [
|
||||
"clap 4.1.4",
|
||||
"client-core",
|
||||
@@ -3929,7 +3971,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-sphinx-types"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"sphinx-packet",
|
||||
]
|
||||
@@ -3964,7 +4006,6 @@ dependencies = [
|
||||
name = "nym-topology"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bs58",
|
||||
"log",
|
||||
"nym-bin-common",
|
||||
@@ -4014,12 +4055,12 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen",
|
||||
"vergen 5.1.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"nym-contracts-common",
|
||||
@@ -6898,13 +6939,30 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "7.4.3"
|
||||
version = "5.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "447f9238a4553957277b3ee09d80babeae0811f1b3baefb093de1c0448437a37"
|
||||
checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"enum-iterator",
|
||||
"chrono",
|
||||
"enum-iterator 0.8.1",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
"rustversion",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "7.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"enum-iterator 1.2.0",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
|
||||
+2
-1
@@ -36,6 +36,7 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/testing",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/mobile-storage",
|
||||
"common/credential-storage",
|
||||
@@ -105,7 +106,7 @@ edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.64"
|
||||
async-trait = "0.1.63"
|
||||
cfg-if = "1.0.0"
|
||||
dotenv = "0.15.0"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
@@ -4,7 +4,7 @@ no-clippy: build cargo-test wasm fmt
|
||||
happy: fmt clippy-happy test
|
||||
clippy-all: clippy-main clippy-main-examples clippy-all-contracts clippy-all-wallet clippy-all-connect clippy-all-connect-mobile clippy-all-wasm-client
|
||||
clippy-happy: clippy-happy-main clippy-happy-contracts clippy-happy-wallet clippy-happy-connect clippy-happy-connect-mobile
|
||||
cargo-test: test-main test-contracts test-wallet test-connect test-connect-mobile
|
||||
cargo-test: test-main test-contracts test-contracts-integration test-wallet test-connect test-connect-mobile
|
||||
cargo-test-expensive: test-main-expensive test-contracts-expensive test-wallet-expensive test-connect-expensive
|
||||
build: build-contracts build-wallet build-main build-main-examples build-connect build-connect-mobile build-wasm-client
|
||||
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect fmt-connect-mobile fmt-wasm-client
|
||||
@@ -33,9 +33,8 @@ clippy-main-examples:
|
||||
clippy-wasm:
|
||||
cargo clippy --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown --workspace -- -D warnings
|
||||
|
||||
|
||||
clippy-all-contracts:
|
||||
cargo clippy --workspace --manifest-path contracts/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
|
||||
cargo clippy --workspace --manifest-path contracts/Cargo.toml --target wasm32-unknown-unknown -- -D warnings
|
||||
|
||||
clippy-all-wallet:
|
||||
cargo clippy --workspace --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
|
||||
@@ -61,6 +60,9 @@ test-contracts:
|
||||
test-contracts-expensive:
|
||||
cargo test --manifest-path contracts/Cargo.toml --all-features -- --ignored
|
||||
|
||||
test-contracts-integration:
|
||||
cargo test --manifest-path contracts/integration-tests/Cargo.toml
|
||||
|
||||
test-wallet:
|
||||
cargo test --manifest-path nym-wallet/Cargo.toml --all-features
|
||||
|
||||
@@ -126,8 +128,6 @@ fmt-wasm-client:
|
||||
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown
|
||||
wasm-opt -Os contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm -o contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
|
||||
wasm-opt -Os contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm -o contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
|
||||
|
||||
mixnet-opt: wasm
|
||||
cd contracts/mixnet && make opt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "client-core"
|
||||
version = "1.1.12"
|
||||
version = "1.1.10"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
@@ -8,7 +8,7 @@ rust-version = "1.66"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
async-trait = { version = "0.1.58" }
|
||||
dirs = "4.0"
|
||||
dashmap = "5.4.0"
|
||||
futures = "0.3"
|
||||
@@ -20,7 +20,6 @@ serde_json = "1.0.89"
|
||||
tap = "1.0.1"
|
||||
thiserror = "1.0.34"
|
||||
url = { version ="2.2", features = ["serde"] }
|
||||
tungstenite = { version = "0.13.0", default-features = false }
|
||||
tokio = { version = "1.24.1", features = ["macros"]}
|
||||
time = "0.3.17"
|
||||
|
||||
@@ -45,9 +44,6 @@ features = ["time"]
|
||||
version = "1.24.1"
|
||||
features = ["time"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
|
||||
version = "0.14"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
version = "0.6.2"
|
||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
||||
@@ -69,7 +65,6 @@ features = ["futures"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils]
|
||||
path = "../../common/wasm-utils"
|
||||
features = ["websocket"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.time]
|
||||
version = "0.3.17"
|
||||
|
||||
@@ -15,7 +15,6 @@ use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyCon
|
||||
use crate::client::replies::reply_storage::{
|
||||
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
|
||||
};
|
||||
use crate::client::topology_control::nym_api_provider::NymApiTopologyProvider;
|
||||
use crate::client::topology_control::{
|
||||
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
|
||||
};
|
||||
@@ -38,12 +37,10 @@ use nym_sphinx::addressing::nodes::NodeIdentity;
|
||||
use nym_sphinx::receiver::ReconstructedMessage;
|
||||
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tap::TapFallible;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use validator_client::nyxd::CosmWasmClient;
|
||||
|
||||
@@ -93,7 +90,6 @@ impl ClientOutput {
|
||||
pub struct ClientState {
|
||||
pub shared_lane_queue_lengths: LaneQueueLengths,
|
||||
pub reply_controller_sender: ReplyControllerSender,
|
||||
pub topology_accessor: TopologyAccessor,
|
||||
}
|
||||
|
||||
pub enum ClientInputStatus {
|
||||
@@ -158,7 +154,6 @@ pub struct BaseClientBuilder<'a, B, C: Clone> {
|
||||
nym_api_endpoints: Vec<Url>,
|
||||
reply_storage_backend: B,
|
||||
|
||||
custom_topology_provider: Option<Box<dyn TopologyProvider>>,
|
||||
bandwidth_controller: Option<BandwidthController<C>>,
|
||||
key_manager: KeyManager,
|
||||
}
|
||||
@@ -182,7 +177,6 @@ where
|
||||
bandwidth_controller,
|
||||
reply_storage_backend,
|
||||
key_manager,
|
||||
custom_topology_provider: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,17 +195,11 @@ where
|
||||
disabled_credentials: credentials_toggle.is_disabled(),
|
||||
nym_api_endpoints,
|
||||
reply_storage_backend,
|
||||
custom_topology_provider: None,
|
||||
bandwidth_controller,
|
||||
key_manager,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_topology_provider(mut self, provider: Box<dyn TopologyProvider>) -> Self {
|
||||
self.custom_topology_provider = Some(provider);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_mix_recipient(&self) -> Recipient {
|
||||
Recipient::new(
|
||||
*self.key_manager.identity_keypair().public_key(),
|
||||
@@ -316,7 +304,7 @@ where
|
||||
}
|
||||
let gateway_address = self.gateway_config.gateway_listener.clone();
|
||||
if gateway_address.is_empty() {
|
||||
return Err(ClientCoreError::GatewayAddressUnknown);
|
||||
return Err(ClientCoreError::GatwayAddressUnknown);
|
||||
}
|
||||
|
||||
let gateway_identity = identity::PublicKey::from_base58_string(gateway_id)
|
||||
@@ -353,38 +341,25 @@ where
|
||||
Ok(gateway_client)
|
||||
}
|
||||
|
||||
fn setup_topology_provider(
|
||||
custom_provider: Option<Box<dyn TopologyProvider>>,
|
||||
nym_api_urls: Vec<Url>,
|
||||
) -> Box<dyn TopologyProvider> {
|
||||
// if no custom provider was ... provided ..., create one using nym-api
|
||||
custom_provider.unwrap_or_else(|| {
|
||||
Box::new(NymApiTopologyProvider::new(
|
||||
nym_api_urls,
|
||||
env!("CARGO_PKG_VERSION").to_string(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// future responsible for periodically polling directory server and updating
|
||||
// the current global view of topology
|
||||
async fn start_topology_refresher(
|
||||
topology_provider: Box<dyn TopologyProvider>,
|
||||
nym_api_urls: Vec<Url>,
|
||||
refresh_rate: Duration,
|
||||
topology_accessor: TopologyAccessor,
|
||||
shutdown: TaskClient,
|
||||
) -> Result<(), ClientCoreError> {
|
||||
let topology_refresher_config = TopologyRefresherConfig::new(refresh_rate);
|
||||
|
||||
let mut topology_refresher = TopologyRefresher::new(
|
||||
topology_refresher_config,
|
||||
topology_accessor,
|
||||
topology_provider,
|
||||
let topology_refresher_config = TopologyRefresherConfig::new(
|
||||
nym_api_urls,
|
||||
refresh_rate,
|
||||
env!("CARGO_PKG_VERSION").to_string(),
|
||||
);
|
||||
let mut topology_refresher =
|
||||
TopologyRefresher::new(topology_refresher_config, topology_accessor);
|
||||
// before returning, block entire runtime to refresh the current network view so that any
|
||||
// components depending on topology would see a non-empty view
|
||||
info!("Obtaining initial network topology");
|
||||
topology_refresher.try_refresh().await;
|
||||
topology_refresher.refresh().await;
|
||||
|
||||
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
|
||||
log::error!(
|
||||
@@ -493,12 +468,8 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
let topology_provider = Self::setup_topology_provider(
|
||||
self.custom_topology_provider.take(),
|
||||
self.nym_api_endpoints,
|
||||
);
|
||||
Self::start_topology_refresher(
|
||||
topology_provider,
|
||||
self.nym_api_endpoints.clone(),
|
||||
self.debug_config.topology_refresh_rate,
|
||||
shared_topology_accessor.clone(),
|
||||
task_manager.subscribe(),
|
||||
@@ -559,7 +530,7 @@ where
|
||||
self.debug_config,
|
||||
self.key_manager.ack_key(),
|
||||
self_address,
|
||||
shared_topology_accessor.clone(),
|
||||
shared_topology_accessor,
|
||||
sphinx_message_sender,
|
||||
task_manager.subscribe(),
|
||||
);
|
||||
@@ -583,7 +554,6 @@ where
|
||||
client_state: ClientState {
|
||||
shared_lane_queue_lengths,
|
||||
reply_controller_sender,
|
||||
topology_accessor: shared_topology_accessor,
|
||||
},
|
||||
task_manager,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::spawn_future;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::params::DEFAULT_NUM_MIX_HOPS;
|
||||
use nym_topology::{nym_topology_from_detailed, NymTopology, NymTopologyError};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use url::Url;
|
||||
|
||||
// I'm extremely curious why compiler NEVER complained about lack of Debug here before
|
||||
#[derive(Debug)]
|
||||
pub struct TopologyAccessorInner(Option<NymTopology>);
|
||||
|
||||
impl AsRef<Option<NymTopology>> for TopologyAccessorInner {
|
||||
fn as_ref(&self) -> &Option<NymTopology> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TopologyAccessorInner {
|
||||
fn new() -> Self {
|
||||
TopologyAccessorInner(None)
|
||||
}
|
||||
|
||||
fn update(&mut self, new: Option<NymTopology>) {
|
||||
self.0 = new;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyReadPermit<'a> {
|
||||
permit: RwLockReadGuard<'a, TopologyAccessorInner>,
|
||||
}
|
||||
|
||||
impl<'a> Deref for TopologyReadPermit<'a> {
|
||||
type Target = TopologyAccessorInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.permit
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TopologyReadPermit<'a> {
|
||||
/// Using provided topology read permit, tries to get an immutable reference to the underlying
|
||||
/// topology. For obvious reasons the lifetime of the topology reference is bound to the permit.
|
||||
pub(super) fn try_get_valid_topology_ref(
|
||||
&'a self,
|
||||
ack_recipient: &Recipient,
|
||||
packet_recipient: Option<&Recipient>,
|
||||
) -> Result<&'a NymTopology, NymTopologyError> {
|
||||
// 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through?
|
||||
let topology = self
|
||||
.permit
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.ok_or(NymTopologyError::EmptyNetworkTopology)?;
|
||||
|
||||
// 2. does it have any mixnode at all?
|
||||
// 3. does it have any gateways at all?
|
||||
// 4. does it have a mixnode on each layer?
|
||||
topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?;
|
||||
|
||||
// 5. does it contain OUR gateway (so that we could create an ack packet)?
|
||||
if !topology.gateway_exists(ack_recipient.gateway()) {
|
||||
return Err(NymTopologyError::NonExistentGatewayError {
|
||||
identity_key: ack_recipient.gateway().to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 6. for our target recipient, does it contain THEIR gateway (so that we could create
|
||||
if let Some(recipient) = packet_recipient {
|
||||
if !topology.gateway_exists(recipient.gateway()) {
|
||||
return Err(NymTopologyError::NonExistentGatewayError {
|
||||
identity_key: recipient.gateway().to_base58_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(topology)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RwLockReadGuard<'a, TopologyAccessorInner>> for TopologyReadPermit<'a> {
|
||||
fn from(read_permit: RwLockReadGuard<'a, TopologyAccessorInner>) -> Self {
|
||||
TopologyReadPermit {
|
||||
permit: read_permit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TopologyAccessor {
|
||||
// `RwLock` *seems to* be the better approach for this as write access is only requested every
|
||||
// few seconds, while reads are needed every single packet generated.
|
||||
// However, proper benchmarks will be needed to determine if `RwLock` is indeed a better
|
||||
// approach than a `Mutex`
|
||||
inner: Arc<RwLock<TopologyAccessorInner>>,
|
||||
}
|
||||
|
||||
impl TopologyAccessor {
|
||||
pub fn new() -> Self {
|
||||
TopologyAccessor {
|
||||
inner: Arc::new(RwLock::new(TopologyAccessorInner::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_read_permit(&self) -> TopologyReadPermit<'_> {
|
||||
self.inner.read().await.into()
|
||||
}
|
||||
|
||||
async fn update_global_topology(&self, new_topology: Option<NymTopology>) {
|
||||
self.inner.write().await.update(new_topology);
|
||||
}
|
||||
|
||||
// only used by the client at startup to get a slightly more reasonable error message
|
||||
// (currently displays as unused because health checker is disabled due to required changes)
|
||||
pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> {
|
||||
match &self.inner.read().await.0 {
|
||||
None => Err(NymTopologyError::EmptyNetworkTopology),
|
||||
Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TopologyAccessor {
|
||||
fn default() -> Self {
|
||||
TopologyAccessor::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyRefresherConfig {
|
||||
nym_api_urls: Vec<Url>,
|
||||
refresh_rate: Duration,
|
||||
client_version: String,
|
||||
}
|
||||
|
||||
impl TopologyRefresherConfig {
|
||||
pub fn new(nym_api_urls: Vec<Url>, refresh_rate: Duration, client_version: String) -> Self {
|
||||
TopologyRefresherConfig {
|
||||
nym_api_urls,
|
||||
refresh_rate,
|
||||
client_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyRefresher {
|
||||
validator_client: validator_client::client::NymApiClient,
|
||||
client_version: String,
|
||||
|
||||
nym_api_urls: Vec<Url>,
|
||||
topology_accessor: TopologyAccessor,
|
||||
refresh_rate: Duration,
|
||||
|
||||
currently_used_api: usize,
|
||||
was_latest_valid: bool,
|
||||
}
|
||||
|
||||
impl TopologyRefresher {
|
||||
pub fn new(mut cfg: TopologyRefresherConfig, topology_accessor: TopologyAccessor) -> Self {
|
||||
cfg.nym_api_urls.shuffle(&mut thread_rng());
|
||||
|
||||
TopologyRefresher {
|
||||
validator_client: validator_client::client::NymApiClient::new(
|
||||
cfg.nym_api_urls[0].clone(),
|
||||
),
|
||||
client_version: cfg.client_version,
|
||||
nym_api_urls: cfg.nym_api_urls,
|
||||
topology_accessor,
|
||||
refresh_rate: cfg.refresh_rate,
|
||||
currently_used_api: 0,
|
||||
was_latest_valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn use_next_nym_api(&mut self) {
|
||||
if self.nym_api_urls.len() == 1 {
|
||||
warn!("There's only a single nym API available - it won't be possible to use a different one");
|
||||
return;
|
||||
}
|
||||
|
||||
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
|
||||
self.validator_client
|
||||
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
|
||||
}
|
||||
|
||||
/// Verifies whether nodes a reasonably distributed among all mix layers.
|
||||
///
|
||||
/// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3.
|
||||
/// However, this is a rather unrealistic expectation, instead we check whether there exists
|
||||
/// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `topology`: active topology constructed from validator api data
|
||||
fn check_layer_distribution(&self, active_topology: &NymTopology) -> bool {
|
||||
let mixes = active_topology.mixes();
|
||||
let mixnodes_count = active_topology.num_mixnodes();
|
||||
|
||||
if active_topology.gateways().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// trivial check to see if have at least a single node on each layer (regardless of active set size)
|
||||
if mixes.get(&1).is_none() || mixes.get(&2).is_none() || mixes.get(&3).is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let upper_bound = (mixnodes_count as f32 * 0.66) as usize;
|
||||
let lower_bound = (mixnodes_count as f32 * 0.15) as usize;
|
||||
|
||||
let layer1 = mixes.get(&1).unwrap().len();
|
||||
let layer2 = mixes.get(&2).unwrap().len();
|
||||
let layer3 = mixes.get(&3).unwrap().len();
|
||||
|
||||
if layer1 < lower_bound || layer1 > upper_bound {
|
||||
warn!(
|
||||
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
|
||||
mixnodes_count, layer1, layer2, layer3
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if layer2 < lower_bound || layer2 > upper_bound {
|
||||
warn!(
|
||||
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
|
||||
mixnodes_count, layer1, layer2, layer3
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if layer3 < lower_bound || layer3 > upper_bound {
|
||||
warn!(
|
||||
"nodes: {}, layer1: {}, layer2: {}, layer3: {}",
|
||||
mixnodes_count, layer1, layer2, layer3
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
async fn get_current_compatible_topology(&self) -> Option<NymTopology> {
|
||||
// TODO: optimization for the future:
|
||||
// only refresh mixnodes on timer and refresh gateways only when
|
||||
// we have to send to a new, unknown, gateway
|
||||
|
||||
let mixnodes = match self.validator_client.get_cached_active_mixnodes().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network mixnodes - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
let gateways = match self.validator_client.get_cached_gateways().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network gateways - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(gateways) => gateways,
|
||||
};
|
||||
|
||||
let topology = nym_topology_from_detailed(mixnodes, gateways)
|
||||
.filter_system_version(&self.client_version);
|
||||
|
||||
if !self.check_layer_distribution(&topology) {
|
||||
warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used.");
|
||||
None
|
||||
} else {
|
||||
Some(topology)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh(&mut self) {
|
||||
trace!("Refreshing the topology");
|
||||
let new_topology = self.get_current_compatible_topology().await;
|
||||
|
||||
if new_topology.is_none() {
|
||||
self.use_next_nym_api();
|
||||
}
|
||||
|
||||
if new_topology.is_none() && self.was_latest_valid {
|
||||
// if we failed to grab this topology, but the one before it was alright, let's assume
|
||||
// validator had a tiny hiccup and use the old data
|
||||
warn!("we're going to keep on using the old topology for this iteration");
|
||||
self.was_latest_valid = false;
|
||||
return;
|
||||
} else if new_topology.is_some() {
|
||||
self.was_latest_valid = true;
|
||||
}
|
||||
|
||||
self.topology_accessor
|
||||
.update_global_topology(new_topology)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> {
|
||||
self.topology_accessor.ensure_is_routable().await
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
|
||||
spawn_future(async move {
|
||||
debug!("Started TopologyRefresher with graceful shutdown support");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
|
||||
self.refresh_rate,
|
||||
));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut interval =
|
||||
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.next() => {
|
||||
self.refresh().await;
|
||||
},
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("TopologyRefresher: Received shutdown");
|
||||
},
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
log::debug!("TopologyRefresher: Exiting");
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::params::DEFAULT_NUM_MIX_HOPS;
|
||||
use nym_topology::{NymTopology, NymTopologyError};
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Notify, RwLock, RwLockReadGuard};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TopologyAccessorInner {
|
||||
controlled_manually: AtomicBool,
|
||||
released_manual_control: Notify,
|
||||
// `RwLock` *seems to* be the better approach for this as write access is only requested every
|
||||
// few seconds, while reads are needed every single packet generated.
|
||||
// However, proper benchmarks will be needed to determine if `RwLock` is indeed a better
|
||||
// approach than a `Mutex`
|
||||
topology: RwLock<Option<NymTopology>>,
|
||||
}
|
||||
|
||||
impl TopologyAccessorInner {
|
||||
fn new() -> Self {
|
||||
TopologyAccessorInner {
|
||||
controlled_manually: AtomicBool::new(false),
|
||||
released_manual_control: Notify::new(),
|
||||
topology: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(&self, new: Option<NymTopology>) {
|
||||
*self.topology.write().await = new;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyReadPermit<'a> {
|
||||
permit: RwLockReadGuard<'a, Option<NymTopology>>,
|
||||
}
|
||||
|
||||
impl<'a> Deref for TopologyReadPermit<'a> {
|
||||
type Target = Option<NymTopology>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.permit
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TopologyReadPermit<'a> {
|
||||
/// Using provided topology read permit, tries to get an immutable reference to the underlying
|
||||
/// topology. For obvious reasons the lifetime of the topology reference is bound to the permit.
|
||||
pub(crate) fn try_get_valid_topology_ref(
|
||||
&'a self,
|
||||
ack_recipient: &Recipient,
|
||||
packet_recipient: Option<&Recipient>,
|
||||
) -> Result<&'a NymTopology, NymTopologyError> {
|
||||
// 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through?
|
||||
let topology = self
|
||||
.permit
|
||||
.as_ref()
|
||||
.ok_or(NymTopologyError::EmptyNetworkTopology)?;
|
||||
|
||||
// 2. does it have any mixnode at all?
|
||||
// 3. does it have any gateways at all?
|
||||
// 4. does it have a mixnode on each layer?
|
||||
topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?;
|
||||
|
||||
// 5. does it contain OUR gateway (so that we could create an ack packet)?
|
||||
if !topology.gateway_exists(ack_recipient.gateway()) {
|
||||
return Err(NymTopologyError::NonExistentGatewayError {
|
||||
identity_key: ack_recipient.gateway().to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 6. for our target recipient, does it contain THEIR gateway (so that we could create
|
||||
if let Some(recipient) = packet_recipient {
|
||||
if !topology.gateway_exists(recipient.gateway()) {
|
||||
return Err(NymTopologyError::NonExistentGatewayError {
|
||||
identity_key: recipient.gateway().to_base58_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(topology)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RwLockReadGuard<'a, Option<NymTopology>>> for TopologyReadPermit<'a> {
|
||||
fn from(read_permit: RwLockReadGuard<'a, Option<NymTopology>>) -> Self {
|
||||
TopologyReadPermit {
|
||||
permit: read_permit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TopologyAccessor {
|
||||
inner: Arc<TopologyAccessorInner>,
|
||||
}
|
||||
|
||||
impl TopologyAccessor {
|
||||
pub fn new() -> Self {
|
||||
TopologyAccessor {
|
||||
inner: Arc::new(TopologyAccessorInner::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn controlled_manually(&self) -> bool {
|
||||
self.inner.controlled_manually.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub async fn get_read_permit(&self) -> TopologyReadPermit<'_> {
|
||||
self.inner.topology.read().await.into()
|
||||
}
|
||||
|
||||
pub(crate) async fn update_global_topology(&self, new_topology: Option<NymTopology>) {
|
||||
self.inner.update(new_topology).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_released_manual_control(&self) {
|
||||
self.inner.released_manual_control.notified().await
|
||||
}
|
||||
|
||||
pub async fn current_topology(&self) -> Option<NymTopology> {
|
||||
self.inner.topology.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn manually_change_topology(&self, new_topology: NymTopology) {
|
||||
self.inner.controlled_manually.store(true, Ordering::SeqCst);
|
||||
self.inner.update(Some(new_topology)).await;
|
||||
}
|
||||
|
||||
pub fn release_manual_control(&self) {
|
||||
self.inner
|
||||
.controlled_manually
|
||||
.store(false, Ordering::SeqCst);
|
||||
self.inner.released_manual_control.notify_waiters();
|
||||
}
|
||||
|
||||
// only used by the client at startup to get a slightly more reasonable error message
|
||||
// (currently displays as unused because health checker is disabled due to required changes)
|
||||
pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> {
|
||||
match self.inner.topology.read().await.deref() {
|
||||
None => Err(NymTopologyError::EmptyNetworkTopology),
|
||||
Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TopologyAccessor {
|
||||
fn default() -> Self {
|
||||
TopologyAccessor::new()
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::spawn_future;
|
||||
pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use nym_topology::NymTopologyError;
|
||||
use std::time::Duration;
|
||||
|
||||
mod accessor;
|
||||
pub(crate) mod nym_api_provider;
|
||||
|
||||
// TODO: move it to config later
|
||||
const MAX_FAILURE_COUNT: usize = 10;
|
||||
|
||||
pub struct TopologyRefresherConfig {
|
||||
refresh_rate: Duration,
|
||||
}
|
||||
|
||||
impl TopologyRefresherConfig {
|
||||
pub fn new(refresh_rate: Duration) -> Self {
|
||||
TopologyRefresherConfig { refresh_rate }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TopologyRefresher {
|
||||
topology_provider: Box<dyn TopologyProvider>,
|
||||
topology_accessor: TopologyAccessor,
|
||||
|
||||
refresh_rate: Duration,
|
||||
consecutive_failure_count: usize,
|
||||
}
|
||||
|
||||
impl TopologyRefresher {
|
||||
pub fn new(
|
||||
cfg: TopologyRefresherConfig,
|
||||
topology_accessor: TopologyAccessor,
|
||||
topology_provider: Box<dyn TopologyProvider>,
|
||||
) -> Self {
|
||||
TopologyRefresher {
|
||||
topology_provider,
|
||||
topology_accessor,
|
||||
refresh_rate: cfg.refresh_rate,
|
||||
consecutive_failure_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_topology_provider(&mut self, provider: Box<dyn TopologyProvider>) {
|
||||
self.topology_provider = provider;
|
||||
}
|
||||
|
||||
pub async fn try_refresh(&mut self) {
|
||||
trace!("Refreshing the topology");
|
||||
|
||||
if self.topology_accessor.controlled_manually() {
|
||||
info!("topology is being controlled manually - we're going to wait until the control is released...");
|
||||
self.topology_accessor
|
||||
.wait_for_released_manual_control()
|
||||
.await;
|
||||
}
|
||||
|
||||
let new_topology = self.topology_provider.get_new_topology().await;
|
||||
if new_topology.is_none() {
|
||||
warn!("failed to obtain new network topology");
|
||||
}
|
||||
|
||||
if new_topology.is_none() && self.consecutive_failure_count < MAX_FAILURE_COUNT {
|
||||
// if we failed to grab this topology, but the one before it was alright, let's assume
|
||||
// validator had a tiny hiccup and use the old data
|
||||
warn!("we're going to keep on using the old topology for this iteration");
|
||||
self.consecutive_failure_count += 1;
|
||||
return;
|
||||
} else if new_topology.is_some() {
|
||||
self.consecutive_failure_count = 0;
|
||||
}
|
||||
|
||||
self.topology_accessor
|
||||
.update_global_topology(new_topology)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> {
|
||||
self.topology_accessor.ensure_is_routable().await
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
|
||||
spawn_future(async move {
|
||||
debug!("Started TopologyRefresher with graceful shutdown support");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
|
||||
self.refresh_rate,
|
||||
));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut interval =
|
||||
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.next() => {
|
||||
self.try_refresh().await;
|
||||
},
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("TopologyRefresher: Received shutdown");
|
||||
},
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
log::debug!("TopologyRefresher: Exiting");
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{error, warn};
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use nym_topology::{nym_topology_from_detailed, NymTopology, NymTopologyError};
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use url::Url;
|
||||
|
||||
pub(crate) struct NymApiTopologyProvider {
|
||||
validator_client: validator_client::client::NymApiClient,
|
||||
nym_api_urls: Vec<Url>,
|
||||
|
||||
client_version: String,
|
||||
currently_used_api: usize,
|
||||
}
|
||||
|
||||
impl NymApiTopologyProvider {
|
||||
pub(crate) fn new(mut nym_api_urls: Vec<Url>, client_version: String) -> Self {
|
||||
nym_api_urls.shuffle(&mut thread_rng());
|
||||
|
||||
NymApiTopologyProvider {
|
||||
validator_client: validator_client::client::NymApiClient::new(nym_api_urls[0].clone()),
|
||||
nym_api_urls,
|
||||
client_version,
|
||||
currently_used_api: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn use_next_nym_api(&mut self) {
|
||||
if self.nym_api_urls.len() == 1 {
|
||||
warn!("There's only a single nym API available - it won't be possible to use a different one");
|
||||
return;
|
||||
}
|
||||
|
||||
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
|
||||
self.validator_client
|
||||
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
|
||||
}
|
||||
|
||||
/// Verifies whether nodes a reasonably distributed among all mix layers.
|
||||
///
|
||||
/// In ideal world we would have 33% nodes on layer 1, 33% on layer 2 and 33% on layer 3.
|
||||
/// However, this is a rather unrealistic expectation, instead we check whether there exists
|
||||
/// a layer with more than 66% of nodes or with fewer than 15% and if so, we trigger a failure.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `topology`: active topology constructed from validator api data
|
||||
fn check_layer_distribution(
|
||||
&self,
|
||||
active_topology: &NymTopology,
|
||||
) -> Result<(), NymTopologyError> {
|
||||
let lower_threshold = 0.15;
|
||||
let upper_threshold = 0.66;
|
||||
active_topology.ensure_even_layer_distribution(lower_threshold, upper_threshold)
|
||||
}
|
||||
|
||||
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
|
||||
let mixnodes = match self.validator_client.get_cached_active_mixnodes().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network mixnodes - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
let gateways = match self.validator_client.get_cached_gateways().await {
|
||||
Err(err) => {
|
||||
error!("failed to get network gateways - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(gateways) => gateways,
|
||||
};
|
||||
|
||||
let topology = nym_topology_from_detailed(mixnodes, gateways)
|
||||
.filter_system_version(&self.client_version);
|
||||
|
||||
if let Err(err) = self.check_layer_distribution(&topology) {
|
||||
warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used: {err}");
|
||||
self.use_next_nym_api();
|
||||
None
|
||||
} else {
|
||||
Some(topology)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hehe, wasm
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl TopologyProvider for NymApiTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
self.get_current_compatible_topology().await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl TopologyProvider for NymApiTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
self.get_current_compatible_topology().await
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_config::defaults::NymNetworkDetails;
|
||||
use nym_config::{NymConfig, OptionalSet, CRED_DB_FILE_NAME};
|
||||
use nym_config::{NymConfig, OptionalSet, DB_FILE_NAME};
|
||||
use nym_sphinx::params::PacketSize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
@@ -579,7 +579,7 @@ impl<T: NymConfig> Client<T> {
|
||||
}
|
||||
|
||||
fn default_database_path(id: &str) -> PathBuf {
|
||||
T::default_data_directory(id).join(CRED_DB_FILE_NAME)
|
||||
T::default_data_directory(id).join(DB_FILE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use gateway_client::error::GatewayClientError;
|
||||
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
|
||||
use nym_topology::gateway::GatewayConversionError;
|
||||
use nym_topology::NymTopologyError;
|
||||
use validator_client::ValidatorClientError;
|
||||
|
||||
@@ -54,32 +53,7 @@ pub enum ClientCoreError {
|
||||
GatewayOwnerUnknown,
|
||||
|
||||
#[error("The address of the gateway is unknown - did you run init?")]
|
||||
GatewayAddressUnknown,
|
||||
|
||||
#[error("The gateway is malformed: {source}")]
|
||||
MalformedGateway {
|
||||
#[from]
|
||||
source: GatewayConversionError,
|
||||
},
|
||||
|
||||
#[error("failed to establish connection to gateway: {source}")]
|
||||
GatewayConnectionFailure {
|
||||
#[from]
|
||||
source: tungstenite::Error,
|
||||
},
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[error("failed to establish gateway connection (wasm)")]
|
||||
GatewayJsConnectionFailure,
|
||||
|
||||
#[error("Gateway connection was abruptly closed")]
|
||||
GatewayConnectionAbruptlyClosed,
|
||||
|
||||
#[error("Timed out while trying to establish gateway connection")]
|
||||
GatewayConnectionTimeout,
|
||||
|
||||
#[error("No ping measurements for the gateway ({identity}) performed")]
|
||||
NoGatewayMeasurements { identity: String },
|
||||
GatwayAddressUnknown,
|
||||
|
||||
#[error("failed to register receiver for reconstructed mixnet messages")]
|
||||
FailedToRegisterReceiver,
|
||||
|
||||
@@ -6,223 +6,52 @@ use crate::{
|
||||
config::{persistence::key_pathfinder::ClientKeyPathfinder, Config},
|
||||
error::ClientCoreError,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use gateway_client::wasm_mockups::SigningNyxdClient;
|
||||
use gateway_client::GatewayClient;
|
||||
use gateway_requests::registration::handshake::SharedKeys;
|
||||
use log::{debug, info, trace, warn};
|
||||
use nym_config::NymConfig;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_topology::{filter::VersionFilterable, gateway};
|
||||
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tap::TapFallible;
|
||||
use tungstenite::Message;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::net::TcpStream;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::Instant;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::connect_async;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use validator_client::nyxd::SigningNyxdClient;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
type WsConn = WebSocketStream<MaybeTlsStream<TcpStream>>;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use gateway_client::wasm_mockups::SigningNyxdClient;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_timer::Instant;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_utils::websocket::JSWebsocket;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
type WsConn = JSWebsocket;
|
||||
|
||||
const MEASUREMENTS: usize = 3;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const CONN_TIMEOUT: Duration = Duration::from_millis(1500);
|
||||
const PING_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
struct GatewayWithLatency {
|
||||
gateway: gateway::Node,
|
||||
latency: Duration,
|
||||
}
|
||||
|
||||
impl GatewayWithLatency {
|
||||
fn new(gateway: gateway::Node, latency: Duration) -> Self {
|
||||
GatewayWithLatency { gateway, latency }
|
||||
}
|
||||
}
|
||||
|
||||
async fn current_gateways<R: Rng>(
|
||||
rng: &mut R,
|
||||
nym_apis: Vec<Url>,
|
||||
) -> Result<Vec<gateway::Node>, ClientCoreError> {
|
||||
let nym_api = nym_apis
|
||||
.choose(rng)
|
||||
pub(super) async fn query_gateway_details(
|
||||
validator_servers: Vec<Url>,
|
||||
chosen_gateway_id: Option<identity::PublicKey>,
|
||||
) -> Result<gateway::Node, ClientCoreError> {
|
||||
let nym_api = validator_servers
|
||||
.choose(&mut thread_rng())
|
||||
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
|
||||
let client = validator_client::client::NymApiClient::new(nym_api.clone());
|
||||
let validator_client = validator_client::client::NymApiClient::new(nym_api.clone());
|
||||
|
||||
log::trace!("Fetching list of gateways from: {}", nym_api);
|
||||
|
||||
let gateways = client.get_cached_gateways().await?;
|
||||
let gateways = validator_client.get_cached_gateways().await?;
|
||||
let valid_gateways = gateways
|
||||
.into_iter()
|
||||
.filter_map(|gateway| gateway.try_into().ok())
|
||||
.collect::<Vec<gateway::Node>>();
|
||||
|
||||
// we were always filtering by version so I'm not removing that 'feature'
|
||||
let filtered_gateways = valid_gateways.filter_by_version(env!("CARGO_PKG_VERSION"));
|
||||
Ok(filtered_gateways)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
|
||||
match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await {
|
||||
Err(_elapsed) => Err(ClientCoreError::GatewayConnectionTimeout),
|
||||
Ok(Err(conn_failure)) => Err(conn_failure.into()),
|
||||
Ok(Ok((stream, _))) => Ok(stream),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
|
||||
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
|
||||
}
|
||||
|
||||
async fn measure_latency(gateway: gateway::Node) -> Result<GatewayWithLatency, ClientCoreError> {
|
||||
let addr = gateway.clients_address();
|
||||
trace!(
|
||||
"establishing connection to {} ({addr})...",
|
||||
gateway.identity_key,
|
||||
);
|
||||
let mut stream = connect(&addr).await?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for _ in 0..MEASUREMENTS {
|
||||
let measurement_future = async {
|
||||
let ping_content = vec![1, 2, 3];
|
||||
let start = Instant::now();
|
||||
stream.send(Message::Ping(ping_content.clone())).await?;
|
||||
|
||||
match stream.next().await {
|
||||
Some(Ok(Message::Pong(content))) => {
|
||||
if content == ping_content {
|
||||
let elapsed = Instant::now().duration_since(start);
|
||||
trace!("current ping time: {elapsed:?}");
|
||||
results.push(elapsed);
|
||||
} else {
|
||||
warn!("received a pong message with different content? wtf.")
|
||||
}
|
||||
}
|
||||
Some(Ok(_)) => warn!("received a message that's not a pong!"),
|
||||
Some(Err(err)) => return Err(err.into()),
|
||||
None => return Err(ClientCoreError::GatewayConnectionAbruptlyClosed),
|
||||
}
|
||||
|
||||
Ok::<(), ClientCoreError>(())
|
||||
};
|
||||
|
||||
// thanks to wasm we can't use tokio::time::timeout : (
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let timeout = tokio::time::sleep(PING_TIMEOUT);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
tokio::pin!(timeout);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut timeout = wasm_timer::Delay::new(PING_TIMEOUT);
|
||||
|
||||
tokio::select! {
|
||||
_ = &mut timeout => {
|
||||
warn!("timed out while trying to perform measurement...")
|
||||
}
|
||||
res = measurement_future => res?,
|
||||
}
|
||||
}
|
||||
|
||||
let count = results.len() as u64;
|
||||
if count == 0 {
|
||||
return Err(ClientCoreError::NoGatewayMeasurements {
|
||||
identity: gateway.identity_key.to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let sum: Duration = results.into_iter().sum();
|
||||
let avg = Duration::from_nanos(sum.as_nanos() as u64 / count);
|
||||
|
||||
Ok(GatewayWithLatency::new(gateway, avg))
|
||||
}
|
||||
|
||||
async fn choose_gateway_by_latency<R: Rng>(
|
||||
rng: &mut R,
|
||||
gateways: Vec<gateway::Node>,
|
||||
) -> Result<gateway::Node, ClientCoreError> {
|
||||
info!("choosing gateway by latency...");
|
||||
|
||||
let mut gateways_with_latency = Vec::new();
|
||||
for gateway in gateways {
|
||||
let id = *gateway.identity();
|
||||
trace!("measuring latency to {id}...");
|
||||
let with_latency = match measure_latency(gateway).await {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
warn!("failed to measure {id}: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
"{id} ({}): {:?}",
|
||||
with_latency.gateway.location, with_latency.latency
|
||||
);
|
||||
gateways_with_latency.push(with_latency)
|
||||
}
|
||||
|
||||
let chosen = gateways_with_latency
|
||||
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
|
||||
.expect("invalid selection weight!");
|
||||
|
||||
info!(
|
||||
"chose gateway {} (located at {}) with average latency of {:?}",
|
||||
chosen.gateway.identity_key, chosen.gateway.location, chosen.latency
|
||||
);
|
||||
|
||||
Ok(chosen.gateway.clone())
|
||||
}
|
||||
|
||||
fn uniformly_random_gateway<R: Rng>(
|
||||
rng: &mut R,
|
||||
gateways: Vec<gateway::Node>,
|
||||
) -> Result<gateway::Node, ClientCoreError> {
|
||||
gateways
|
||||
.choose(rng)
|
||||
.ok_or(ClientCoreError::NoGatewaysOnNetwork)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(super) async fn query_gateway_details(
|
||||
validator_servers: Vec<Url>,
|
||||
chosen_gateway_id: Option<identity::PublicKey>,
|
||||
by_latency: bool,
|
||||
) -> Result<gateway::Node, ClientCoreError> {
|
||||
let mut rng = thread_rng();
|
||||
let gateways = current_gateways(&mut rng, validator_servers).await?;
|
||||
|
||||
// if we set an explicit gateway, use that one and nothing else
|
||||
if let Some(explicitly_chosen) = chosen_gateway_id {
|
||||
gateways
|
||||
.into_iter()
|
||||
.find(|gateway| gateway.identity_key == explicitly_chosen)
|
||||
.ok_or_else(|| ClientCoreError::NoGatewayWithId(explicitly_chosen.to_string()))
|
||||
} else if by_latency {
|
||||
choose_gateway_by_latency(&mut rng, gateways).await
|
||||
// if we have chosen particular gateway - use it, otherwise choose a random one.
|
||||
// (remember that in active topology all gateways have at least 100 reputation so should
|
||||
// be working correctly)
|
||||
if let Some(gateway_id) = chosen_gateway_id {
|
||||
filtered_gateways
|
||||
.iter()
|
||||
.find(|gateway| gateway.identity_key == gateway_id)
|
||||
.ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_id.to_string()))
|
||||
.cloned()
|
||||
} else {
|
||||
uniformly_random_gateway(&mut rng, gateways)
|
||||
filtered_gateways
|
||||
.choose(&mut rand::thread_rng())
|
||||
.ok_or(ClientCoreError::NoGatewaysOnNetwork)
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,11 +77,9 @@ pub async fn register_with_gateway(
|
||||
key_manager: &mut KeyManager,
|
||||
nym_api_endpoints: Vec<Url>,
|
||||
chosen_gateway_id: Option<identity::PublicKey>,
|
||||
by_latency: bool,
|
||||
) -> Result<GatewayEndpointConfig, ClientCoreError> {
|
||||
// Get the gateway details of the gateway we will use
|
||||
let gateway =
|
||||
helpers::query_gateway_details(nym_api_endpoints, chosen_gateway_id, by_latency).await?;
|
||||
let gateway = helpers::query_gateway_details(nym_api_endpoints, chosen_gateway_id).await?;
|
||||
log::debug!("Querying gateway gives: {}", gateway);
|
||||
|
||||
let our_identity = key_manager.identity_keypair();
|
||||
@@ -104,7 +102,6 @@ pub async fn setup_gateway_from_config<C, T>(
|
||||
register_gateway: bool,
|
||||
user_chosen_gateway_id: Option<identity::PublicKey>,
|
||||
config: &Config<T>,
|
||||
by_latency: bool,
|
||||
) -> Result<GatewayEndpointConfig, ClientCoreError>
|
||||
where
|
||||
C: NymConfig + ClientCoreConfigTrait,
|
||||
@@ -120,12 +117,9 @@ where
|
||||
}
|
||||
|
||||
// Else, we preceed by querying the nym-api
|
||||
let gateway = helpers::query_gateway_details(
|
||||
config.get_nym_api_endpoints(),
|
||||
user_chosen_gateway_id,
|
||||
by_latency,
|
||||
)
|
||||
.await?;
|
||||
let gateway =
|
||||
helpers::query_gateway_details(config.get_nym_api_endpoints(), user_chosen_gateway_id)
|
||||
.await?;
|
||||
log::debug!("Querying gateway gives: {}", gateway);
|
||||
|
||||
// If we are not registering, just return this and assume the caller has the keys already and
|
||||
|
||||
@@ -11,7 +11,7 @@ use commands::*;
|
||||
use error::Result;
|
||||
use log::*;
|
||||
use nym_bin_common::completions::fig_generate;
|
||||
use nym_config::{CRED_DB_FILE_NAME, DATA_DIR};
|
||||
use nym_config::{DATA_DIR, DB_FILE_NAME};
|
||||
use nym_network_defaults::{setup_env, NymNetworkDetails};
|
||||
use std::process::exit;
|
||||
use std::time::{Duration, SystemTime};
|
||||
@@ -51,11 +51,8 @@ async fn block_until_coconut_is_available<C: Clone + CosmWasmClient + Send + Syn
|
||||
|
||||
break;
|
||||
} else {
|
||||
// Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries
|
||||
let secs_until_final = epoch
|
||||
.final_timestamp_secs()
|
||||
.saturating_sub(current_timestamp_secs)
|
||||
+ 1;
|
||||
// Use 20 additional seconds to avoid the exact moment of going into the final epoch state
|
||||
let secs_until_final = epoch.final_timestamp_secs() + 20 - current_timestamp_secs;
|
||||
info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final);
|
||||
std::thread::sleep(Duration::from_secs(secs_until_final));
|
||||
}
|
||||
@@ -73,10 +70,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
match args.command {
|
||||
Command::Run(r) => {
|
||||
let db_path = r
|
||||
.client_home_directory
|
||||
.join(DATA_DIR)
|
||||
.join(CRED_DB_FILE_NAME);
|
||||
let db_path = r.client_home_directory.join(DATA_DIR).join(DB_FILE_NAME);
|
||||
let shared_storage = credential_storage::initialise_storage(db_path).await;
|
||||
let recovery_storage = recovery_storage::RecoveryStorage::new(r.recovery_dir)?;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.12"
|
||||
version = "1.1.10"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -101,7 +101,6 @@ impl SocketClient {
|
||||
let ClientState {
|
||||
shared_lane_queue_lengths,
|
||||
reply_controller_sender,
|
||||
..
|
||||
} = client_state;
|
||||
|
||||
let websocket_handler = websocket::HandlerBuilder::new(
|
||||
|
||||
@@ -25,11 +25,6 @@ pub(crate) struct Init {
|
||||
#[clap(long)]
|
||||
gateway: Option<identity::PublicKey>,
|
||||
|
||||
/// Specifies whether the new gateway should be determined based by latency as opposed to being chosen
|
||||
/// uniformly.
|
||||
#[clap(long, conflicts_with = "gateway")]
|
||||
latency_based_selection: bool,
|
||||
|
||||
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
|
||||
/// potentially causing loss of access.
|
||||
#[clap(long)]
|
||||
@@ -148,7 +143,6 @@ pub(crate) async fn execute(args: &Init) -> Result<(), ClientError> {
|
||||
register_gateway,
|
||||
user_chosen_gateway_id,
|
||||
config.get_base(),
|
||||
args.latency_based_selection,
|
||||
)
|
||||
.await
|
||||
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.12"
|
||||
version = "1.1.10"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -123,7 +123,7 @@ impl NymClient {
|
||||
|
||||
let ClientState {
|
||||
shared_lane_queue_lengths,
|
||||
..
|
||||
reply_controller_sender: _,
|
||||
} = client_status;
|
||||
|
||||
let authenticator = Authenticator::new(auth_methods, allowed_users);
|
||||
|
||||
@@ -37,11 +37,6 @@ pub(crate) struct Init {
|
||||
#[clap(long)]
|
||||
gateway: Option<identity::PublicKey>,
|
||||
|
||||
/// Specifies whether the new gateway should be determined based by latency as opposed to being chosen
|
||||
/// uniformly.
|
||||
#[clap(long, conflicts_with = "gateway")]
|
||||
latency_based_selection: bool,
|
||||
|
||||
/// Force register gateway. WARNING: this will overwrite any existing keys for the given id,
|
||||
/// potentially causing loss of access.
|
||||
#[clap(long)]
|
||||
@@ -154,7 +149,6 @@ pub(crate) async fn execute(args: &Init) -> Result<(), Socks5ClientError> {
|
||||
register_gateway,
|
||||
user_chosen_gateway_id,
|
||||
config.get_base(),
|
||||
args.latency_based_selection,
|
||||
)
|
||||
.await
|
||||
.tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-bin-common"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
description = "Common code for nym binaries"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
@@ -14,7 +14,7 @@ log = { workspace = true }
|
||||
thiserror = "1.0"
|
||||
url = "2.2"
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
async-trait = { workspace = true }
|
||||
async-trait = { version = "0.1.51" }
|
||||
tokio = { version = "1.24.1", features = ["macros"] }
|
||||
|
||||
# internal
|
||||
|
||||
@@ -35,7 +35,7 @@ nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
|
||||
# required for nyxd-client
|
||||
# at some point it might be possible to make it wasm-compatible
|
||||
# perhaps after https://github.com/cosmos/cosmos-rust/pull/97 is resolved (and tendermint-rs is updated)
|
||||
async-trait = { workspace = true, optional = true }
|
||||
async-trait = { version = "0.1.51", optional = true }
|
||||
bip39 = { version = "1", features = ["rand"], optional = true }
|
||||
nym-config = { path = "../../config", optional = true }
|
||||
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", features = ["rpc", "bip32", "cosmwasm"], optional = true}
|
||||
|
||||
@@ -11,7 +11,9 @@ use nym_api_requests::models::{
|
||||
GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
|
||||
RewardEstimationResponse, StakeSaturationResponse,
|
||||
};
|
||||
pub use nym_mixnet_contract_common::{mixnode::MixNodeDetails, GatewayBond, IdentityKeyRef, MixId};
|
||||
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::MixId;
|
||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef};
|
||||
|
||||
#[cfg(feature = "nyxd-client")]
|
||||
use crate::nyxd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient};
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::{Gas, GasPrice};
|
||||
pub use cosmrs::Coin as CosmosCoin;
|
||||
pub use cosmwasm_std::Coin as CosmWasmCoin;
|
||||
use cosmwasm_std::{Fraction, Uint128};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::ops::Div;
|
||||
|
||||
pub use cosmrs::Coin as CosmosCoin;
|
||||
pub use cosmwasm_std::Coin as CosmWasmCoin;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
pub struct MismatchedDenoms;
|
||||
@@ -22,40 +19,6 @@ pub struct Coin {
|
||||
pub denom: String,
|
||||
}
|
||||
|
||||
impl Div<GasPrice> for Coin {
|
||||
type Output = Gas;
|
||||
|
||||
fn div(self, rhs: GasPrice) -> Self::Output {
|
||||
&self / rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Div<GasPrice> for &'a Coin {
|
||||
type Output = Gas;
|
||||
|
||||
fn div(self, rhs: GasPrice) -> Self::Output {
|
||||
if self.denom != rhs.denom {
|
||||
panic!(
|
||||
"attempted to use two different denoms for gas calculation ({} and {})",
|
||||
self.denom, rhs.denom
|
||||
);
|
||||
}
|
||||
|
||||
// tsk, tsk. somebody tried to divide by zero here!
|
||||
let Some(gas_price_inv) = rhs.amount.inv() else {
|
||||
panic!("attempted to divide by zero!")
|
||||
};
|
||||
|
||||
let implicit_gas_limit = gas_price_inv * Uint128::new(self.amount);
|
||||
if implicit_gas_limit.u128() >= u64::MAX as u128 {
|
||||
u64::MAX
|
||||
} else {
|
||||
implicit_gas_limit.u128() as u64
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Coin {
|
||||
pub fn new<S: Into<String>>(amount: u128, denom: S) -> Self {
|
||||
Coin {
|
||||
@@ -165,67 +128,3 @@ impl CoinConverter for CosmWasmCoin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn division_by_zero_gas_price() {
|
||||
let gas_price: GasPrice = "0unym".parse().unwrap();
|
||||
let amount = Coin::new(123, "unym");
|
||||
let _res = amount / gas_price;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn division_by_gas_price_of_different_denom() {
|
||||
let gas_price: GasPrice = "0.025unyx".parse().unwrap();
|
||||
let amount = Coin::new(123, "unym");
|
||||
let _res = amount / gas_price;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gas_price_division() {
|
||||
let amount = Coin::new(3938, "unym");
|
||||
let gas_price = "0.025unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(157520, res.value());
|
||||
|
||||
let amount = Coin::new(1234567890, "unym");
|
||||
let gas_price = "0.025unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(49382715600, res.value());
|
||||
|
||||
let amount = Coin::new(1, "unym");
|
||||
let gas_price = "0.025unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(40, res.value());
|
||||
|
||||
let amount = Coin::new(150_000_000, "unym");
|
||||
let gas_price = "0.001234unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(121555915721, res.value());
|
||||
|
||||
let amount = Coin::new(150_000_000, "unym");
|
||||
let gas_price = "1unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(150_000_000, res.value());
|
||||
|
||||
let amount = Coin::new(150_000_000, "unym");
|
||||
let gas_price = "1234.56unym".parse().unwrap();
|
||||
let res = amount / gas_price;
|
||||
assert_eq!(121500, res.value());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gas_price_division_identity() {
|
||||
let amount = Coin::new(1234567890, "unym");
|
||||
let gas_price: GasPrice = "0.025unym".parse().unwrap();
|
||||
let res1 = (&amount) / gas_price.clone();
|
||||
let res2 = &gas_price * res1;
|
||||
|
||||
assert_eq!(amount, Coin::from(res2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::Coin;
|
||||
use crate::nyxd::Gas;
|
||||
use crate::nyxd::{Coin, GasPrice};
|
||||
use cosmrs::{tx, AccountId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
@@ -64,12 +64,6 @@ impl Display for Fee {
|
||||
}
|
||||
|
||||
impl Fee {
|
||||
pub fn manual_with_gas_price(fee: Coin, gas_price: GasPrice) -> Self {
|
||||
let gas_limit = &fee / gas_price;
|
||||
|
||||
Fee::Manual(tx::Fee::from_amount_and_gas(fee.into(), gas_limit))
|
||||
}
|
||||
|
||||
pub fn new_payer_granter_auto(
|
||||
gas_adjustment: Option<GasAdjustment>,
|
||||
payer: Option<AccountId>,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub mod defaults;
|
||||
|
||||
pub const CONFIG_DIR: &str = "config";
|
||||
pub const DATA_DIR: &str = "data";
|
||||
pub const CRED_DB_FILE_NAME: &str = "credentials_database.db";
|
||||
pub const DB_FILE_NAME: &str = "db.sqlite";
|
||||
|
||||
pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
fn template() -> &'static str;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-contracts-common"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
description = "Common library for Nym cosmwasm contracts"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
description = "Common library for the Nym mixnet contract"
|
||||
rust-version = "1.62"
|
||||
edition = { workspace = true }
|
||||
@@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1"
|
||||
schemars = "0.8"
|
||||
thiserror = "1.0"
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.2.0" }
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.1.0" }
|
||||
serde_json = "1.0.0"
|
||||
humantime-serde = "1.1.1"
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "cosmwasm-contract-testing"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
base64 = { version = "0.21.0", optional = true }
|
||||
cosmwasm-std = { version = "1.0.0" }
|
||||
cosmwasm-storage = { version = "1.0.0", optional = true }
|
||||
cw-storage-plus = { version = "0.13.4", optional = true }
|
||||
hex = { version = "0.4.3", optional = true }
|
||||
rand_chacha = { version = "0.3", optional = true }
|
||||
serde = { version = "1", features=["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
thiserror = { version = "1.0.38" }
|
||||
|
||||
[features]
|
||||
default = ["full"]
|
||||
full = ["testable-trait", "contract-mocks", "rand", "state-importing", "cosmwasm-storage", "cw-storage-plus", "serde_json"]
|
||||
rand = ["rand_chacha"]
|
||||
contract-mocks = []
|
||||
state-importing = ["base64", "hex"]
|
||||
testable-trait = []
|
||||
@@ -0,0 +1,291 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::mock_api::CW12MockApi;
|
||||
use crate::raw_state::{DecodingError, EncodingError, ImportedContractState, KeyValue};
|
||||
use crate::AVERAGE_BLOCKTIME_SECS;
|
||||
use cosmwasm_std::testing::{mock_env, MockQuerier, MockStorage};
|
||||
use cosmwasm_std::{
|
||||
Addr, Coin, Deps, DepsMut, Env, Order, QuerierWrapper, StdResult, Storage, Timestamp,
|
||||
TransactionInfo,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(feature = "cw-storage-plus")]
|
||||
use cw_storage_plus::{Item, Map, PrimaryKey};
|
||||
|
||||
// extracted into separate struct for easier cloning, access to mock structs, etc.
|
||||
// we also had to redefine the MockApi
|
||||
struct MockedDependencies {
|
||||
storage: MockStorage,
|
||||
api: CW12MockApi,
|
||||
querier: MockQuerier,
|
||||
|
||||
// that's a bit annoying. We have to keep track of all balance changes for when we clone the state
|
||||
// as there's no easy way of obtaining the up to date list of all balances from the querier...
|
||||
_balances: HashMap<String, Vec<Coin>>,
|
||||
}
|
||||
|
||||
impl MockedDependencies {
|
||||
fn new_mock() -> MockedDependencies {
|
||||
MockedDependencies {
|
||||
storage: Default::default(),
|
||||
api: Default::default(),
|
||||
querier: Default::default(),
|
||||
_balances: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_state(&self) -> MockedDependencies {
|
||||
let new_querier = MockQuerier::new(
|
||||
&self
|
||||
._balances
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_ref(), v.as_ref()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let mut new_storage = MockStorage::new();
|
||||
for (k, v) in self.storage.range(None, None, Order::Ascending) {
|
||||
new_storage.set(&k, &v)
|
||||
}
|
||||
|
||||
MockedDependencies {
|
||||
storage: new_storage,
|
||||
api: self.api,
|
||||
querier: new_querier,
|
||||
_balances: self._balances.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_raw(kvs: Vec<KeyValue>) -> Self {
|
||||
let mut new = Self::new_mock();
|
||||
for kv in kvs {
|
||||
new.storage.set(&kv.key, &kv.value)
|
||||
}
|
||||
new
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContractState {
|
||||
deps: MockedDependencies,
|
||||
env: Env,
|
||||
}
|
||||
|
||||
impl ContractState {
|
||||
pub fn new() -> Self {
|
||||
ContractState {
|
||||
deps: MockedDependencies::new_mock(),
|
||||
env: mock_env(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_env(env: Env) -> Self {
|
||||
ContractState {
|
||||
deps: MockedDependencies::new_mock(),
|
||||
env,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_state(&self) -> Self {
|
||||
ContractState {
|
||||
deps: self.deps.clone_state(),
|
||||
env: self.env.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// set a new balance for the given address and return the old balance
|
||||
pub fn update_account_balance(
|
||||
&mut self,
|
||||
addr: impl Into<String>,
|
||||
balance: Vec<Coin>,
|
||||
) -> Option<Vec<Coin>> {
|
||||
// that's a bit annoying. We have to keep track of all balance changes for when we clone the state
|
||||
// as there's no easy way of obtaining the up to date list of all balances from the querier...
|
||||
let addr = addr.into();
|
||||
self.deps._balances.insert(addr.clone(), balance.clone());
|
||||
self.deps.querier.update_balance(addr, balance)
|
||||
}
|
||||
|
||||
pub fn account_balance(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
denom: impl Into<String>,
|
||||
) -> StdResult<Coin> {
|
||||
self.deps().querier.query_balance(address, denom)
|
||||
}
|
||||
|
||||
pub fn all_account_balances(&self, address: impl Into<String>) -> StdResult<Vec<Coin>> {
|
||||
self.deps().querier.query_all_balances(address)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cw-storage-plus")]
|
||||
pub fn save_map_value<'a, K, T>(&mut self, map: &Map<'a, K, T>, k: K, data: &T) -> StdResult<()>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: PrimaryKey<'a>,
|
||||
{
|
||||
map.save(&mut self.deps.storage, k, data)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cw-storage-plus")]
|
||||
pub fn load_map_value<'a, K, T>(&self, map: &Map<'a, K, T>, k: K) -> StdResult<T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: PrimaryKey<'a>,
|
||||
{
|
||||
map.load(&self.deps.storage, k)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cw-storage-plus")]
|
||||
pub fn may_load_map_value<'a, K, T>(&self, map: &Map<'a, K, T>, k: K) -> StdResult<Option<T>>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: PrimaryKey<'a>,
|
||||
{
|
||||
map.may_load(&self.deps.storage, k)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cw-storage-plus")]
|
||||
pub fn save_item<T>(&mut self, item: &Item<T>, data: &T) -> StdResult<()>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
item.save(&mut self.deps.storage, data)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cw-storage-plus")]
|
||||
pub fn load_item<T>(&self, item: &Item<T>) -> StdResult<T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
item.load(&self.deps.storage)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cw-storage-plus")]
|
||||
pub fn may_load_item<T>(&self, item: &Item<T>) -> StdResult<Option<T>>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
item.may_load(&self.deps.storage)
|
||||
}
|
||||
|
||||
pub fn read_key(&self, key: &[u8]) -> Option<Vec<u8>> {
|
||||
self.deps.storage.get(key)
|
||||
}
|
||||
|
||||
pub fn set_key_value(&mut self, key: &[u8], value: &[u8]) {
|
||||
self.deps.storage.set(key, value)
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> Deps<'_> {
|
||||
Deps {
|
||||
storage: &self.deps.storage,
|
||||
api: &self.deps.api,
|
||||
querier: QuerierWrapper::new(&self.deps.querier),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deps_mut(&mut self) -> DepsMut<'_> {
|
||||
DepsMut {
|
||||
storage: &mut self.deps.storage,
|
||||
api: &self.deps.api,
|
||||
querier: QuerierWrapper::new(&self.deps.querier),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_blocks(&mut self, new_blocks: u64) {
|
||||
self.advance_block_height(new_blocks);
|
||||
self.advance_blocktime(new_blocks * AVERAGE_BLOCKTIME_SECS)
|
||||
}
|
||||
|
||||
pub fn advance_block_height(&mut self, by: u64) {
|
||||
self.env.block.height += by;
|
||||
}
|
||||
|
||||
pub fn advance_blocktime(&mut self, by_secs: u64) {
|
||||
self.env.block.time = self.env.block.time.plus_seconds(by_secs)
|
||||
}
|
||||
|
||||
pub fn env(&self) -> &Env {
|
||||
&self.env
|
||||
}
|
||||
|
||||
pub fn env_cloned(&self) -> Env {
|
||||
self.env.clone()
|
||||
}
|
||||
|
||||
pub fn contract_address(&self) -> &Addr {
|
||||
&self.env.contract.address
|
||||
}
|
||||
|
||||
pub fn with_contract_address(mut self, address: impl Into<String>) -> Self {
|
||||
self.env.contract.address = Addr::unchecked(address);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_transaction_info(mut self, transaction: Option<TransactionInfo>) -> Self {
|
||||
self.env.transaction = transaction;
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "state-importing")]
|
||||
pub(crate) fn from_state_dump(state: ImportedContractState, custom_env: Option<Env>) -> Self {
|
||||
let env = custom_env.unwrap_or_else(|| {
|
||||
// this is not ideal, but we're making an assumption here that block time is approximately 5s
|
||||
// at block 5000000, we had a timestamp of 1672411689
|
||||
let mut env = mock_env();
|
||||
env.block.chain_id = "nyx".to_string();
|
||||
env.block.height = state.height;
|
||||
if state.height > 5000000 {
|
||||
let diff = state.height - 5000000;
|
||||
env.block.time =
|
||||
Timestamp::from_seconds(1672411689 + diff * AVERAGE_BLOCKTIME_SECS);
|
||||
} else {
|
||||
let diff = 5000000 - state.height;
|
||||
env.block.time =
|
||||
Timestamp::from_seconds(1672411689 - diff * AVERAGE_BLOCKTIME_SECS);
|
||||
}
|
||||
env
|
||||
});
|
||||
|
||||
let deps = MockedDependencies::from_raw(state.data);
|
||||
|
||||
ContractState { deps, env }
|
||||
}
|
||||
|
||||
#[cfg(feature = "state-importing")]
|
||||
pub fn try_from_state_dump<P: AsRef<Path>>(
|
||||
path: P,
|
||||
custom_env: Option<Env>,
|
||||
) -> Result<Self, DecodingError> {
|
||||
Ok(ImportedContractState::try_load_from_file(path)?.into_test_mock(custom_env))
|
||||
}
|
||||
|
||||
#[cfg(feature = "state-importing")]
|
||||
pub fn dump_state<P: AsRef<Path>>(&self, output_path: P) -> Result<(), EncodingError> {
|
||||
let mut data = Vec::new();
|
||||
for (key, value) in self.deps.storage.range(None, None, Order::Ascending) {
|
||||
data.push(KeyValue { key, value })
|
||||
}
|
||||
|
||||
let state = ImportedContractState {
|
||||
height: self.env.block.height,
|
||||
data,
|
||||
};
|
||||
|
||||
state.encode().to_file(output_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContractState {
|
||||
fn default() -> Self {
|
||||
ContractState {
|
||||
deps: MockedDependencies::new_mock(),
|
||||
env: mock_env(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::{Addr, StdError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MockingError {
|
||||
#[error(transparent)]
|
||||
StdError {
|
||||
#[from]
|
||||
source: StdError,
|
||||
},
|
||||
|
||||
#[error("attempted to add another contract mock that has the same address as an existing one - {address}")]
|
||||
DuplicateContractAddress { address: Addr },
|
||||
|
||||
#[error("attempted to use a contract that doesn't exist - {address}")]
|
||||
NonExistentContract { address: Addr },
|
||||
|
||||
#[error("contract execution failed with error: {error}. We called {contract} with {message}")]
|
||||
ContractExecutionError {
|
||||
message: String,
|
||||
contract: Addr,
|
||||
error: String,
|
||||
},
|
||||
|
||||
#[error("contract query failed with error: {error}. We called {contract} with {message}")]
|
||||
ContractQueryError {
|
||||
message: String,
|
||||
contract: Addr,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::helpers::raw_msg_to_string;
|
||||
use cosmwasm_std::{Addr, BankMsg, Binary, Coin, Event};
|
||||
|
||||
fn format_coins(coins: &[Coin]) -> String {
|
||||
if coins.is_empty() {
|
||||
"<zero>".to_string()
|
||||
} else {
|
||||
coins
|
||||
.iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
}
|
||||
|
||||
// specifically for tokens included in `Execute` that go into the contract
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrossContractTokenMove {
|
||||
pub amount: Vec<Coin>,
|
||||
pub sender: Addr,
|
||||
pub receiver: Addr,
|
||||
}
|
||||
|
||||
impl CrossContractTokenMove {
|
||||
pub fn new(amount: Vec<Coin>, sender: Addr, receiver: Addr) -> Self {
|
||||
Self {
|
||||
amount,
|
||||
sender,
|
||||
receiver,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pretty(&self) -> String {
|
||||
let total_amount = format_coins(&self.amount);
|
||||
|
||||
format!(
|
||||
"{total_amount} will be transferred from {} to {} (CONTRACTS)",
|
||||
self.sender, self.receiver
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ExecutionResult {
|
||||
pub steps: Vec<ExecutionStepResult>,
|
||||
}
|
||||
|
||||
impl ExecutionResult {
|
||||
pub fn new() -> Self {
|
||||
Self { steps: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn pretty(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for (i, step) in self.steps.iter().enumerate() {
|
||||
out.push_str(&format!("STEP {}\n", i + 1));
|
||||
out.push_str(&format!("{}\n", step.pretty()));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FurtherExecution {
|
||||
pub contract: Addr,
|
||||
pub msg: Binary,
|
||||
pub funds: Vec<Coin>,
|
||||
}
|
||||
|
||||
impl FurtherExecution {
|
||||
pub fn new(contract: String, msg: Binary, funds: Vec<Coin>) -> Self {
|
||||
Self {
|
||||
contract: Addr::unchecked(contract),
|
||||
msg,
|
||||
funds,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pretty(&self) -> String {
|
||||
let msg = raw_msg_to_string(&self.msg);
|
||||
let total_funds = format_coins(&self.funds);
|
||||
|
||||
format!(
|
||||
"{} will be called with msg {msg} and {total_funds} funds",
|
||||
self.contract
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutionStepResult {
|
||||
pub events: Vec<Event>,
|
||||
pub incoming_tokens: Vec<CrossContractTokenMove>,
|
||||
pub bank_msgs: Vec<BankMsg>,
|
||||
pub further_execution: Vec<FurtherExecution>,
|
||||
}
|
||||
|
||||
impl ExecutionStepResult {
|
||||
pub fn pretty(&self) -> String {
|
||||
let mut out = String::new();
|
||||
|
||||
// let's keep them squished for now...
|
||||
let events = format!("EVENTS: {:?}\n", self.events);
|
||||
out.push_str(&events);
|
||||
|
||||
if self.incoming_tokens.iter().any(|c| !c.amount.is_empty()) {
|
||||
out.push_str("MOVED TOKENS (CONTRACTS):\n");
|
||||
for incoming in &self.incoming_tokens {
|
||||
if !incoming.amount.is_empty() {
|
||||
out.push_str(&format!("{}\n", incoming.pretty()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.bank_msgs.is_empty() {
|
||||
out.push_str("MOVED TOKENS (BANK):\n");
|
||||
for bank in &self.bank_msgs {
|
||||
let formatted = match bank {
|
||||
BankMsg::Send { to_address, amount } => format!(
|
||||
"{} will be transferred to {to_address}",
|
||||
format_coins(amount)
|
||||
),
|
||||
BankMsg::Burn { amount } => format!("{} WILL BE BURNT", format_coins(amount)),
|
||||
_ => "unknown variant of BankMsg was introduced!".to_string(),
|
||||
};
|
||||
out.push_str(&format!("{formatted}\n"))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.further_execution.is_empty() {
|
||||
out.push_str("FURTHER CONTRACT CALLS:\n");
|
||||
for further_exec in &self.further_execution {
|
||||
out.push_str(&format!("{}\n", further_exec.pretty()))
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::{Binary, BlockInfo, Env, StdResult};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(feature = "rand")]
|
||||
pub fn test_rng() -> rand_chacha::ChaCha20Rng {
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
|
||||
let dummy_seed = [42u8; 32];
|
||||
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
|
||||
pub fn env_with_block_info(info: BlockInfo) -> Env {
|
||||
let mut env = mock_env();
|
||||
env.block = info;
|
||||
env
|
||||
}
|
||||
|
||||
pub fn deserialize_msg<M: DeserializeOwned>(raw: &Binary) -> StdResult<M> {
|
||||
cosmwasm_std::from_binary(raw)
|
||||
}
|
||||
|
||||
pub fn serialize_msg<M: Serialize>(msg: &M) -> StdResult<Binary> {
|
||||
cosmwasm_std::to_binary(msg)
|
||||
}
|
||||
|
||||
// used only for purposes of providing more informative error messages
|
||||
pub(crate) fn raw_msg_to_string(raw: &Binary) -> String {
|
||||
#[cfg(not(feature = "serde_json"))]
|
||||
return "<serde_json feature is not enabled - can't format the message>".to_string();
|
||||
|
||||
#[cfg(feature = "serde_json")]
|
||||
match serde_json::from_slice::<serde_json::Value>(raw.as_slice()) {
|
||||
Ok(deserialized) => deserialized.to_string(),
|
||||
Err(_) => "ERR: COULD NOT RECOVER THE ORIGINAL MESSAGE".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
mod contract_mock;
|
||||
mod error;
|
||||
mod execution;
|
||||
mod helpers;
|
||||
mod mock_api;
|
||||
mod multi_contract_mock;
|
||||
mod raw_state;
|
||||
mod single_contract_mock;
|
||||
mod traits;
|
||||
|
||||
pub use contract_mock::ContractState;
|
||||
pub use error::MockingError;
|
||||
pub use helpers::{deserialize_msg, env_with_block_info, serialize_msg};
|
||||
|
||||
#[cfg(feature = "state-importing")]
|
||||
pub use raw_state::ImportedContractState;
|
||||
|
||||
#[cfg(feature = "contract-mocks")]
|
||||
pub use multi_contract_mock::MultiContractMock;
|
||||
|
||||
#[cfg(feature = "contract-mocks")]
|
||||
pub use single_contract_mock::SingleContractMock;
|
||||
|
||||
#[cfg(feature = "contract-mocks")]
|
||||
pub use execution::{
|
||||
CrossContractTokenMove, ExecutionResult, ExecutionStepResult, FurtherExecution,
|
||||
};
|
||||
|
||||
#[cfg(feature = "testable-trait")]
|
||||
pub use traits::TestableContract;
|
||||
|
||||
pub const AVERAGE_BLOCKTIME_SECS: u64 = 5;
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// unfortunately we have to redefine cosmwasm' MockApi,
|
||||
// as in cw1.0 they have set `CANONICAL_LENGTH` to 54 which makes
|
||||
// `addr_validate` of our existing contracts fail (say of 'n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw')
|
||||
// this has changed in 1.2 but we can't use that version yet...
|
||||
|
||||
use cosmwasm_std::testing::{digit_sum, riffle_shuffle, MockApi};
|
||||
use cosmwasm_std::{
|
||||
Addr, Api, CanonicalAddr, RecoverPubkeyError, StdError, StdResult, VerificationError,
|
||||
};
|
||||
|
||||
const CANONICAL_LENGTH: usize = 90; // n = 45
|
||||
const SHUFFLES_ENCODE: usize = 10;
|
||||
const SHUFFLES_DECODE: usize = 2;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct CW12MockApi {
|
||||
inner: MockApi,
|
||||
canonical_length: usize,
|
||||
}
|
||||
|
||||
impl Default for CW12MockApi {
|
||||
fn default() -> Self {
|
||||
CW12MockApi {
|
||||
inner: MockApi::default(),
|
||||
canonical_length: CANONICAL_LENGTH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// whatever we can, hand over to 1.0 MockApi
|
||||
impl Api for CW12MockApi {
|
||||
fn addr_validate(&self, input: &str) -> StdResult<Addr> {
|
||||
let canonical = self.addr_canonicalize(input)?;
|
||||
let normalized = self.addr_humanize(&canonical)?;
|
||||
if input != normalized {
|
||||
return Err(StdError::generic_err(
|
||||
"Invalid input: address not normalized",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Addr::unchecked(input))
|
||||
}
|
||||
|
||||
fn addr_canonicalize(&self, input: &str) -> StdResult<CanonicalAddr> {
|
||||
// Dummy input validation. This is more sophisticated for formats like bech32, where format and checksum are validated.
|
||||
let min_length = 3;
|
||||
let max_length = self.canonical_length;
|
||||
if input.len() < min_length {
|
||||
return Err(StdError::generic_err(
|
||||
format!("Invalid input: human address too short for this mock implementation (must be >= {min_length})."),
|
||||
));
|
||||
}
|
||||
if input.len() > max_length {
|
||||
return Err(StdError::generic_err(
|
||||
format!("Invalid input: human address too long for this mock implementation (must be <= {max_length})."),
|
||||
));
|
||||
}
|
||||
|
||||
// mimicks formats like hex or bech32 where different casings are valid for one address
|
||||
let normalized = input.to_lowercase();
|
||||
|
||||
let mut out = Vec::from(normalized);
|
||||
|
||||
// pad to canonical length with NULL bytes
|
||||
out.resize(self.canonical_length, 0x00);
|
||||
// content-dependent rotate followed by shuffle to destroy
|
||||
// the most obvious structure (https://github.com/CosmWasm/cosmwasm/issues/552)
|
||||
let rotate_by = digit_sum(&out) % self.canonical_length;
|
||||
out.rotate_left(rotate_by);
|
||||
for _ in 0..SHUFFLES_ENCODE {
|
||||
out = riffle_shuffle(&out);
|
||||
}
|
||||
Ok(out.into())
|
||||
}
|
||||
|
||||
fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult<Addr> {
|
||||
if canonical.len() != self.canonical_length {
|
||||
return Err(StdError::generic_err(
|
||||
"Invalid input: canonical address length not correct",
|
||||
));
|
||||
}
|
||||
|
||||
let mut tmp: Vec<u8> = canonical.clone().into();
|
||||
// Shuffle two more times which restored the original value (24 elements are back to original after 20 rounds)
|
||||
for _ in 0..SHUFFLES_DECODE {
|
||||
tmp = riffle_shuffle(&tmp);
|
||||
}
|
||||
// Rotate back
|
||||
let rotate_by = digit_sum(&tmp) % self.canonical_length;
|
||||
tmp.rotate_right(rotate_by);
|
||||
// Remove NULL bytes (i.e. the padding)
|
||||
let trimmed = tmp.into_iter().filter(|&x| x != 0x00).collect();
|
||||
// decode UTF-8 bytes into string
|
||||
let human = String::from_utf8(trimmed)?;
|
||||
Ok(Addr::unchecked(human))
|
||||
}
|
||||
|
||||
fn secp256k1_verify(
|
||||
&self,
|
||||
message_hash: &[u8],
|
||||
signature: &[u8],
|
||||
public_key: &[u8],
|
||||
) -> Result<bool, VerificationError> {
|
||||
self.inner
|
||||
.secp256k1_verify(message_hash, signature, public_key)
|
||||
}
|
||||
|
||||
fn secp256k1_recover_pubkey(
|
||||
&self,
|
||||
message_hash: &[u8],
|
||||
signature: &[u8],
|
||||
recovery_param: u8,
|
||||
) -> Result<Vec<u8>, RecoverPubkeyError> {
|
||||
self.inner
|
||||
.secp256k1_recover_pubkey(message_hash, signature, recovery_param)
|
||||
}
|
||||
|
||||
fn ed25519_verify(
|
||||
&self,
|
||||
message: &[u8],
|
||||
signature: &[u8],
|
||||
public_key: &[u8],
|
||||
) -> Result<bool, VerificationError> {
|
||||
self.inner.ed25519_verify(message, signature, public_key)
|
||||
}
|
||||
|
||||
fn ed25519_batch_verify(
|
||||
&self,
|
||||
messages: &[&[u8]],
|
||||
signatures: &[&[u8]],
|
||||
public_keys: &[&[u8]],
|
||||
) -> Result<bool, VerificationError> {
|
||||
self.inner
|
||||
.ed25519_batch_verify(messages, signatures, public_keys)
|
||||
}
|
||||
|
||||
fn debug(&self, message: &str) {
|
||||
self.inner.debug(message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract_mock::ContractState;
|
||||
use crate::execution::{
|
||||
CrossContractTokenMove, ExecutionResult, ExecutionStepResult, FurtherExecution,
|
||||
};
|
||||
use crate::helpers::raw_msg_to_string;
|
||||
use crate::traits::sealed;
|
||||
use crate::{serialize_msg, MockingError, TestableContract};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{
|
||||
Addr, Binary, CosmosMsg, Env, MessageInfo, QueryResponse, ReplyOn, Response, WasmMsg,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct MockedContract {
|
||||
state: ContractState,
|
||||
entry_points: Box<dyn sealed::ErasedTestableContract>,
|
||||
}
|
||||
|
||||
impl MockedContract {
|
||||
fn new<C: TestableContract + 'static>(state: ContractState) -> Self {
|
||||
MockedContract {
|
||||
state,
|
||||
entry_points: Box::new(C::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MultiContractMock {
|
||||
contracts: HashMap<Addr, MockedContract>,
|
||||
}
|
||||
|
||||
impl MultiContractMock {
|
||||
#[cfg(feature = "rand")]
|
||||
fn generate_new_contract_address(&self) -> Addr {
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
let mut rng = crate::helpers::test_rng();
|
||||
loop {
|
||||
// for the testing purposes u64 contains enough entropy
|
||||
// (I could even argue u8 would be sufficient)
|
||||
// as I doubt anyone would want to generate so many contract names
|
||||
// they would have started colliding...
|
||||
let candidate_id = rng.next_u64();
|
||||
let name = Addr::unchecked(format!("new-contract{candidate_id}"));
|
||||
if !self.contracts.contains_key(&name) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
MultiContractMock {
|
||||
contracts: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_contract<C: TestableContract + 'static>(
|
||||
&mut self,
|
||||
contract_state: ContractState,
|
||||
) -> Result<(), MockingError> {
|
||||
let address = contract_state.contract_address().clone();
|
||||
if self
|
||||
.contracts
|
||||
.contains_key(contract_state.contract_address())
|
||||
{
|
||||
Err(MockingError::DuplicateContractAddress { address })
|
||||
} else {
|
||||
let mocked = MockedContract::new::<C>(contract_state);
|
||||
self.contracts.insert(address, mocked);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_contract<C: TestableContract + 'static>(
|
||||
mut self,
|
||||
state: ContractState,
|
||||
) -> Result<Self, MockingError> {
|
||||
self.add_contract::<C>(state)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn advance_blocks(&mut self, new_blocks: u64) {
|
||||
for contract in self.contracts.values_mut() {
|
||||
contract.state.advance_blocks(new_blocks)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_block_height(&mut self, by: u64) {
|
||||
for contract in self.contracts.values_mut() {
|
||||
contract.state.advance_block_height(by)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_blocktime(&mut self, by_secs: u64) {
|
||||
for contract in self.contracts.values_mut() {
|
||||
contract.state.advance_blocktime(by_secs)
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_step(
|
||||
&mut self,
|
||||
contract_address: impl Into<String>,
|
||||
info: MessageInfo,
|
||||
binary_msg: Binary,
|
||||
) -> Result<ExecutionStepResult, MockingError> {
|
||||
let addr = Addr::unchecked(contract_address.into());
|
||||
let contract =
|
||||
self.contracts
|
||||
.get_mut(&addr)
|
||||
.ok_or_else(|| MockingError::NonExistentContract {
|
||||
address: addr.clone(),
|
||||
})?;
|
||||
|
||||
let env = contract.state.env_cloned();
|
||||
let deps = contract.state.deps_mut();
|
||||
|
||||
let res = match contract
|
||||
.entry_points
|
||||
.execute(deps, env, info, binary_msg.clone())
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(error) => {
|
||||
return Err(MockingError::ContractExecutionError {
|
||||
message: raw_msg_to_string(&binary_msg),
|
||||
contract: addr,
|
||||
error,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let mut bank_msgs = Vec::new();
|
||||
let mut further_execution = Vec::new();
|
||||
let mut incoming_tokens = Vec::new();
|
||||
|
||||
for sub_msg in res.messages {
|
||||
if sub_msg.reply_on != ReplyOn::Never {
|
||||
unimplemented!("currently there's no support for 'reply_on'")
|
||||
}
|
||||
|
||||
match sub_msg.msg {
|
||||
CosmosMsg::Bank(bank_msg) => bank_msgs.push(bank_msg),
|
||||
CosmosMsg::Wasm(wasm_msg) => {
|
||||
match wasm_msg {
|
||||
WasmMsg::Execute { contract_addr, msg, funds } => {
|
||||
incoming_tokens.push(CrossContractTokenMove::new(funds.clone(), addr.clone(), Addr::unchecked(&contract_addr)));
|
||||
further_execution.push(FurtherExecution::new(contract_addr, msg, funds))
|
||||
}
|
||||
_ => unimplemented!("currently we only support 'ExecuteMsg' for 'WasmMsg'")
|
||||
}
|
||||
}
|
||||
// other variants might get support later on
|
||||
_ => unimplemented!("currently there's no support for sub msgs different from 'WasmMsg' or 'BankMsg")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionStepResult {
|
||||
events: res.events,
|
||||
incoming_tokens,
|
||||
bank_msgs,
|
||||
further_execution,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: verify that this is the actual order of execution of sub messages in cosmwasm
|
||||
fn execute_branch(
|
||||
&mut self,
|
||||
res: &mut ExecutionResult,
|
||||
contract: String,
|
||||
info: MessageInfo,
|
||||
msg: Binary,
|
||||
) -> Result<(), MockingError> {
|
||||
let step_res = self.execute_step(contract.clone(), info, msg)?;
|
||||
res.steps.push(step_res.clone());
|
||||
for further_exec in step_res.further_execution {
|
||||
let info = mock_info(&contract, &further_exec.funds);
|
||||
self.execute_branch(
|
||||
res,
|
||||
further_exec.contract.into_string(),
|
||||
info,
|
||||
further_exec.msg,
|
||||
)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contract_state(
|
||||
&self,
|
||||
contract_address: impl Into<String>,
|
||||
) -> Result<&ContractState, MockingError> {
|
||||
let addr = Addr::unchecked(contract_address.into());
|
||||
let contract =
|
||||
self.contracts
|
||||
.get(&addr)
|
||||
.ok_or_else(|| MockingError::NonExistentContract {
|
||||
address: addr.clone(),
|
||||
})?;
|
||||
Ok(&contract.state)
|
||||
}
|
||||
|
||||
pub fn contract_state_mut(
|
||||
&mut self,
|
||||
contract_address: impl Into<String>,
|
||||
) -> Result<&mut ContractState, MockingError> {
|
||||
let addr = Addr::unchecked(contract_address.into());
|
||||
let contract =
|
||||
self.contracts
|
||||
.get_mut(&addr)
|
||||
.ok_or_else(|| MockingError::NonExistentContract {
|
||||
address: addr.clone(),
|
||||
})?;
|
||||
Ok(&mut contract.state)
|
||||
}
|
||||
|
||||
// TODO: add support for sub msgs in instantiate response
|
||||
pub fn instantiate<C>(
|
||||
&mut self,
|
||||
custom_env: Option<Env>,
|
||||
info: MessageInfo,
|
||||
msg: C::InstantiateMsg,
|
||||
) -> Result<Response, C::ContractError>
|
||||
where
|
||||
C: TestableContract + 'static,
|
||||
{
|
||||
// if custom environment wasn't provided, generate a pseudorandom address so that it wouldn't
|
||||
// clash with any existing contracts
|
||||
let env = custom_env.unwrap_or_else(|| {
|
||||
let mut env = mock_env();
|
||||
env.contract.address = self.generate_new_contract_address();
|
||||
env
|
||||
});
|
||||
let mut state = ContractState::new_with_env(env);
|
||||
let env = state.env_cloned();
|
||||
let deps = state.deps_mut();
|
||||
C::instantiate(deps, env, info, msg)
|
||||
}
|
||||
|
||||
pub fn execute_full<C>(
|
||||
&mut self,
|
||||
initial_contract: impl Into<String>,
|
||||
info: MessageInfo,
|
||||
msg: C::ExecuteMsg,
|
||||
) -> Result<ExecutionResult, MockingError>
|
||||
where
|
||||
C: TestableContract + 'static,
|
||||
C::ExecuteMsg: Serialize,
|
||||
{
|
||||
let mut execution_result = ExecutionResult::new();
|
||||
let serialized_msg = serialize_msg(&msg)?;
|
||||
|
||||
self.execute_branch(
|
||||
&mut execution_result,
|
||||
initial_contract.into(),
|
||||
info,
|
||||
serialized_msg,
|
||||
)?;
|
||||
Ok(execution_result)
|
||||
}
|
||||
|
||||
// provide unchecked variant of execute to return original error enum
|
||||
pub fn unchecked_execute<C>(
|
||||
&mut self,
|
||||
contract_address: impl Into<String>,
|
||||
info: MessageInfo,
|
||||
msg: C::ExecuteMsg,
|
||||
) -> Result<Response, C::ContractError>
|
||||
where
|
||||
C: TestableContract + 'static,
|
||||
{
|
||||
let addr = Addr::unchecked(contract_address.into());
|
||||
let contract = self
|
||||
.contracts
|
||||
.get_mut(&addr)
|
||||
.expect("specified contract does not exist");
|
||||
|
||||
let env = contract.state.env_cloned();
|
||||
let deps = contract.state.deps_mut();
|
||||
C::execute(deps, env, info, msg)
|
||||
}
|
||||
|
||||
// executes only the top level message
|
||||
pub fn execute<C>(
|
||||
&mut self,
|
||||
contract_address: impl Into<String>,
|
||||
info: MessageInfo,
|
||||
msg: C::ExecuteMsg,
|
||||
) -> Result<Response, MockingError>
|
||||
where
|
||||
C: TestableContract + 'static,
|
||||
C::ExecuteMsg: Serialize,
|
||||
{
|
||||
let addr = Addr::unchecked(contract_address.into());
|
||||
let contract =
|
||||
self.contracts
|
||||
.get_mut(&addr)
|
||||
.ok_or_else(|| MockingError::NonExistentContract {
|
||||
address: addr.clone(),
|
||||
})?;
|
||||
|
||||
let env = contract.state.env_cloned();
|
||||
let deps = contract.state.deps_mut();
|
||||
|
||||
let serialized_msg = serialize_msg(&msg)?;
|
||||
C::execute(deps, env, info, msg).map_err(|err| MockingError::ContractExecutionError {
|
||||
message: raw_msg_to_string(&serialized_msg),
|
||||
contract: addr,
|
||||
error: err.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// provide unchecked variant of query to return original error enum
|
||||
pub fn unchecked_query<C, T>(
|
||||
&self,
|
||||
contract_address: impl Into<String>,
|
||||
msg: C::QueryMsg,
|
||||
) -> Result<T, C::ContractError>
|
||||
where
|
||||
C: TestableContract + 'static,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let addr = Addr::unchecked(contract_address.into());
|
||||
let contract = self
|
||||
.contracts
|
||||
.get(&addr)
|
||||
.expect("specified contract does not exist");
|
||||
|
||||
let env = contract.state.env_cloned();
|
||||
let deps = contract.state.deps();
|
||||
C::query(deps, env, msg).map(|res| serde_json::from_slice(&res).unwrap())
|
||||
}
|
||||
|
||||
pub fn query<C>(
|
||||
&self,
|
||||
contract_address: impl Into<String>,
|
||||
msg: C::QueryMsg,
|
||||
) -> Result<QueryResponse, MockingError>
|
||||
where
|
||||
C: TestableContract + 'static,
|
||||
C::QueryMsg: Serialize,
|
||||
{
|
||||
let addr = Addr::unchecked(contract_address.into());
|
||||
let contract =
|
||||
self.contracts
|
||||
.get(&addr)
|
||||
.ok_or_else(|| MockingError::NonExistentContract {
|
||||
address: addr.clone(),
|
||||
})?;
|
||||
|
||||
let env = contract.state.env_cloned();
|
||||
let deps = contract.state.deps();
|
||||
|
||||
let serialized_msg = serialize_msg(&msg)?;
|
||||
C::query(deps, env, msg).map_err(|err| MockingError::ContractQueryError {
|
||||
message: raw_msg_to_string(&serialized_msg),
|
||||
contract: addr,
|
||||
error: err.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_de<C, T>(
|
||||
&self,
|
||||
contract_address: impl Into<String>,
|
||||
msg: C::QueryMsg,
|
||||
) -> Result<T, MockingError>
|
||||
where
|
||||
C: TestableContract + 'static,
|
||||
C::QueryMsg: Serialize,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
self.query::<C>(contract_address, msg)
|
||||
.map(|res| serde_json::from_slice(&res).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[test]
|
||||
fn converting_msg_to_string() {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Dummy {
|
||||
field1: String,
|
||||
field2: u32,
|
||||
field3: Vec<u32>,
|
||||
}
|
||||
|
||||
let dummy = Dummy {
|
||||
field1: "aaaa".to_string(),
|
||||
field2: 42,
|
||||
field3: vec![1, 2, 3, 4],
|
||||
};
|
||||
|
||||
let bin = serialize_msg(&dummy).unwrap();
|
||||
let expected = r#"{"field1":"aaaa","field2":42,"field3":[1,2,3,4]}"#;
|
||||
let stringified = raw_msg_to_string(&bin);
|
||||
assert_eq!(expected, stringified)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract_mock::ContractState;
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use cosmwasm_std::Env;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::num::ParseIntError;
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DecodingError {
|
||||
#[error("failed to parse the block height information of the state dump: {source}")]
|
||||
MalformedBlockHeight {
|
||||
#[from]
|
||||
source: ParseIntError,
|
||||
},
|
||||
|
||||
#[error("failed to open the specified state dump: {source}")]
|
||||
FileOpenError {
|
||||
#[from]
|
||||
source: io::Error,
|
||||
},
|
||||
|
||||
#[error("failed to decode the provided json state: {source}")]
|
||||
JsonDecodeError {
|
||||
#[from]
|
||||
source: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("failed to decode one of the state keys: {source}")]
|
||||
HexDecodeError {
|
||||
#[from]
|
||||
source: hex::FromHexError,
|
||||
},
|
||||
|
||||
#[error("failed to decode one of the state values: {source}")]
|
||||
Base64DecodeError {
|
||||
#[from]
|
||||
source: base64::DecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EncodingError {
|
||||
#[error("failed to open the specified state dump file: {source}")]
|
||||
FileOpenError {
|
||||
#[from]
|
||||
source: io::Error,
|
||||
},
|
||||
|
||||
#[error("failed to encode the provided json state: {source}")]
|
||||
JsonEncodeError {
|
||||
#[from]
|
||||
source: serde_json::Error,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ImportedContractState {
|
||||
pub height: u64,
|
||||
pub data: Vec<KeyValue>,
|
||||
}
|
||||
|
||||
pub struct KeyValue {
|
||||
pub key: Vec<u8>,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ImportedContractState {
|
||||
pub fn try_from_json(value: &str) -> Result<Self, DecodingError> {
|
||||
RawContractState::from_json(value)?.decode()
|
||||
}
|
||||
|
||||
pub fn try_load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, DecodingError> {
|
||||
RawContractState::from_file(path)?.decode()
|
||||
}
|
||||
|
||||
pub fn find_value(&self, key: &[u8]) -> Option<&[u8]> {
|
||||
self.data
|
||||
.iter()
|
||||
.find(|kv| kv.key == key)
|
||||
.map(|kv| kv.value.as_ref())
|
||||
}
|
||||
|
||||
pub fn into_test_mock(self, custom_env: Option<Env>) -> ContractState {
|
||||
ContractState::from_state_dump(self, custom_env)
|
||||
}
|
||||
|
||||
pub(crate) fn encode(self) -> RawContractState {
|
||||
RawContractState {
|
||||
height: self.height.to_string(),
|
||||
result: self.data.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct RawContractState {
|
||||
height: String,
|
||||
result: Vec<RawKeyValue>,
|
||||
}
|
||||
|
||||
impl RawContractState {
|
||||
fn decode(self) -> Result<ImportedContractState, DecodingError> {
|
||||
Ok(ImportedContractState {
|
||||
height: self.height.parse()?,
|
||||
data: self
|
||||
.result
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn from_json(value: &str) -> Result<Self, DecodingError> {
|
||||
Ok(serde_json::from_str(value)?)
|
||||
}
|
||||
|
||||
fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, DecodingError> {
|
||||
let file = File::open(path)?;
|
||||
Ok(serde_json::from_reader(file)?)
|
||||
}
|
||||
|
||||
pub(crate) fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), EncodingError> {
|
||||
let file = File::open(path)?;
|
||||
Ok(serde_json::to_writer(file, &self)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct RawKeyValue {
|
||||
// encoded as hex
|
||||
key: String,
|
||||
|
||||
// encoded as base64
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl TryFrom<RawKeyValue> for KeyValue {
|
||||
type Error = DecodingError;
|
||||
|
||||
fn try_from(raw: RawKeyValue) -> Result<Self, Self::Error> {
|
||||
Ok(KeyValue {
|
||||
key: hex::decode(&raw.key)?,
|
||||
value: general_purpose::STANDARD.decode(&raw.value)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyValue> for RawKeyValue {
|
||||
fn from(decoded: KeyValue) -> Self {
|
||||
RawKeyValue {
|
||||
key: hex::encode(decoded.key),
|
||||
value: general_purpose::STANDARD.encode(decoded.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{ContractState, TestableContract};
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::{from_slice, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub struct SingleContractMock<C> {
|
||||
pub state: ContractState,
|
||||
_contract: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C: TestableContract> SingleContractMock<C> {
|
||||
pub fn new_empty() -> Self {
|
||||
SingleContractMock {
|
||||
state: Default::default(),
|
||||
_contract: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(state: ContractState) -> Self {
|
||||
SingleContractMock {
|
||||
state,
|
||||
_contract: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> Deps<'_> {
|
||||
self.state.deps()
|
||||
}
|
||||
|
||||
pub fn deps_mut(&mut self) -> DepsMut<'_> {
|
||||
self.state.deps_mut()
|
||||
}
|
||||
|
||||
pub fn env(&self) -> &Env {
|
||||
self.state.env()
|
||||
}
|
||||
|
||||
pub fn env_cloned(&self) -> Env {
|
||||
self.state.env_cloned()
|
||||
}
|
||||
|
||||
pub fn instantiate(
|
||||
custom_env: Option<Env>,
|
||||
info: MessageInfo,
|
||||
msg: C::InstantiateMsg,
|
||||
) -> Result<(Self, Response), C::ContractError> {
|
||||
// if we're instantiating fresh contract it means there was no pre-existing state
|
||||
let env = custom_env.unwrap_or_else(mock_env);
|
||||
let state = ContractState::new_with_env(env);
|
||||
let mut this = Self::new(state);
|
||||
|
||||
let env = this.state.env_cloned();
|
||||
let deps = this.state.deps_mut();
|
||||
|
||||
let res = C::instantiate(deps, env, info, msg)?;
|
||||
Ok((this, res))
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
&mut self,
|
||||
info: MessageInfo,
|
||||
msg: C::ExecuteMsg,
|
||||
) -> Result<Response, C::ContractError> {
|
||||
let env = self.state.env_cloned();
|
||||
let deps = self.state.deps_mut();
|
||||
|
||||
C::execute(deps, env, info, msg)
|
||||
}
|
||||
|
||||
pub fn query(&self, msg: C::QueryMsg) -> Result<QueryResponse, C::ContractError> {
|
||||
let env = self.state.env_cloned();
|
||||
let deps = self.state.deps();
|
||||
|
||||
C::query(deps, env, msg)
|
||||
}
|
||||
|
||||
pub fn query_de<T: DeserializeOwned>(&self, msg: C::QueryMsg) -> Result<T, C::ContractError> {
|
||||
self.query(msg).map(|res| from_slice(&res).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, QueryResponse, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
// TODO: see if it's possible to create a macro to auto-derive it
|
||||
// if you intend to use the MultiContractMock, you need to implement this trait
|
||||
// for your contract
|
||||
/// ```
|
||||
/// use cosmwasm_std::{
|
||||
/// entry_point, Deps, DepsMut, Env, MessageInfo, Querier, QueryResponse, Response, StdError,
|
||||
/// Storage,
|
||||
/// };
|
||||
/// use cosmwasm_contract_testing::TestableContract;
|
||||
///
|
||||
/// type ExecuteMsg = ();
|
||||
/// type QueryMsg = ();
|
||||
/// type InstantiateMsg = ();
|
||||
/// type ContractError = StdError;
|
||||
///
|
||||
/// #[entry_point]
|
||||
/// pub fn instantiate (
|
||||
/// deps: DepsMut,
|
||||
/// env: Env,
|
||||
/// info: MessageInfo,
|
||||
/// msg: InstantiateMsg,
|
||||
/// ) -> Result<Response, ContractError> {
|
||||
/// Ok(Default::default())
|
||||
/// }
|
||||
///
|
||||
/// #[entry_point]
|
||||
/// pub fn execute(
|
||||
/// deps: DepsMut,
|
||||
/// env: Env,
|
||||
/// info: MessageInfo,
|
||||
/// msg: ExecuteMsg,
|
||||
/// ) -> Result<Response, ContractError> {
|
||||
/// Ok(Default::default())
|
||||
/// }
|
||||
///
|
||||
/// #[entry_point]
|
||||
/// pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
|
||||
/// Ok(Default::default())
|
||||
/// }
|
||||
///
|
||||
/// struct MyContract;
|
||||
///
|
||||
/// impl TestableContract for MyContract {
|
||||
/// type ContractError = ContractError;
|
||||
/// type InstantiateMsg = InstantiateMsg;
|
||||
/// type ExecuteMsg = ExecuteMsg;
|
||||
/// type QueryMsg = QueryMsg;
|
||||
///
|
||||
/// fn new() -> Self {
|
||||
/// MyContract
|
||||
/// }
|
||||
///
|
||||
/// fn instantiate(
|
||||
/// deps: DepsMut<'_>,
|
||||
/// env: Env,
|
||||
/// info: MessageInfo,
|
||||
/// msg: Self::InstantiateMsg,
|
||||
/// ) -> Result<Response, Self::ContractError> {
|
||||
/// instantiate(deps, env, info, msg)
|
||||
/// }
|
||||
///
|
||||
/// fn execute(
|
||||
/// deps: DepsMut<'_>,
|
||||
/// env: Env,
|
||||
/// info: MessageInfo,
|
||||
/// msg: Self::ExecuteMsg,
|
||||
/// ) -> Result<Response, Self::ContractError> {
|
||||
/// execute(deps, env, info, msg)
|
||||
/// }
|
||||
///
|
||||
/// fn query(
|
||||
/// deps: Deps<'_>,
|
||||
/// env: Env,
|
||||
/// msg: Self::QueryMsg,
|
||||
/// ) -> Result<QueryResponse, Self::ContractError> {
|
||||
/// query(deps, env, msg)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait TestableContract {
|
||||
type ContractError: ToString;
|
||||
type InstantiateMsg: DeserializeOwned;
|
||||
type ExecuteMsg: DeserializeOwned;
|
||||
type QueryMsg: DeserializeOwned;
|
||||
|
||||
fn new() -> Self;
|
||||
|
||||
fn instantiate(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: Self::InstantiateMsg,
|
||||
) -> Result<Response, Self::ContractError>;
|
||||
|
||||
fn execute(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: Self::ExecuteMsg,
|
||||
) -> Result<Response, Self::ContractError>;
|
||||
|
||||
fn query(
|
||||
deps: Deps<'_>,
|
||||
env: Env,
|
||||
msg: Self::QueryMsg,
|
||||
) -> Result<QueryResponse, Self::ContractError>;
|
||||
}
|
||||
|
||||
pub(crate) mod sealed {
|
||||
use crate::deserialize_msg;
|
||||
use crate::traits::TestableContract;
|
||||
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response};
|
||||
|
||||
pub(crate) trait ErasedTestableContract {
|
||||
fn query(&self, deps: Deps<'_>, env: Env, raw_msg: Binary)
|
||||
-> Result<QueryResponse, String>;
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
raw_msg: Binary,
|
||||
) -> Result<Response, String>;
|
||||
|
||||
fn instantiate(
|
||||
&self,
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
raw_msg: Binary,
|
||||
) -> Result<Response, String>;
|
||||
}
|
||||
|
||||
impl<T: TestableContract> ErasedTestableContract for T {
|
||||
fn query(
|
||||
&self,
|
||||
deps: Deps<'_>,
|
||||
env: Env,
|
||||
raw_msg: Binary,
|
||||
) -> Result<QueryResponse, String> {
|
||||
let msg = deserialize_msg(&raw_msg).expect("failed to deserialize 'QueryMsg'");
|
||||
<Self as TestableContract>::query(deps, env, msg).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
raw_msg: Binary,
|
||||
) -> Result<Response, String> {
|
||||
let msg = deserialize_msg(&raw_msg).expect("failed to deserialize 'ExecuteMsg'");
|
||||
<Self as TestableContract>::execute(deps, env, info, msg).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn instantiate(
|
||||
&self,
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
raw_msg: Binary,
|
||||
) -> Result<Response, String> {
|
||||
let msg = deserialize_msg(&raw_msg).expect("failed to deserialize 'InstantiateMsg'");
|
||||
<Self as TestableContract>::instantiate(deps, env, info, msg)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
description = "Common library for the Nym vesting contract"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
@@ -9,8 +9,8 @@ repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0"
|
||||
mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.2.0" }
|
||||
mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.1.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
ts-rs = {version = "6.1.2", optional = true}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
async-trait = { version = "0.1.51" }
|
||||
|
||||
log = { workspace = true }
|
||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-crypto"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
description = "Crypto library for the nym mixnet"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
@@ -26,8 +26,8 @@ subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
|
||||
thiserror = "1.0.37"
|
||||
|
||||
# internal
|
||||
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0" }
|
||||
nym-pemstore = { path = "../../common/pemstore", version = "0.2.0" }
|
||||
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.1.0" }
|
||||
nym-pemstore = { path = "../../common/pemstore", version = "0.1.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.2"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "rand")]
|
||||
@@ -132,14 +131,6 @@ impl PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PublicKey {
|
||||
type Err = KeyRecoveryError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
PublicKey::from_base58_string(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
||||
@@ -6,6 +6,6 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
async-trait = { version = "0.1.51" }
|
||||
thiserror = "1.0"
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ nym-sphinx-types = { path = "types" }
|
||||
|
||||
# those dependencies are due to intriducing preparer and receiver. Perpaphs that indicates they should be moved
|
||||
# to separate crate?
|
||||
nym-crypto = { path = "../crypto", version = "0.2.0" }
|
||||
nym-crypto = { path = "../crypto", version = "0.1.0" }
|
||||
nym-topology = { path = "../topology" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-sphinx-types"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
description = "Re-export sphinx packet types"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-pemstore"
|
||||
description = "Store private-public keypairs in PEM format"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# List of published common crates
|
||||
nym-bin-common
|
||||
nym-contracts-common
|
||||
nym-crypto
|
||||
nym-mixnet-contract-common
|
||||
nym-pemstore
|
||||
nym-sphinx-types
|
||||
nym-vesting-contract-common
|
||||
@@ -9,7 +9,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
async-trait = { version = "0.1.51" }
|
||||
log = { workspace = true }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::error::StatsError;
|
||||
use crate::StatsMessage;
|
||||
|
||||
pub const DEFAULT_STATISTICS_SERVICE_ADDRESS: &str = "127.0.0.1";
|
||||
pub const DEFAULT_STATISTICS_SERVICE_PORT: u16 = 8091;
|
||||
pub const DEFAULT_STATISTICS_SERVICE_PORT: u16 = 8090;
|
||||
|
||||
pub const STATISTICS_SERVICE_VERSION: &str = "/v1";
|
||||
pub const STATISTICS_SERVICE_API_STATISTICS: &str = "statistic";
|
||||
|
||||
@@ -16,7 +16,6 @@ bs58 = "0.4"
|
||||
log = { workspace = true }
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
thiserror = "1.0.37"
|
||||
async-trait = { workspace = true, optional = true }
|
||||
|
||||
## internal
|
||||
nym-crypto = { path = "../crypto" }
|
||||
@@ -24,7 +23,3 @@ nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contra
|
||||
nym-sphinx-addressing = { path = "../nymsphinx/addressing" }
|
||||
nym-sphinx-types = { path = "../nymsphinx/types" }
|
||||
nym-bin-common = { path = "../bin-common" }
|
||||
|
||||
[features]
|
||||
default = ["provider-trait"]
|
||||
provider-trait = ["async-trait"]
|
||||
@@ -20,9 +20,6 @@ pub mod filter;
|
||||
pub mod gateway;
|
||||
pub mod mix;
|
||||
|
||||
#[cfg(feature = "provider-trait")]
|
||||
pub mod provider_trait;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum NymTopologyError {
|
||||
#[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")]
|
||||
@@ -42,16 +39,6 @@ pub enum NymTopologyError {
|
||||
|
||||
#[error("No mixnodes available on layer {layer}")]
|
||||
EmptyMixLayer { layer: MixLayer },
|
||||
|
||||
#[error("Uneven layer distribution. Layer {layer} has {nodes} on it, while we expected a value between {lower_bound} and {upper_bound} as we have {total_nodes} nodes in total. Full breakdown: {layer_distribution:?}")]
|
||||
UnevenLayerDistribution {
|
||||
layer: MixLayer,
|
||||
nodes: usize,
|
||||
lower_bound: usize,
|
||||
upper_bound: usize,
|
||||
total_nodes: usize,
|
||||
layer_distribution: Vec<(MixLayer, usize)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -110,7 +97,7 @@ impl NymTopology {
|
||||
}
|
||||
|
||||
pub fn num_mixnodes(&self) -> usize {
|
||||
self.mixes.values().map(|m| m.len()).sum()
|
||||
self.mixes.values().flat_map(|m| m.iter()).count()
|
||||
}
|
||||
|
||||
pub fn mixes_as_vec(&self) -> Vec<mix::Node> {
|
||||
@@ -251,46 +238,6 @@ impl NymTopology {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_even_layer_distribution(
|
||||
&self,
|
||||
lower_threshold: f32,
|
||||
upper_threshold: f32,
|
||||
) -> Result<(), NymTopologyError> {
|
||||
let mixnodes_count = self.num_mixnodes();
|
||||
|
||||
let layers = self
|
||||
.mixes
|
||||
.iter()
|
||||
.map(|(k, v)| (*k, v.len()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if self.gateways.is_empty() {
|
||||
return Err(NymTopologyError::NoGatewaysAvailable);
|
||||
}
|
||||
|
||||
if layers.is_empty() {
|
||||
return Err(NymTopologyError::NoMixnodesAvailable);
|
||||
}
|
||||
|
||||
let upper_bound = (mixnodes_count as f32 * upper_threshold) as usize;
|
||||
let lower_bound = (mixnodes_count as f32 * lower_threshold) as usize;
|
||||
|
||||
for (layer, nodes) in &layers {
|
||||
if nodes < &lower_bound || nodes > &upper_bound {
|
||||
return Err(NymTopologyError::UnevenLayerDistribution {
|
||||
layer: *layer,
|
||||
nodes: *nodes,
|
||||
lower_bound,
|
||||
upper_bound,
|
||||
total_nodes: mixnodes_count,
|
||||
layer_distribution: layers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter_system_version(&self, expected_version: &str) -> Self {
|
||||
self.filter_node_versions(expected_version)
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
use crate::{filter, NetworkAddress};
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
pub use nym_mixnet_contract_common::Layer;
|
||||
use nym_mixnet_contract_common::{MixId, MixNodeBond};
|
||||
use nym_mixnet_contract_common::{Layer, MixId, MixNodeBond};
|
||||
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx_types::Node as SphinxNode;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::NymTopology;
|
||||
pub use async_trait::async_trait;
|
||||
|
||||
// hehe, wasm
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
pub trait TopologyProvider: Send {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology>;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
pub trait TopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology>;
|
||||
}
|
||||
|
||||
pub struct HardcodedTopologyProvider {
|
||||
topology: NymTopology,
|
||||
}
|
||||
|
||||
impl HardcodedTopologyProvider {
|
||||
pub fn new(topology: NymTopology) -> Self {
|
||||
HardcodedTopologyProvider { topology }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl TopologyProvider for HardcodedTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
Some(self.topology.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl TopologyProvider for HardcodedTopologyProvider {
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
Some(self.topology.clone())
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,11 @@ members = [
|
||||
"multisig/cw3-flex-multisig",
|
||||
"multisig/cw4-group",
|
||||
"coconut-test",
|
||||
"coconut-dkg"
|
||||
"coconut-dkg",
|
||||
]
|
||||
|
||||
exclude = ["integration-tests"]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Nym Technologies SA"]
|
||||
repository = "https://github.com/nymtech/nym"
|
||||
|
||||
@@ -9,7 +9,7 @@ use cw_storage_plus::Bound;
|
||||
|
||||
use crate::storage::{self, SPEND_CREDENTIAL_PAGE_DEFAULT_LIMIT, SPEND_CREDENTIAL_PAGE_MAX_LIMIT};
|
||||
|
||||
pub(crate) fn query_all_spent_credentials_paged(
|
||||
pub fn query_all_spent_credentials_paged(
|
||||
deps: Deps<'_>,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
@@ -37,7 +37,7 @@ pub(crate) fn query_all_spent_credentials_paged(
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn query_spent_credential(
|
||||
pub fn query_spent_credential(
|
||||
deps: Deps<'_>,
|
||||
blinded_serial_number: String,
|
||||
) -> StdResult<SpendCredentialResponse> {
|
||||
|
||||
@@ -80,7 +80,7 @@ pub(crate) mod tests {
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::support::tests::fixtures::dealer_details_fixture;
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, GROUP_MEMBERS};
|
||||
use crate::support::tests::helpers::GROUP_MEMBERS;
|
||||
use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cw4::Member;
|
||||
@@ -147,8 +147,6 @@ pub(crate) mod tests {
|
||||
.block
|
||||
.time
|
||||
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
|
||||
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
advance_epoch_state(deps.as_mut(), env).unwrap();
|
||||
|
||||
let ret = try_add_dealer(
|
||||
|
||||
@@ -53,7 +53,6 @@ pub(crate) mod tests {
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::support::tests::fixtures::{dealer_details_fixture, dealing_bytes_fixture};
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::add_fixture_dealer;
|
||||
use coconut_dkg_common::dealer::DealerDetails;
|
||||
use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
@@ -81,7 +80,6 @@ pub(crate) mod tests {
|
||||
.block
|
||||
.time
|
||||
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
advance_epoch_state(deps.as_mut(), env).unwrap();
|
||||
|
||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false)
|
||||
|
||||
@@ -6,19 +6,17 @@ use crate::error::ContractError;
|
||||
use coconut_dkg_common::types::{Epoch, InitialReplacementData};
|
||||
use cosmwasm_std::Storage;
|
||||
|
||||
pub(crate) fn query_current_epoch(storage: &dyn Storage) -> Result<Epoch, ContractError> {
|
||||
pub fn query_current_epoch(storage: &dyn Storage) -> Result<Epoch, ContractError> {
|
||||
CURRENT_EPOCH
|
||||
.load(storage)
|
||||
.map_err(|_| ContractError::EpochNotInitialised)
|
||||
}
|
||||
|
||||
pub(crate) fn query_current_epoch_threshold(
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Option<u64>, ContractError> {
|
||||
pub fn query_current_epoch_threshold(storage: &dyn Storage) -> Result<Option<u64>, ContractError> {
|
||||
Ok(THRESHOLD.may_load(storage)?)
|
||||
}
|
||||
|
||||
pub(crate) fn query_initial_dealers(
|
||||
pub fn query_initial_dealers(
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Option<InitialReplacementData>, ContractError> {
|
||||
Ok(INITIAL_REPLACEMENT_DATA.may_load(storage)?)
|
||||
|
||||
@@ -89,29 +89,23 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
|
||||
let current_epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||
let next_epoch = if let Some(state) = current_epoch.state.next() {
|
||||
// We are during DKG process
|
||||
let mut new_state = state;
|
||||
if let EpochState::DealingExchange { resharing } = state {
|
||||
let current_dealers = current_dealers()
|
||||
.keys(deps.storage, None, None, Order::Ascending)
|
||||
.collect::<Result<Vec<Addr>, _>>()?;
|
||||
if current_dealers.is_empty() {
|
||||
// If no dealer registered yet, we just stay in the same state until there's at least one
|
||||
new_state = current_epoch.state;
|
||||
} else {
|
||||
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
|
||||
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
|
||||
THRESHOLD.save(deps.storage, &threshold)?;
|
||||
if !resharing {
|
||||
let replacement_data = InitialReplacementData {
|
||||
initial_dealers: current_dealers,
|
||||
initial_height: None,
|
||||
};
|
||||
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
|
||||
}
|
||||
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
|
||||
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
|
||||
THRESHOLD.save(deps.storage, &threshold)?;
|
||||
if !resharing {
|
||||
let replacement_data = InitialReplacementData {
|
||||
initial_dealers: current_dealers,
|
||||
initial_height: None,
|
||||
};
|
||||
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
Epoch::new(
|
||||
new_state,
|
||||
state,
|
||||
current_epoch.epoch_id,
|
||||
current_epoch.time_configuration,
|
||||
env.block.time,
|
||||
@@ -398,14 +392,6 @@ pub(crate) mod tests {
|
||||
EarlyEpochStateAdvancement(1)
|
||||
);
|
||||
|
||||
env.block.time = env.block.time.plus_seconds(1);
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
epoch.state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
|
||||
// setup dealer details
|
||||
let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1));
|
||||
for details in all_details.iter() {
|
||||
@@ -418,7 +404,7 @@ pub(crate) mod tests {
|
||||
.may_load(&deps.storage)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
env.block.time = env.block.time.plus_seconds(epoch.time_configuration.public_key_submission_time_secs);
|
||||
env.block.time = env.block.time.plus_seconds(1);
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::instantiate;
|
||||
use crate::dealers::storage::current_dealers;
|
||||
use coconut_dkg_common::msg::InstantiateMsg;
|
||||
use coconut_dkg_common::types::DealerDetails;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
|
||||
use cosmwasm_std::{
|
||||
from_binary, to_binary, Addr, ContractResult, DepsMut, Empty, MemoryStorage, OwnedDeps,
|
||||
QuerierResult, SystemResult, WasmQuery,
|
||||
from_binary, to_binary, ContractResult, Empty, MemoryStorage, OwnedDeps, QuerierResult,
|
||||
SystemResult, WasmQuery,
|
||||
};
|
||||
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
|
||||
use lazy_static::lazy_static;
|
||||
@@ -24,22 +22,6 @@ lazy_static! {
|
||||
pub static ref GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(vec![]);
|
||||
}
|
||||
|
||||
pub fn add_fixture_dealer(deps: DepsMut<'_>) {
|
||||
let owner = Addr::unchecked("owner");
|
||||
current_dealers()
|
||||
.save(
|
||||
deps.storage,
|
||||
&owner,
|
||||
&DealerDetails {
|
||||
address: owner.clone(),
|
||||
bte_public_key_with_proof: String::new(),
|
||||
announce_address: String::new(),
|
||||
assigned_index: 100,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn querier_handler(query: &WasmQuery) -> QuerierResult {
|
||||
let bin = match query {
|
||||
WasmQuery::Smart { contract_addr, msg } => {
|
||||
|
||||
@@ -91,7 +91,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, MULTISIG_CONTRACT};
|
||||
use crate::support::tests::helpers::MULTISIG_CONTRACT;
|
||||
use coconut_dkg_common::dealer::DealerDetails;
|
||||
use coconut_dkg_common::types::{EpochState, TimeConfiguration};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
@@ -104,7 +104,6 @@ mod tests {
|
||||
let info = mock_info("requester", &[]);
|
||||
let share = "share".to_string();
|
||||
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
@@ -172,7 +171,6 @@ mod tests {
|
||||
.to_string()
|
||||
}
|
||||
);
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
@@ -249,7 +247,6 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
@@ -295,7 +292,6 @@ mod tests {
|
||||
let share = "share".to_string();
|
||||
let multisig_info = mock_info(MULTISIG_CONTRACT, &[]);
|
||||
|
||||
add_fixture_dealer(deps.as_mut());
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "integration-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dev-dependencies]
|
||||
cosmwasm-std = "1.0.0"
|
||||
cw-storage-plus = "0.13.4"
|
||||
cosmwasm-contract-testing = { path = "../../common/cosmwasm-smart-contracts/testing" }
|
||||
nym-mixnet-contract-common = { path= "../../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-vesting-contract-common = { path= "../../common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-vesting-contract = { path = "../vesting", features = ["testing-mocks"] }
|
||||
nym-mixnet-contract = { path = "../mixnet", features = ["testing-mocks"] }
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_contract_testing::{env_with_block_info, ContractState, MultiContractMock};
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{Addr, BankMsg, BlockInfo, Timestamp};
|
||||
use cw_storage_plus::Map;
|
||||
use mixnet_contract::MixnetContract;
|
||||
use nym_mixnet_contract_common::rewarding::PendingRewardResponse;
|
||||
use vesting_contract::vesting::Account;
|
||||
use vesting_contract::VestingContract;
|
||||
|
||||
// this is not directly exported by the vesting contract, but we can easily recreate it
|
||||
const VESTING_ACCOUNTS: Map<'_, Addr, Account> = Map::new("acc");
|
||||
|
||||
// hardcoded values from the data dump sources
|
||||
const MIXNET_CONTRACT_ADDRESS: &str =
|
||||
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
|
||||
const VESTING_CONTRACT_ADDRESS: &str =
|
||||
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
|
||||
|
||||
fn set_mock() -> MultiContractMock {
|
||||
let current_block = BlockInfo {
|
||||
height: 1928125,
|
||||
time: Timestamp::from_seconds(1676482616),
|
||||
chain_id: "nymnet".to_string(),
|
||||
};
|
||||
let custom_env = env_with_block_info(current_block);
|
||||
|
||||
let mix_mock = ContractState::try_from_state_dump(
|
||||
"contract-states/15.02.23-173000-qwerty-mixnet.json",
|
||||
Some(custom_env.clone()),
|
||||
)
|
||||
.unwrap()
|
||||
.with_contract_address(MIXNET_CONTRACT_ADDRESS);
|
||||
let vesting_mock = ContractState::try_from_state_dump(
|
||||
"contract-states/15.02.23-173000-qwerty-vesting.json",
|
||||
Some(custom_env),
|
||||
)
|
||||
.unwrap()
|
||||
.with_contract_address(VESTING_CONTRACT_ADDRESS);
|
||||
|
||||
let mut multi_mock = MultiContractMock::new();
|
||||
|
||||
multi_mock.add_contract::<MixnetContract>(mix_mock).unwrap();
|
||||
multi_mock
|
||||
.add_contract::<VestingContract>(vesting_mock)
|
||||
.unwrap();
|
||||
multi_mock
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claiming_vesting_delegator_rewards() {
|
||||
let mut multi_mock = set_mock();
|
||||
|
||||
let dummy_account = Addr::unchecked("n1ktpuwtweku40uaxcl4uq7mdkkmjeh698g3l3c8");
|
||||
|
||||
// do some queries to verify state is updated correctly for both contracts
|
||||
let pending_reward: PendingRewardResponse = multi_mock
|
||||
.query_de::<MixnetContract, _>(
|
||||
MIXNET_CONTRACT_ADDRESS,
|
||||
nym_mixnet_contract_common::QueryMsg::GetPendingDelegatorReward {
|
||||
address: dummy_account.to_string(),
|
||||
mix_id: 8,
|
||||
proxy: Some(VESTING_CONTRACT_ADDRESS.to_string()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let pending_reward_amount = pending_reward.amount_earned.unwrap().amount;
|
||||
|
||||
// we can also get whatever we want directly from storage!
|
||||
let contract_state = multi_mock.contract_state(VESTING_CONTRACT_ADDRESS).unwrap();
|
||||
let vesting_account = contract_state
|
||||
.load_map_value(&VESTING_ACCOUNTS, dummy_account.clone())
|
||||
.unwrap();
|
||||
let vesting_balance = vesting_account
|
||||
.load_balance(contract_state.deps().storage)
|
||||
.unwrap();
|
||||
|
||||
let res = multi_mock.execute_full::<VestingContract>(
|
||||
VESTING_CONTRACT_ADDRESS,
|
||||
mock_info(dummy_account.as_str(), &[]),
|
||||
nym_vesting_contract_common::ExecuteMsg::ClaimDelegatorReward { mix_id: 8 },
|
||||
);
|
||||
|
||||
match res {
|
||||
Ok(success) => {
|
||||
println!("{}", success.pretty());
|
||||
|
||||
// check the output
|
||||
|
||||
// unfortunately `ClaimDelegatorReward` doesn't emit any events, but we can see
|
||||
// it's going to result into a call into the mixnet contract
|
||||
assert_eq!(
|
||||
success.steps[0].further_execution[0].contract.as_str(),
|
||||
MIXNET_CONTRACT_ADDRESS
|
||||
);
|
||||
|
||||
// mixnet contract will emit a `v2_withdraw_delegator_reward` event
|
||||
// and call the vesting contract again
|
||||
assert_eq!(
|
||||
"v2_withdraw_delegator_reward",
|
||||
success.steps[1].events[0].ty
|
||||
);
|
||||
assert_eq!(
|
||||
success.steps[1].further_execution[0].contract.as_str(),
|
||||
VESTING_CONTRACT_ADDRESS
|
||||
);
|
||||
// and will move our reward amount into the vesting contract...
|
||||
assert!(matches!(
|
||||
&success.steps[1].bank_msgs[0],
|
||||
BankMsg::Send { to_address, amount }
|
||||
if to_address == VESTING_CONTRACT_ADDRESS && amount[0].amount == pending_reward_amount
|
||||
));
|
||||
|
||||
// and finally the vesting contract will emit the mistyped `track_reaward` event
|
||||
assert_eq!("track_reaward", success.steps[2].events[0].ty);
|
||||
}
|
||||
Err(err) => panic!("{err}"),
|
||||
}
|
||||
|
||||
// state after execution (we can still read values the 'normal' way)
|
||||
let updated_state = multi_mock.contract_state(VESTING_CONTRACT_ADDRESS).unwrap();
|
||||
let deps = updated_state.deps();
|
||||
let vesting_account = VESTING_ACCOUNTS.load(deps.storage, dummy_account).unwrap();
|
||||
let new_vesting_balance = vesting_account.load_balance(deps.storage).unwrap();
|
||||
assert_eq!(new_vesting_balance, vesting_balance + pending_reward_amount)
|
||||
}
|
||||
@@ -22,8 +22,8 @@ name = "mixnet_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.2.0" }
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.1.0" }
|
||||
#nym-config = { path = "../../common/config"}
|
||||
|
||||
cosmwasm-std = "1.0.0"
|
||||
@@ -36,15 +36,23 @@ serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "1.0.23" }
|
||||
time = { version = "0.3", features = ["macros"] }
|
||||
|
||||
cosmwasm-contract-testing = { path = "../../common/cosmwasm-smart-contracts/testing", default-features = false, features = ["testable-trait"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cosmwasm-schema = "1.0.0"
|
||||
rand_chacha = "0.2"
|
||||
#rand = "0.7"
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
cosmwasm-contract-testing = { path = "../../common/cosmwasm-smart-contracts/testing" }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
|
||||
|
||||
[[example]]
|
||||
name = "mock_testing"
|
||||
required-features = ["testing-mocks"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
contract-testing = ["mixnet-contract-common/contract-testing"]
|
||||
testing-mocks = ["contract-testing", "cosmwasm-contract-testing"]
|
||||
|
||||
@@ -4,10 +4,5 @@
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
fn main() {
|
||||
let mut config = Config::default();
|
||||
if std::env::var("DOCS_RS").is_ok() {
|
||||
// If we don't have access to git information, such as in a docs.rs build, don't error
|
||||
*config.git_mut().skip_if_error_mut() = true;
|
||||
}
|
||||
vergen(config).expect("failed to extract build metadata");
|
||||
vergen(Config::default()).expect("failed to extract build metadata")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_contract_testing::{env_with_block_info, ContractState, SingleContractMock};
|
||||
use cosmwasm_std::from_slice;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{BlockInfo, Timestamp};
|
||||
use mixnet_contract::mixnet_contract_settings::storage::CONTRACT_STATE;
|
||||
use mixnet_contract::mixnodes::queries::query_mixnode_details;
|
||||
use mixnet_contract::MixnetContract;
|
||||
use mixnet_contract::{mixnet_contract_settings, mixnodes};
|
||||
use mixnet_contract_common::{ContractState as MixnetContractState, ExecuteMsg, Layer, QueryMsg};
|
||||
|
||||
const MIXNET_CONTRACT_ADDRESS: &str =
|
||||
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
|
||||
|
||||
fn set_mock() -> SingleContractMock<MixnetContract> {
|
||||
let current_block = BlockInfo {
|
||||
height: 1928125,
|
||||
time: Timestamp::from_seconds(1676482616),
|
||||
chain_id: "nymnet".to_string(),
|
||||
};
|
||||
let custom_env = env_with_block_info(current_block);
|
||||
|
||||
let mix_state = ContractState::try_from_state_dump(
|
||||
"../integration-tests/contract-states/15.02.23-173000-qwerty-mixnet.json",
|
||||
Some(custom_env.clone()),
|
||||
)
|
||||
.unwrap()
|
||||
.with_contract_address(MIXNET_CONTRACT_ADDRESS);
|
||||
|
||||
SingleContractMock::new(mix_state)
|
||||
}
|
||||
|
||||
fn normal_queries() {
|
||||
let mock = set_mock();
|
||||
|
||||
// the simplest example of a query: 'what's the current contract state?'
|
||||
let query = QueryMsg::GetState {};
|
||||
let result: MixnetContractState = mock.query_de(query).unwrap();
|
||||
// println!("{:?}", result);
|
||||
assert_eq!(
|
||||
"n1fxwdqgwht4j2suv5pr55304kt9z0avrvxs9ls0",
|
||||
result.owner.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
fn queries_with_native_functions() {
|
||||
let mock = set_mock();
|
||||
|
||||
// access exactly the same information as before, but this time with native functions
|
||||
let deps = mock.deps();
|
||||
let result = mixnet_contract_settings::queries::query_contract_state(deps).unwrap();
|
||||
// println!("{:?}", result);
|
||||
assert_eq!(
|
||||
"n1fxwdqgwht4j2suv5pr55304kt9z0avrvxs9ls0",
|
||||
result.owner.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
fn raw_storage_reads() {
|
||||
// we can also read any arbitrary data that's normally not exposed via queries
|
||||
// for this example, let's read exactly the same data again
|
||||
let mock = set_mock();
|
||||
|
||||
// wrapped in a cw-storage-plus 'item'
|
||||
let result = mock.state.load_item(&CONTRACT_STATE).unwrap();
|
||||
// println!("{:?}", result);
|
||||
assert_eq!(
|
||||
"n1fxwdqgwht4j2suv5pr55304kt9z0avrvxs9ls0",
|
||||
result.owner.as_ref()
|
||||
);
|
||||
|
||||
// a raw key-value read
|
||||
let result_raw = mock.state.read_key(b"state").unwrap();
|
||||
let result: MixnetContractState = from_slice(&result_raw).unwrap();
|
||||
assert_eq!(
|
||||
"n1fxwdqgwht4j2suv5pr55304kt9z0avrvxs9ls0",
|
||||
result.owner.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
fn normal_transactions() {
|
||||
let mut mock = set_mock();
|
||||
|
||||
// pretend you're the rewarding validator and force assign somebody's layer!
|
||||
let current_mixnode = query_mixnode_details(mock.deps(), 7).unwrap();
|
||||
assert_eq!(
|
||||
current_mixnode
|
||||
.mixnode_details
|
||||
.unwrap()
|
||||
.bond_information
|
||||
.layer,
|
||||
Layer::One
|
||||
);
|
||||
let rewarding_validator = mixnet_contract_settings::queries::query_contract_state(mock.deps())
|
||||
.unwrap()
|
||||
.rewarding_validator_address;
|
||||
|
||||
let msg_sender = mock_info(rewarding_validator.as_ref(), &[]);
|
||||
let msg = ExecuteMsg::AssignNodeLayer {
|
||||
mix_id: 7,
|
||||
layer: Layer::Two,
|
||||
};
|
||||
mock.execute(msg_sender, msg).unwrap();
|
||||
|
||||
let updated_mixnode = query_mixnode_details(mock.deps(), 7).unwrap();
|
||||
assert_eq!(
|
||||
updated_mixnode
|
||||
.mixnode_details
|
||||
.unwrap()
|
||||
.bond_information
|
||||
.layer,
|
||||
Layer::Two
|
||||
);
|
||||
}
|
||||
|
||||
fn changing_state_with_native_functions() {
|
||||
// do the same thing but this time calling contract methods directly
|
||||
let mut mock = set_mock();
|
||||
|
||||
let current_mixnode = query_mixnode_details(mock.deps(), 7).unwrap();
|
||||
assert_eq!(
|
||||
current_mixnode
|
||||
.mixnode_details
|
||||
.unwrap()
|
||||
.bond_information
|
||||
.layer,
|
||||
Layer::One
|
||||
);
|
||||
let rewarding_validator = mixnet_contract_settings::queries::query_contract_state(mock.deps())
|
||||
.unwrap()
|
||||
.rewarding_validator_address;
|
||||
|
||||
let msg_sender = mock_info(rewarding_validator.as_ref(), &[]);
|
||||
let deps = mock.deps_mut();
|
||||
|
||||
mixnodes::transactions::assign_mixnode_layer(deps, msg_sender, 7, Layer::Two).unwrap();
|
||||
let updated_mixnode = query_mixnode_details(mock.deps(), 7).unwrap();
|
||||
assert_eq!(
|
||||
updated_mixnode
|
||||
.mixnode_details
|
||||
.unwrap()
|
||||
.bond_information
|
||||
.layer,
|
||||
Layer::Two
|
||||
);
|
||||
}
|
||||
|
||||
fn writing_to_raw_storage() {
|
||||
// bypass this whole transaction business, authorization checks, etc and just write to the storage yourself
|
||||
let mut mock = set_mock();
|
||||
|
||||
let mut mix_bond = mixnodes::storage::mixnode_bonds()
|
||||
.load(mock.deps().storage, 7)
|
||||
.unwrap();
|
||||
assert_eq!(mix_bond.layer, Layer::One);
|
||||
mix_bond.layer = Layer::Two;
|
||||
|
||||
mixnodes::storage::mixnode_bonds()
|
||||
.save(mock.deps_mut().storage, 7, &mix_bond)
|
||||
.unwrap();
|
||||
|
||||
let updated_mixnode = query_mixnode_details(mock.deps(), 7).unwrap();
|
||||
assert_eq!(
|
||||
updated_mixnode
|
||||
.mixnode_details
|
||||
.unwrap()
|
||||
.bond_information
|
||||
.layer,
|
||||
Layer::Two
|
||||
);
|
||||
}
|
||||
|
||||
// run with `cargo run --example mock_testing --features="testing-mocks"`
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() {
|
||||
normal_queries();
|
||||
queries_with_native_functions();
|
||||
raw_storage_reads();
|
||||
|
||||
normal_transactions();
|
||||
changing_state_with_native_functions();
|
||||
writing_to_raw_storage();
|
||||
}
|
||||
@@ -16,7 +16,7 @@ use mixnet_contract_common::{
|
||||
PagedMixNodeDelegationsResponse,
|
||||
};
|
||||
|
||||
pub(crate) fn query_mixnode_delegations_paged(
|
||||
pub fn query_mixnode_delegations_paged(
|
||||
deps: Deps<'_>,
|
||||
mix_id: MixId,
|
||||
start_after: Option<String>,
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn query_mixnode_delegations_paged(
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn query_delegator_delegations_paged(
|
||||
pub fn query_delegator_delegations_paged(
|
||||
deps: Deps<'_>,
|
||||
delegation_owner: String,
|
||||
start_after: Option<(MixId, OwnerProxySubKey)>,
|
||||
@@ -83,7 +83,7 @@ pub(crate) fn query_delegator_delegations_paged(
|
||||
}
|
||||
|
||||
// queries for delegation value of given address for particular node
|
||||
pub(crate) fn query_mixnode_delegation(
|
||||
pub fn query_mixnode_delegation(
|
||||
deps: Deps<'_>,
|
||||
mix_id: MixId,
|
||||
delegation_owner: String,
|
||||
@@ -109,7 +109,7 @@ pub(crate) fn query_mixnode_delegation(
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn query_all_delegations_paged(
|
||||
pub fn query_all_delegations_paged(
|
||||
deps: Deps<'_>,
|
||||
start_after: Option<delegation::StorageKey>,
|
||||
limit: Option<u32>,
|
||||
|
||||
@@ -9,7 +9,7 @@ use mixnet_contract_common::{
|
||||
GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, IdentityKey, PagedGatewayResponse,
|
||||
};
|
||||
|
||||
pub(crate) fn query_gateways_paged(
|
||||
pub fn query_gateways_paged(
|
||||
deps: Deps<'_>,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: Option<u32>,
|
||||
@@ -31,10 +31,7 @@ pub(crate) fn query_gateways_paged(
|
||||
Ok(PagedGatewayResponse::new(nodes, limit, start_next_after))
|
||||
}
|
||||
|
||||
pub(crate) fn query_owned_gateway(
|
||||
deps: Deps<'_>,
|
||||
address: String,
|
||||
) -> StdResult<GatewayOwnershipResponse> {
|
||||
pub fn query_owned_gateway(deps: Deps<'_>, address: String) -> StdResult<GatewayOwnershipResponse> {
|
||||
let validated_addr = deps.api.addr_validate(&address)?;
|
||||
|
||||
let gateway = storage::gateways()
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
mod constants;
|
||||
pub mod constants;
|
||||
pub mod contract;
|
||||
mod delegations;
|
||||
mod families;
|
||||
mod gateways;
|
||||
mod interval;
|
||||
mod mixnet_contract_settings;
|
||||
mod mixnodes;
|
||||
mod rewards;
|
||||
mod support;
|
||||
pub mod delegations;
|
||||
pub mod families;
|
||||
pub mod gateways;
|
||||
pub mod interval;
|
||||
pub mod mixnet_contract_settings;
|
||||
pub mod mixnodes;
|
||||
pub mod rewards;
|
||||
pub mod support;
|
||||
|
||||
#[cfg(feature = "contract-testing")]
|
||||
mod testing;
|
||||
|
||||
#[cfg(feature = "testing-mocks")]
|
||||
pub use testing::mock_helpers::MixnetContract;
|
||||
|
||||
@@ -5,36 +5,32 @@ use super::storage;
|
||||
use cosmwasm_std::{Deps, StdResult};
|
||||
use mixnet_contract_common::{ContractBuildInformation, ContractState, ContractStateParams};
|
||||
|
||||
pub(crate) fn query_contract_state(deps: Deps<'_>) -> StdResult<ContractState> {
|
||||
pub fn query_contract_state(deps: Deps<'_>) -> StdResult<ContractState> {
|
||||
storage::CONTRACT_STATE.load(deps.storage)
|
||||
}
|
||||
|
||||
pub(crate) fn query_contract_settings_params(deps: Deps<'_>) -> StdResult<ContractStateParams> {
|
||||
pub fn query_contract_settings_params(deps: Deps<'_>) -> StdResult<ContractStateParams> {
|
||||
storage::CONTRACT_STATE
|
||||
.load(deps.storage)
|
||||
.map(|settings| settings.params)
|
||||
}
|
||||
|
||||
pub(crate) fn query_rewarding_validator_address(deps: Deps<'_>) -> StdResult<String> {
|
||||
pub fn query_rewarding_validator_address(deps: Deps<'_>) -> StdResult<String> {
|
||||
storage::CONTRACT_STATE
|
||||
.load(deps.storage)
|
||||
.map(|settings| settings.rewarding_validator_address.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn query_contract_version() -> ContractBuildInformation {
|
||||
pub fn query_contract_version() -> ContractBuildInformation {
|
||||
// as per docs
|
||||
// env! macro will expand to the value of the named environment variable at
|
||||
// compile time, yielding an expression of type `&'static str`
|
||||
ContractBuildInformation {
|
||||
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
||||
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
|
||||
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
commit_branch: option_env!("VERGEN_GIT_BRANCH")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
|
||||
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
|
||||
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
|
||||
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use cw_storage_plus::Item;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::ContractState;
|
||||
|
||||
pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY);
|
||||
pub const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY);
|
||||
|
||||
pub fn rewarding_validator_address(storage: &dyn Storage) -> Result<Addr, MixnetContractError> {
|
||||
Ok(CONTRACT_STATE
|
||||
|
||||
@@ -256,7 +256,7 @@ pub fn query_stake_saturation(deps: Deps<'_>, mix_id: MixId) -> StdResult<StakeS
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn query_layer_distribution(deps: Deps<'_>) -> StdResult<LayerDistribution> {
|
||||
pub fn query_layer_distribution(deps: Deps<'_>) -> StdResult<LayerDistribution> {
|
||||
storage::LAYERS.load(deps.storage)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,10 +48,10 @@ pub(crate) fn unbonded_mixnodes<'a>(
|
||||
IndexedMap::new(UNBONDED_MIXNODES_PK_NAMESPACE, indexes)
|
||||
}
|
||||
|
||||
pub(crate) const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY);
|
||||
pub const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY);
|
||||
pub const MIXNODE_ID_COUNTER: Item<MixId> = Item::new(NODE_ID_COUNTER_KEY);
|
||||
|
||||
pub(crate) struct MixnodeBondIndex<'a> {
|
||||
pub struct MixnodeBondIndex<'a> {
|
||||
pub(crate) owner: UniqueIndex<'a, Addr, MixNodeBond>,
|
||||
|
||||
pub(crate) identity_key: UniqueIndex<'a, IdentityKey, MixNodeBond>,
|
||||
@@ -70,7 +70,7 @@ impl<'a> IndexList<MixNodeBond> for MixnodeBondIndex<'a> {
|
||||
}
|
||||
|
||||
// mixnode_bonds() is the storage access function.
|
||||
pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeBondIndex<'a>> {
|
||||
pub fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeBondIndex<'a>> {
|
||||
let indexes = MixnodeBondIndex {
|
||||
owner: UniqueIndex::new(|d| d.owner.clone(), MIXNODES_OWNER_IDX_NAMESPACE),
|
||||
identity_key: UniqueIndex::new(
|
||||
|
||||
@@ -16,7 +16,7 @@ use mixnet_contract_common::rewarding::{
|
||||
};
|
||||
use mixnet_contract_common::{Delegation, MixId};
|
||||
|
||||
pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult<RewardingParams> {
|
||||
pub fn query_rewarding_params(deps: Deps<'_>) -> StdResult<RewardingParams> {
|
||||
storage::REWARDING_PARAMS.load(deps.storage)
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ fn zero_reward(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn query_estimated_current_epoch_operator_reward(
|
||||
pub fn query_estimated_current_epoch_operator_reward(
|
||||
deps: Deps<'_>,
|
||||
mix_id: MixId,
|
||||
estimated_performance: Performance,
|
||||
@@ -157,7 +157,7 @@ pub(crate) fn query_estimated_current_epoch_operator_reward(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn query_estimated_current_epoch_delegator_reward(
|
||||
pub fn query_estimated_current_epoch_delegator_reward(
|
||||
deps: Deps<'_>,
|
||||
owner: String,
|
||||
mix_id: MixId,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract;
|
||||
use cosmwasm_contract_testing::TestableContract;
|
||||
use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, QueryResponse, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::{ExecuteMsg, InstantiateMsg, QueryMsg};
|
||||
|
||||
pub struct MixnetContract;
|
||||
|
||||
impl TestableContract for MixnetContract {
|
||||
type ContractError = MixnetContractError;
|
||||
type InstantiateMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
|
||||
fn new() -> Self {
|
||||
MixnetContract
|
||||
}
|
||||
|
||||
fn instantiate(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: Self::InstantiateMsg,
|
||||
) -> Result<Response, Self::ContractError> {
|
||||
contract::instantiate(deps, env, info, msg)
|
||||
}
|
||||
|
||||
fn execute(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: Self::ExecuteMsg,
|
||||
) -> Result<Response, Self::ContractError> {
|
||||
contract::execute(deps, env, info, msg)
|
||||
}
|
||||
|
||||
fn query(
|
||||
deps: Deps<'_>,
|
||||
env: Env,
|
||||
msg: Self::QueryMsg,
|
||||
) -> Result<QueryResponse, Self::ContractError> {
|
||||
contract::query(deps, env, msg)
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod transactions;
|
||||
|
||||
#[cfg(feature = "testing-mocks")]
|
||||
pub mod mock_helpers;
|
||||
|
||||
@@ -20,9 +20,9 @@ name = "vesting_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.2.0" }
|
||||
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common", version = "0.2.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.2.0" }
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.1.0" }
|
||||
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common", version = "0.1.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.1.0" }
|
||||
|
||||
cosmwasm-std = { version = "1.0.0 "}
|
||||
cw-storage-plus = { version = "0.13.4", features = ["iterator"] }
|
||||
@@ -31,11 +31,14 @@ schemars = "0.8"
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "1.0" }
|
||||
|
||||
cosmwasm-contract-testing = { path = "../../common/cosmwasm-smart-contracts/testing", default-features = false, features = ["testable-trait"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.3.1"
|
||||
base64 = "0.21.0"
|
||||
hex = "0.4.3"
|
||||
serde_json = "1.0.66"
|
||||
cosmwasm-contract-testing = { path = "../../common/cosmwasm-smart-contracts/testing" }
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
testing-mocks = ["cosmwasm-contract-testing"]
|
||||
@@ -4,10 +4,5 @@
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
fn main() {
|
||||
let mut config = Config::default();
|
||||
if std::env::var("DOCS_RS").is_ok() {
|
||||
// If we don't have access to git information, such as in a docs.rs build, don't error
|
||||
*config.git_mut().skip_if_error_mut() = true;
|
||||
}
|
||||
vergen(Config::default()).expect("failed to extract build metadata")
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ pub fn try_withdraw_vested_coins(
|
||||
}
|
||||
|
||||
/// Transfer ownership of the entire vesting account.
|
||||
fn try_transfer_ownership(
|
||||
pub fn try_transfer_ownership(
|
||||
to_address: String,
|
||||
info: MessageInfo,
|
||||
deps: DepsMut<'_>,
|
||||
@@ -316,7 +316,7 @@ fn try_transfer_ownership(
|
||||
}
|
||||
|
||||
/// Set or update staking address for a vesting account.
|
||||
fn try_update_staking_address(
|
||||
pub fn try_update_staking_address(
|
||||
to_address: Option<String>,
|
||||
info: MessageInfo,
|
||||
deps: DepsMut<'_>,
|
||||
@@ -434,7 +434,7 @@ pub fn try_track_unbond_mixnode(
|
||||
}
|
||||
|
||||
/// Track reward collection, invoked by the mixnert contract after sucessful reward compounding or claiming
|
||||
fn try_track_reward(
|
||||
pub fn try_track_reward(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
amount: Coin,
|
||||
@@ -449,7 +449,7 @@ fn try_track_reward(
|
||||
}
|
||||
|
||||
/// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards.
|
||||
fn try_track_undelegation(
|
||||
pub fn try_track_undelegation(
|
||||
address: &str,
|
||||
mix_id: MixId,
|
||||
amount: Coin,
|
||||
@@ -466,7 +466,7 @@ fn try_track_undelegation(
|
||||
}
|
||||
|
||||
/// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]..
|
||||
fn try_delegate_to_mixnode(
|
||||
pub fn try_delegate_to_mixnode(
|
||||
mix_id: MixId,
|
||||
amount: Coin,
|
||||
on_behalf_of: Option<String>,
|
||||
@@ -491,7 +491,7 @@ fn try_delegate_to_mixnode(
|
||||
}
|
||||
|
||||
/// Claims operator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimOperatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
|
||||
fn try_claim_operator_reward(
|
||||
pub fn try_claim_operator_reward(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
) -> Result<Response, ContractError> {
|
||||
@@ -500,7 +500,7 @@ fn try_claim_operator_reward(
|
||||
}
|
||||
|
||||
/// Claims delegator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimDelegatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
|
||||
fn try_claim_delegator_reward(
|
||||
pub fn try_claim_delegator_reward(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
@@ -511,7 +511,7 @@ fn try_claim_delegator_reward(
|
||||
}
|
||||
|
||||
/// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].
|
||||
fn try_undelegate_from_mixnode(
|
||||
pub fn try_undelegate_from_mixnode(
|
||||
mix_id: MixId,
|
||||
on_behalf_of: Option<String>,
|
||||
info: MessageInfo,
|
||||
@@ -533,7 +533,7 @@ fn try_undelegate_from_mixnode(
|
||||
/// Creates a new periodic vesting account, and deposits funds to vest into the contract.
|
||||
///
|
||||
/// Callable by ADMIN only, see [instantiate].
|
||||
pub(crate) fn try_create_periodic_vesting_account(
|
||||
pub fn try_create_periodic_vesting_account(
|
||||
owner_address: &str,
|
||||
staking_address: Option<String>,
|
||||
vesting_spec: Option<VestingSpecification>,
|
||||
@@ -743,13 +743,9 @@ pub fn get_contract_version() -> ContractBuildInformation {
|
||||
ContractBuildInformation {
|
||||
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
||||
commit_sha: option_env!("VERGEN_GIT_SHA").unwrap_or("NONE").to_string(),
|
||||
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
commit_branch: option_env!("VERGEN_GIT_BRANCH")
|
||||
.unwrap_or("NONE")
|
||||
.to_string(),
|
||||
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
|
||||
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
|
||||
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
|
||||
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
pub mod contract;
|
||||
mod errors;
|
||||
pub mod errors;
|
||||
mod queued_migrations;
|
||||
mod storage;
|
||||
mod support;
|
||||
mod traits;
|
||||
pub mod vesting;
|
||||
|
||||
#[cfg(feature = "testing-mocks")]
|
||||
pub use support::mock_helpers::VestingContract;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract;
|
||||
use crate::errors::ContractError;
|
||||
use cosmwasm_contract_testing::TestableContract;
|
||||
use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, QueryResponse, Response};
|
||||
use vesting_contract_common::{ExecuteMsg, InitMsg, QueryMsg};
|
||||
|
||||
pub struct VestingContract;
|
||||
|
||||
impl TestableContract for VestingContract {
|
||||
type ContractError = ContractError;
|
||||
type InstantiateMsg = InitMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
|
||||
fn new() -> Self {
|
||||
VestingContract
|
||||
}
|
||||
|
||||
fn instantiate(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: Self::InstantiateMsg,
|
||||
) -> Result<Response, Self::ContractError> {
|
||||
contract::instantiate(deps, env, info, msg)
|
||||
}
|
||||
|
||||
fn execute(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: Self::ExecuteMsg,
|
||||
) -> Result<Response, Self::ContractError> {
|
||||
contract::execute(deps, env, info, msg)
|
||||
}
|
||||
|
||||
fn query(
|
||||
deps: Deps<'_>,
|
||||
env: Env,
|
||||
msg: Self::QueryMsg,
|
||||
) -> Result<QueryResponse, Self::ContractError> {
|
||||
contract::query(deps, env, msg)
|
||||
}
|
||||
}
|
||||
@@ -1 +1,7 @@
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod tests;
|
||||
|
||||
#[cfg(feature = "testing-mocks")]
|
||||
pub mod mock_helpers;
|
||||
|
||||
@@ -1,104 +1,24 @@
|
||||
#[cfg(test)]
|
||||
pub mod helpers {
|
||||
|
||||
// TODO: once https://github.com/nymtech/nym/pull/3040 gets merged,
|
||||
// the `ContractState` should replace the below
|
||||
#[allow(unused)]
|
||||
mod state_dump_decoder {
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct RawState {
|
||||
pub height: String,
|
||||
pub result: Vec<RawKV>,
|
||||
}
|
||||
|
||||
impl RawState {
|
||||
pub fn decode(self) -> DecodedState {
|
||||
DecodedState {
|
||||
height: self.height.parse().unwrap(),
|
||||
result: self
|
||||
.result
|
||||
.into_iter()
|
||||
.map(|raw| DecodedKV {
|
||||
key: hex::decode(&raw.key).unwrap(),
|
||||
value: general_purpose::STANDARD.decode(&raw.value).unwrap(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
|
||||
let file = File::open(path).expect("failed to open specified file");
|
||||
serde_json::from_reader(file).expect("failed to parse specified file")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct RawKV {
|
||||
// hex
|
||||
pub key: String,
|
||||
|
||||
// base64
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct DecodedKV {
|
||||
pub key: Vec<u8>,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct DecodedState {
|
||||
pub height: u64,
|
||||
pub result: Vec<DecodedKV>,
|
||||
}
|
||||
|
||||
impl DecodedState {
|
||||
pub fn find_value(&self, key: &[u8]) -> Option<Vec<u8>> {
|
||||
self.result
|
||||
.iter()
|
||||
.find(|kv| kv.key == key)
|
||||
.map(|kv| kv.value.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::contract::{instantiate, try_create_periodic_vesting_account};
|
||||
use crate::storage::{ACCOUNTS, ADMIN, MIXNET_CONTRACT_ADDRESS, MIX_DENOM};
|
||||
use crate::support::tests::helpers::state_dump_decoder::RawState;
|
||||
use crate::traits::VestingAccount;
|
||||
use crate::vesting::{populate_vesting_periods, Account};
|
||||
use contracts_common::Percent;
|
||||
use cosmwasm_contract_testing::{env_with_block_info, ContractState};
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
|
||||
use cosmwasm_std::{
|
||||
coin, Addr, BlockInfo, Coin, ContractInfo, Deps, DepsMut, Empty, Env, MemoryStorage,
|
||||
MessageInfo, OwnedDeps, Storage, Timestamp, Uint128,
|
||||
coin, Addr, BlockInfo, Coin, Deps, DepsMut, Empty, Env, MemoryStorage, MessageInfo,
|
||||
OwnedDeps, Storage, Timestamp, Uint128,
|
||||
};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use vesting_contract_common::messages::{InitMsg, VestingSpecification};
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
// use rng with constant seed for all tests so that they would be deterministic
|
||||
#[allow(unused)]
|
||||
pub fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub struct TestSetup {
|
||||
pub deps: OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>>,
|
||||
pub env: Env,
|
||||
pub rng: ChaCha20Rng,
|
||||
|
||||
pub state: ContractState,
|
||||
pub admin: MessageInfo,
|
||||
}
|
||||
|
||||
@@ -109,40 +29,28 @@ pub mod helpers {
|
||||
let admin = ADMIN.load(deps.as_ref().storage).unwrap();
|
||||
|
||||
TestSetup {
|
||||
deps,
|
||||
env: mock_env(),
|
||||
rng: test_rng(),
|
||||
state: ContractState::new(),
|
||||
admin: mock_info(admin.as_str(), &[]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_state_dump<P: AsRef<Path>>(dump_file: P) -> Self {
|
||||
let state = RawState::from_file(dump_file).decode();
|
||||
|
||||
let mut deps = mock_dependencies();
|
||||
for kv in state.result {
|
||||
deps.storage.set(&kv.key, &kv.value)
|
||||
}
|
||||
|
||||
let admin = ADMIN.load(deps.as_ref().storage).unwrap();
|
||||
let env = Env {
|
||||
block: BlockInfo {
|
||||
height: 5633424,
|
||||
time: Timestamp::from_seconds(1676025955),
|
||||
chain_id: "nyx".to_string(),
|
||||
},
|
||||
transaction: None,
|
||||
contract: ContractInfo {
|
||||
address: Addr::unchecked(
|
||||
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw",
|
||||
),
|
||||
},
|
||||
let current_block = BlockInfo {
|
||||
height: 5633424,
|
||||
time: Timestamp::from_seconds(1676025955),
|
||||
chain_id: "nyx".to_string(),
|
||||
};
|
||||
let custom_env = env_with_block_info(current_block);
|
||||
let state = ContractState::try_from_state_dump(dump_file, Some(custom_env.clone()))
|
||||
.unwrap()
|
||||
.with_contract_address(
|
||||
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw",
|
||||
);
|
||||
|
||||
let admin = ADMIN.load(state.deps().storage).unwrap();
|
||||
|
||||
TestSetup {
|
||||
deps,
|
||||
env,
|
||||
rng: test_rng(),
|
||||
state,
|
||||
admin: mock_info(admin.as_str(), &[]),
|
||||
}
|
||||
}
|
||||
@@ -173,15 +81,15 @@ pub mod helpers {
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> Deps<'_> {
|
||||
self.deps.as_ref()
|
||||
self.state.deps()
|
||||
}
|
||||
|
||||
pub fn deps_mut(&mut self) -> DepsMut<'_> {
|
||||
self.deps.as_mut()
|
||||
self.state.deps_mut()
|
||||
}
|
||||
|
||||
pub fn env(&self) -> Env {
|
||||
self.env.clone()
|
||||
self.state.env_cloned()
|
||||
}
|
||||
|
||||
pub fn admin(&self) -> MessageInfo {
|
||||
@@ -223,18 +131,18 @@ pub mod helpers {
|
||||
|
||||
let pretty = format!(
|
||||
r#"
|
||||
{:<20}{original}
|
||||
{:<20}{vesting}
|
||||
{:<20}{vested}
|
||||
{:<20}{balance}
|
||||
{:<20}{withdrawn}
|
||||
{:<20}{historical_rewards}
|
||||
{:<20}{locked}
|
||||
{:<20}{spendable}
|
||||
{:<20}{spendable_vested}
|
||||
{:<20}{spendable_reward}
|
||||
{:<20}{total_delegated}
|
||||
"#,
|
||||
{:<20}{original}
|
||||
{:<20}{vesting}
|
||||
{:<20}{vested}
|
||||
{:<20}{balance}
|
||||
{:<20}{withdrawn}
|
||||
{:<20}{historical_rewards}
|
||||
{:<20}{locked}
|
||||
{:<20}{spendable}
|
||||
{:<20}{spendable_vested}
|
||||
{:<20}{spendable_reward}
|
||||
{:<20}{total_delegated}
|
||||
"#,
|
||||
"original",
|
||||
"vesting",
|
||||
"vested:",
|
||||
@@ -265,37 +173,37 @@ pub mod helpers {
|
||||
|
||||
pub fn vesting_coins(&self, account: &Account) -> Coin {
|
||||
account
|
||||
.get_vesting_coins(None, &self.env, self.deps().storage)
|
||||
.get_vesting_coins(None, self.state.env(), self.deps().storage)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn vested_coins(&self, account: &Account) -> Coin {
|
||||
account
|
||||
.get_vested_coins(None, &self.env, self.deps().storage)
|
||||
.get_vested_coins(None, self.state.env(), self.deps().storage)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn locked_coins(&self, account: &Account) -> Coin {
|
||||
account
|
||||
.locked_coins(None, &self.env, self.deps().storage)
|
||||
.locked_coins(None, self.state.env(), self.deps().storage)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn spendable_coins(&self, account: &Account) -> Coin {
|
||||
account
|
||||
.spendable_coins(None, &self.env, self.deps().storage)
|
||||
.spendable_coins(None, self.state.env(), self.deps().storage)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn spendable_vested_coins(&self, account: &Account) -> Coin {
|
||||
account
|
||||
.spendable_vested_coins(None, &self.env, self.deps().storage)
|
||||
.spendable_vested_coins(None, self.state.env(), self.deps().storage)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn spendable_reward_coins(&self, account: &Account) -> Coin {
|
||||
account
|
||||
.spendable_reward_coins(None, &self.env, self.deps().storage)
|
||||
.spendable_reward_coins(None, self.state.env(), self.deps().storage)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "explorer-api"
|
||||
version = "1.1.12"
|
||||
version = "1.1.10"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::mix_node::http::mix_node_make_default_routes;
|
||||
use crate::mix_nodes::http::mix_nodes_make_default_routes;
|
||||
use crate::overview::http::overview_make_default_routes;
|
||||
use crate::ping::http::ping_make_default_routes;
|
||||
use crate::service_providers::http::service_providers_make_default_routes;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use crate::validators::http::validators_make_default_routes;
|
||||
|
||||
@@ -57,7 +56,6 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
|
||||
"/overview" => overview_make_default_routes(&openapi_settings),
|
||||
"/ping" => ping_make_default_routes(&openapi_settings),
|
||||
"/validators" => validators_make_default_routes(&openapi_settings),
|
||||
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
|
||||
};
|
||||
|
||||
building_rocket
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user