Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1266f39446 | |||
| eb59615c56 | |||
| 07c908c497 | |||
| 6de0c4ce92 | |||
| 05d8b31e51 | |||
| 692fbf1392 | |||
| 0de4aea77b | |||
| a7cd8efc04 | |||
| 56aad75220 | |||
| e2f2ab89ec | |||
| 4d09b6f2e5 | |||
| b9339b8f0c | |||
| 43a7360399 | |||
| 5f9f7f0fac | |||
| df0e2fe489 | |||
| bc33cc4c8d | |||
| a31597aca9 | |||
| 378229b04e | |||
| fec196c097 | |||
| 1d7ffc1bb6 | |||
| 0caa627960 | |||
| 3ac58e0c49 | |||
| e52bd918fb | |||
| 9d82d6d111 | |||
| 3593631e4a | |||
| f5846d5bc2 | |||
| d7779df1b7 | |||
| 7fcc188041 | |||
| b8c8d33c94 | |||
| 02909c03dd | |||
| 11262836d2 | |||
| f26fd5384d | |||
| 085103b333 | |||
| 574f7f1abd | |||
| 31e161604a | |||
| e4e349bea8 | |||
| 6391b7ed3a | |||
| c225511f95 | |||
| 4eedbb235a | |||
| 548b8717b2 | |||
| a215b3d0bf | |||
| 03d5a133eb |
@@ -57,6 +57,7 @@ jobs:
|
||||
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_performance_contract.wasm $OUTPUT_DIR
|
||||
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
name: ci-nym-wallet-storybook
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- '.github/workflows/ci-nym-wallet-storybook.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Build dependencies
|
||||
run: yarn && yarn build
|
||||
|
||||
- name: Build storybook
|
||||
run: yarn storybook:build
|
||||
working-directory: ./nym-wallet
|
||||
|
||||
- name: Deploy branch to CI www (storybook)
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "nym-wallet/storybook-static/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nym-wallet
|
||||
NYM_PROJECT_NAME: "nym-wallet"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
@@ -19,7 +19,11 @@ jobs:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: arc-ubuntu-22.04
|
||||
matrix:
|
||||
include:
|
||||
- os: arc-ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
>
|
||||
> ➡️➡️➡️➡️➡️ **View output:**
|
||||
>
|
||||
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}
|
||||
>
|
||||
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
|
||||
>
|
||||
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
|
||||
|
||||
Generated
+177
-9
@@ -1923,6 +1923,26 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw-multi-test"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533b31c94b9e10e77e2468a2b1559aa506505d18c4e52eb64cbfc624ca876ad2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bech32",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"itertools 0.14.0",
|
||||
"prost 0.13.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw-storage-plus"
|
||||
version = "2.0.0"
|
||||
@@ -5306,6 +5326,7 @@ dependencies = [
|
||||
"nym-sphinx 0.1.0",
|
||||
"nym-task 0.1.0",
|
||||
"sqlx",
|
||||
"sqlx-pool-guard",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
@@ -5450,6 +5471,7 @@ dependencies = [
|
||||
name = "nym-contracts-common"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5477,6 +5499,19 @@ dependencies = [
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-cpp-ffi"
|
||||
version = "0.1.2"
|
||||
@@ -5573,6 +5608,7 @@ dependencies = [
|
||||
"nym-ecash-time 0.1.0",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"sqlx-pool-guard",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"zeroize",
|
||||
@@ -6286,7 +6322,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver 1.0.26",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde_repr",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -6649,7 +6684,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -6891,6 +6926,19 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"nym-contracts-common 0.5.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract-common"
|
||||
version = "0.1.0"
|
||||
@@ -7437,7 +7485,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-statistics-api"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -7710,6 +7758,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common 0.6.0",
|
||||
"nym-multisig-contract-common 0.1.0",
|
||||
"nym-network-defaults 0.1.0",
|
||||
"nym-performance-contract-common",
|
||||
"nym-serde-helpers 0.1.0",
|
||||
"nym-vesting-contract-common 0.7.0",
|
||||
"prost 0.13.5",
|
||||
@@ -8691,6 +8740,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc_pidinfo"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af53dad2390f8df98dda1e4188322bdf2f91c86cf6001f51d10d64451edf463a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus"
|
||||
version = "0.14.0"
|
||||
@@ -10278,6 +10336,19 @@ dependencies = [
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-pool-guard"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc_pidinfo",
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.6"
|
||||
@@ -10733,6 +10804,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common 0.6.0",
|
||||
"nym-multisig-contract-common 0.1.0",
|
||||
"nym-pemstore 0.3.0",
|
||||
"nym-performance-contract-common",
|
||||
"nym-validator-client 0.1.0",
|
||||
"nym-vesting-contract-common 0.7.0",
|
||||
"rand 0.8.5",
|
||||
@@ -12292,6 +12364,28 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -12326,6 +12420,30 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
@@ -12348,6 +12466,17 @@ dependencies = [
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
@@ -12371,10 +12500,31 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
@@ -12382,7 +12532,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result 0.3.1",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
@@ -12407,9 +12557,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.1"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -12433,6 +12583,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@@ -12531,6 +12690,15 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
|
||||
+5
-5
@@ -34,11 +34,12 @@ members = [
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
"common/cosmwasm-smart-contracts/contracts-common-testing",
|
||||
"common/cosmwasm-smart-contracts/easy_addr",
|
||||
"common/cosmwasm-smart-contracts/ecash-contract",
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/credential-storage",
|
||||
@@ -126,6 +127,7 @@ members = [
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"sqlx-pool-guard",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
@@ -135,7 +137,6 @@ members = [
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/validator-status-check",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-id-cli",
|
||||
@@ -287,6 +288,7 @@ petgraph = "0.6.5"
|
||||
pin-project = "1.1"
|
||||
pin-project-lite = "0.2.16"
|
||||
publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
@@ -368,9 +370,6 @@ subtle = "2.5.0"
|
||||
# cosmwasm-related
|
||||
cosmwasm-schema = "=2.2.2"
|
||||
cosmwasm-std = "=2.2.2"
|
||||
# use 1.0.1 as that's the version used by cosmwasm-std 2.2.1
|
||||
# (and ideally we don't want to pull the same dependency twice)
|
||||
serde-json-wasm = "=1.0.1"
|
||||
# same version as used by cosmwasm
|
||||
cw-utils = "=2.0.0"
|
||||
cw-storage-plus = "=2.0.0"
|
||||
@@ -378,6 +377,7 @@ cw2 = { version = "=2.0.0" }
|
||||
cw3 = { version = "=2.0.0" }
|
||||
cw4 = { version = "=2.0.0" }
|
||||
cw-controllers = { version = "=2.0.0" }
|
||||
cw-multi-test = "=2.3.2"
|
||||
|
||||
# cosmrs-related
|
||||
bip32 = { version = "0.5.3", default-features = false }
|
||||
|
||||
@@ -133,7 +133,7 @@ clippy: sdk-wasm-lint
|
||||
# Build contracts ready for deploy
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract
|
||||
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract nym_performance_contract
|
||||
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
|
||||
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::BadGateway;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -19,7 +18,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -32,7 +30,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::replies::reply_storage::{
|
||||
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
|
||||
use crate::{
|
||||
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
|
||||
config,
|
||||
config::Config,
|
||||
error::ClientCoreError,
|
||||
};
|
||||
use crate::config;
|
||||
use crate::config::Config;
|
||||
use crate::error::ClientCoreError;
|
||||
use log::{error, info, trace};
|
||||
use nym_bandwidth_controller::BandwidthController;
|
||||
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
|
||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||
use nym_validator_client::nyxd;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use std::path::Path;
|
||||
use std::{fs, io};
|
||||
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
|
||||
use std::{io, path::Path};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
@@ -22,11 +20,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
db_path: P,
|
||||
surb_config: &config::ReplySurbs,
|
||||
) -> Result<fs_backend::Backend, ClientCoreError> {
|
||||
info!("creating fresh surb database");
|
||||
info!("Creating fresh surb database");
|
||||
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
|
||||
Ok(backend) => backend,
|
||||
Err(err) => {
|
||||
error!("failed to setup persistent storage backend for our reply needs: {err}");
|
||||
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
|
||||
return Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
});
|
||||
@@ -40,14 +38,15 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
surb_config.minimum_reply_surb_storage_threshold,
|
||||
surb_config.maximum_reply_surb_storage_threshold,
|
||||
);
|
||||
storage_backend
|
||||
.init_fresh(&mem_store)
|
||||
.await
|
||||
.map_err(|err| ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})?;
|
||||
|
||||
Ok(storage_backend)
|
||||
match storage_backend.init_fresh(&mem_store).await {
|
||||
Ok(()) => Ok(storage_backend),
|
||||
Err(err) => {
|
||||
storage_backend.shutdown().await;
|
||||
Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
|
||||
@@ -58,12 +57,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
// )
|
||||
// }
|
||||
|
||||
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
let db_path = db_path.as_ref();
|
||||
debug_assert!(db_path.exists());
|
||||
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
let suffix = format!("_{now}.corrupted");
|
||||
|
||||
let new_extension =
|
||||
@@ -72,11 +70,15 @@ fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
} else {
|
||||
suffix
|
||||
};
|
||||
let renamed = db_path.with_extension(new_extension);
|
||||
|
||||
let mut renamed = db_path.to_owned();
|
||||
renamed.set_extension(new_extension);
|
||||
|
||||
fs::rename(db_path, renamed)
|
||||
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
|
||||
error!(
|
||||
"Failed to rename corrupt database file: {} to {}",
|
||||
db_path.display(),
|
||||
renamed.display()
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
@@ -87,13 +89,12 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
// the existing one
|
||||
let db_path = db_path.as_ref();
|
||||
if db_path.exists() {
|
||||
info!("loading existing surb database");
|
||||
info!("Loading existing surb database");
|
||||
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
|
||||
Ok(backend) => Ok(backend),
|
||||
Err(err) => {
|
||||
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
|
||||
|
||||
archive_corrupted_database(db_path)?;
|
||||
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
|
||||
archive_corrupted_database(db_path).await?;
|
||||
setup_fresh_backend(db_path, surb_config).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,26 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
nym-task = { path = "../../task" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
workspace = true
|
||||
features = ["fs"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
||||
optional = true
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
path = "../../../sqlx-pool-guard"
|
||||
|
||||
[build-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
|
||||
[features]
|
||||
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -30,7 +29,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -43,7 +41,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -15,9 +15,11 @@ use sqlx::{
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageManager {
|
||||
pub connection_pool: sqlx::SqlitePool,
|
||||
connection_pool: SqlitePoolGuard,
|
||||
}
|
||||
|
||||
// all SQL goes here
|
||||
@@ -37,7 +39,7 @@ impl StorageManager {
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(database_path)
|
||||
.filename(&database_path)
|
||||
.create_if_missing(fresh)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -49,11 +51,15 @@ impl StorageManager {
|
||||
}
|
||||
};
|
||||
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
|
||||
.run(&connection_pool)
|
||||
.run(&*connection_pool)
|
||||
.await
|
||||
{
|
||||
error!("Failed to initialize SQLx database: {err}");
|
||||
connection_pool.close().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
@@ -61,38 +67,43 @@ impl StorageManager {
|
||||
Ok(StorageManager { connection_pool })
|
||||
}
|
||||
|
||||
/// Close connection pool waiting for all connections to be closed.
|
||||
pub async fn close_pool(&self) {
|
||||
self.connection_pool.close().await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.is_some())
|
||||
}
|
||||
|
||||
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT flush_in_progress FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.flush_in_progress > 0)
|
||||
}
|
||||
|
||||
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
|
||||
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.previous_flush_timestamp)
|
||||
}
|
||||
@@ -100,14 +111,14 @@ impl StorageManager {
|
||||
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
|
||||
let in_progress_int = i64::from(in_progress);
|
||||
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT client_in_use FROM status;")
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.client_in_use > 0)
|
||||
}
|
||||
@@ -115,21 +126,21 @@ impl StorageManager {
|
||||
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
|
||||
let in_use_int = i64::from(in_use);
|
||||
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM sender_tag;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -141,21 +152,21 @@ impl StorageManager {
|
||||
stored_tag.recipient,
|
||||
stored_tag.tag
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM reply_key;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -171,14 +182,14 @@ impl StorageManager {
|
||||
stored_reply_key.reply_key,
|
||||
stored_reply_key.sent_at_timestamp
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -193,7 +204,7 @@ impl StorageManager {
|
||||
stored_surb_sender.tag,
|
||||
stored_surb_sender.last_sent_timestamp
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
.last_insert_rowid();
|
||||
Ok(id)
|
||||
@@ -211,17 +222,17 @@ impl StorageManager {
|
||||
"#,
|
||||
sender_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM reply_surb;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM reply_surb_sender;")
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -239,7 +250,7 @@ impl StorageManager {
|
||||
stored_reply_surb.reply_surb,
|
||||
stored_reply_surb.encoded_key_rotation
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -253,7 +264,7 @@ impl StorageManager {
|
||||
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -267,7 +278,7 @@ impl StorageManager {
|
||||
"#,
|
||||
metadata.min_reply_surb_threshold,
|
||||
metadata.max_reply_surb_threshold,
|
||||
).execute(&self.connection_pool).await?;
|
||||
).execute(&*self.connection_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::backend::fs_backend::manager::StorageManager;
|
||||
use crate::backend::fs_backend::models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
|
||||
};
|
||||
use crate::surb_storage::ReceivedReplySurbs;
|
||||
use crate::{
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
|
||||
backend::fs_backend::{
|
||||
manager::StorageManager,
|
||||
models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
|
||||
StoredSurbSender,
|
||||
},
|
||||
},
|
||||
surb_storage::ReceivedReplySurbs,
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
|
||||
UsedSenderTags,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, info, warn};
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -41,15 +44,17 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, true).await?;
|
||||
manager.create_status_table().await?;
|
||||
|
||||
let backend = Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
};
|
||||
|
||||
Ok(backend)
|
||||
match manager.create_status_table().await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
}),
|
||||
Err(err) => {
|
||||
manager.close_pool().await;
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_load<P: AsRef<Path>>(
|
||||
@@ -64,7 +69,28 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, false).await?;
|
||||
match Self::try_load_inner(&manager, fresh_sender_tags).await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
}),
|
||||
Err(e) => {
|
||||
manager.close_pool().await;
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gracefully close sqlite connection pool and drop backend.
|
||||
pub async fn shutdown(self) {
|
||||
self.manager.close_pool().await
|
||||
}
|
||||
|
||||
async fn try_load_inner(
|
||||
manager: &StorageManager,
|
||||
fresh_sender_tags: bool,
|
||||
) -> Result<(), StorageError> {
|
||||
// the database flush wasn't fully finished and thus the data is in inconsistent state
|
||||
// (we don't really know what's properly saved or what's not)
|
||||
if manager.get_flush_status().await? {
|
||||
@@ -126,20 +152,11 @@ impl Backend {
|
||||
manager.delete_all_tags().await?;
|
||||
}
|
||||
|
||||
Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
// manager: StorageManagerState::Storage(manager),
|
||||
manager,
|
||||
})
|
||||
}
|
||||
|
||||
async fn close_pool(&mut self) {
|
||||
self.manager.connection_pool.close().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rotate(&mut self) -> Result<(), StorageError> {
|
||||
self.close_pool().await;
|
||||
self.manager.close_pool().await;
|
||||
|
||||
let new_extension = if let Some(existing_extension) =
|
||||
self.database_path.extension().and_then(|ext| ext.to_str())
|
||||
@@ -152,7 +169,8 @@ impl Backend {
|
||||
let mut temp_old = self.database_path.clone();
|
||||
temp_old.set_extension(new_extension);
|
||||
|
||||
fs::rename(&self.database_path, &temp_old)
|
||||
tokio::fs::rename(&self.database_path, &temp_old)
|
||||
.await
|
||||
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
|
||||
self.manager = StorageManager::init(&self.database_path, true).await?;
|
||||
self.manager.create_status_table().await?;
|
||||
@@ -161,9 +179,10 @@ impl Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
async fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
if let Some(old_path) = self.temporary_old_path.take() {
|
||||
fs::remove_file(old_path)
|
||||
tokio::fs::remove_file(old_path)
|
||||
.await
|
||||
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
|
||||
} else {
|
||||
warn!("the old database file doesn't seem to exist!");
|
||||
@@ -335,7 +354,7 @@ impl ReplyStorageBackend for Backend {
|
||||
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
|
||||
self.dump_reply_surbs(surbs_ref).await?;
|
||||
|
||||
self.remove_old()?;
|
||||
self.remove_old().await?;
|
||||
self.end_storage_flush().await
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ where
|
||||
self.backend.load_surb_storage().await
|
||||
}
|
||||
|
||||
// this will have to get enabled after merging develop
|
||||
pub async fn flush_on_shutdown(
|
||||
mut self,
|
||||
mem_state: CombinedReplyStorage,
|
||||
@@ -50,7 +49,6 @@ where
|
||||
shutdown.recv().await;
|
||||
|
||||
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
|
||||
info!("you MUST NOT forcefully shutdown now or you risk data corruption!");
|
||||
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
|
||||
error!("failed to flush our reply-related data to the persistent storage: {err}")
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,7 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c
|
||||
nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
|
||||
nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
// signing clients
|
||||
@@ -21,6 +22,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
// re-export query traits
|
||||
@@ -29,6 +31,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
// re-export signing traits
|
||||
@@ -37,6 +40,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
// helper for providing blanket implementation for query clients
|
||||
@@ -44,6 +48,7 @@ pub trait NymContractsProvider {
|
||||
// main
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -56,6 +61,7 @@ pub trait NymContractsProvider {
|
||||
pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_contract_address: Option<AccountId>,
|
||||
|
||||
pub ecash_contract_address: Option<AccountId>,
|
||||
pub group_contract_address: Option<AccountId>,
|
||||
@@ -76,6 +82,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.vesting_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
performance_contract_address: value
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
ecash_contract_address: value
|
||||
.ecash_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
pub use nym_performance_contract_common::{
|
||||
msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse,
|
||||
};
|
||||
use nym_performance_contract_common::{
|
||||
EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse,
|
||||
FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation,
|
||||
NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, NodeMeasurementsResponse,
|
||||
NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitor,
|
||||
RetiredNetworkMonitorsPagedResponse,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PerformanceQueryClient {
|
||||
async fn query_performance_contract<T>(
|
||||
&self,
|
||||
query: PerformanceQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::Admin {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_performance(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodePerformanceResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NodePerformance { epoch_id, node_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_performance_paged(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
start_after: Option<EpochId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<NodePerformancePagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NodePerformancePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_measurements(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodeMeasurementsResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_epoch_measurements_paged(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<EpochMeasurementsPagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::EpochMeasurementsPaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_epoch_performance_paged(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<EpochPerformancePagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::EpochPerformancePaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_full_historical_performance_paged(
|
||||
&self,
|
||||
start_after: Option<(EpochId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FullHistoricalPerformancePagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::FullHistoricalPerformancePaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor(
|
||||
&self,
|
||||
address: &AccountId,
|
||||
) -> Result<NetworkMonitorResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NetworkMonitor {
|
||||
address: address.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitors_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<NetworkMonitorsPagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::NetworkMonitorsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_retired_network_monitors_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<RetiredNetworkMonitorsPagedResponse, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::RetiredNetworkMonitorsPaged {
|
||||
start_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
// (it didn't feel appropriate to combine it with the existing trait
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedPerformanceQueryClient: PerformanceQueryClient {
|
||||
async fn get_all_node_performance(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<EpochNodePerformance>, NyxdError> {
|
||||
collect_paged!(self, get_node_performance_paged, performance, node_id)
|
||||
}
|
||||
|
||||
async fn get_all_epoch_measurements(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<Vec<NodeMeasurement>, NyxdError> {
|
||||
collect_paged!(self, get_epoch_measurements_paged, measurements, node_id)
|
||||
}
|
||||
|
||||
async fn get_all_epoch_performance(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Vec<NodePerformance>, NyxdError> {
|
||||
collect_paged!(self, get_epoch_performance_paged, performance, epoch_id)
|
||||
}
|
||||
|
||||
async fn get_all_full_historical_performance(
|
||||
&self,
|
||||
) -> Result<Vec<HistoricalPerformance>, NyxdError> {
|
||||
collect_paged!(self, get_full_historical_performance_paged, performance)
|
||||
}
|
||||
|
||||
async fn get_all_network_monitors(&self) -> Result<Vec<NetworkMonitorInformation>, NyxdError> {
|
||||
collect_paged!(self, get_network_monitors_paged, info)
|
||||
}
|
||||
|
||||
async fn get_all_retired_network_monitors(
|
||||
&self,
|
||||
) -> Result<Vec<RetiredNetworkMonitor>, NyxdError> {
|
||||
collect_paged!(self, get_retired_network_monitors_paged, info)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedPerformanceQueryClient for T where T: PerformanceQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> PerformanceQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_performance_contract<T>(
|
||||
&self,
|
||||
query: PerformanceQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let performance_contract_address = &self
|
||||
.performance_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?;
|
||||
self.query_contract_smart(performance_contract_address, &query)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: PerformanceQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: PerformanceQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
PerformanceQueryMsg::Admin {} => client.admin().ignore(),
|
||||
PerformanceQueryMsg::NodePerformance { epoch_id, node_id } => {
|
||||
client.get_node_performance(epoch_id, node_id).ignore()
|
||||
}
|
||||
PerformanceQueryMsg::NodePerformancePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_node_performance_paged(node_id, start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id } => {
|
||||
client.get_node_measurements(epoch_id, node_id).ignore()
|
||||
}
|
||||
PerformanceQueryMsg::EpochMeasurementsPaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_epoch_measurements_paged(epoch_id, start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::EpochPerformancePaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => client
|
||||
.get_epoch_performance_paged(epoch_id, start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::FullHistoricalPerformancePaged { start_after, limit } => client
|
||||
.get_full_historical_performance_paged(start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::NetworkMonitor { address } => client
|
||||
.get_network_monitor(&address.parse().unwrap())
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::NetworkMonitorsPaged { start_after, limit } => client
|
||||
.get_network_monitors_paged(start_after, limit)
|
||||
.ignore(),
|
||||
PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client
|
||||
.get_retired_network_monitors_paged(start_after, limit)
|
||||
.ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::coin::Coin;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::cosmwasm_client::ContractResponseData;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_performance_contract_common::{
|
||||
EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformance,
|
||||
RemoveEpochMeasurementsResponse,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PerformanceSigningClient {
|
||||
async fn execute_performance_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: PerformanceExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_admin(
|
||||
&self,
|
||||
admin: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::UpdateAdmin { admin },
|
||||
"PerformanceContract::UpdateAdmin".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_performance(
|
||||
&self,
|
||||
epoch: EpochId,
|
||||
data: NodePerformance,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::Submit { epoch, data },
|
||||
"PerformanceContract::Submit".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn batch_submit_performance(
|
||||
&self,
|
||||
epoch: EpochId,
|
||||
data: Vec<NodePerformance>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::BatchSubmit { epoch, data },
|
||||
"PerformanceContract::BatchSubmit".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::AuthoriseNetworkMonitor { address },
|
||||
"PerformanceContract::AuthoriseNetworkMonitor".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn retire_network_monitor(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::RetireNetworkMonitor { address },
|
||||
"PerformanceContract::RetireNetworkMonitor".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_node_measurements(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id },
|
||||
"PerformanceContract::RemoveNodeMeasurements".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn partial_remove_epoch_measurements(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_performance_contract(
|
||||
fee,
|
||||
PerformanceExecuteMsg::RemoveEpochMeasurements { epoch_id },
|
||||
"PerformanceContract::RemoveEpochMeasurements".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_epoch_measurements(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<(), NyxdError> {
|
||||
loop {
|
||||
let execute_res = self
|
||||
.partial_remove_epoch_measurements(epoch_id, fee.clone())
|
||||
.await?;
|
||||
let response = execute_res
|
||||
.parse_singleton_json_contract_response::<RemoveEpochMeasurementsResponse>()?;
|
||||
if !response.additional_entries_to_remove_remaining {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> PerformanceSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_performance_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: PerformanceExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let performance_contract_address = &self
|
||||
.performance_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()?[0];
|
||||
self.execute(
|
||||
signer_address,
|
||||
performance_contract_address,
|
||||
&msg,
|
||||
fee,
|
||||
memo,
|
||||
funds,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_performance_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: PerformanceSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: PerformanceExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
PerformanceExecuteMsg::UpdateAdmin { admin } => {
|
||||
client.update_admin(admin, None).ignore()
|
||||
}
|
||||
PerformanceExecuteMsg::Submit { epoch, data } => {
|
||||
client.submit_performance(epoch, data, None).ignore()
|
||||
}
|
||||
PerformanceExecuteMsg::BatchSubmit { epoch, data } => {
|
||||
client.batch_submit_performance(epoch, data, None).ignore()
|
||||
}
|
||||
PerformanceExecuteMsg::AuthoriseNetworkMonitor { address } => {
|
||||
client.authorise_network_monitor(address, None).ignore()
|
||||
}
|
||||
PerformanceExecuteMsg::RetireNetworkMonitor { address } => {
|
||||
client.retire_network_monitor(address, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => client
|
||||
.remove_node_measurements(epoch_id, node_id, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::RemoveEpochMeasurements { epoch_id } => client
|
||||
.partial_remove_epoch_measurements(epoch_id, None)
|
||||
.ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ use tendermint_rpc::endpoint::broadcast;
|
||||
use tracing::error;
|
||||
|
||||
pub use cosmrs::abci::MsgResponse;
|
||||
use cosmwasm_std::from_json;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub fn parse_singleton_u32_from_contract_response(b: Vec<u8>) -> Result<u32, NyxdError> {
|
||||
if b.len() != 4 {
|
||||
@@ -73,6 +75,11 @@ pub fn parse_msg_responses(data: Bytes) -> Vec<MsgResponse> {
|
||||
|
||||
// requires there's a single response message
|
||||
pub trait ContractResponseData: Sized {
|
||||
fn parse_singleton_json_contract_response<T: DeserializeOwned>(&self) -> Result<T, NyxdError> {
|
||||
let b = self.to_singleton_contract_data()?;
|
||||
from_json(&b).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn parse_singleton_u32_contract_data(&self) -> Result<u32, NyxdError> {
|
||||
let b = self.to_singleton_contract_data()?;
|
||||
parse_singleton_u32_from_contract_response(b)
|
||||
|
||||
@@ -276,6 +276,10 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
self.config.contracts.vesting_contract_address.as_ref()
|
||||
}
|
||||
|
||||
fn performance_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.ecash_contract_address.as_ref()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
cw-multi-test = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage};
|
||||
use cosmwasm_std::{
|
||||
coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps,
|
||||
Response, StdResult, Storage,
|
||||
};
|
||||
use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey};
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const TEST_DENOM: &str = "unym";
|
||||
pub const TEST_PREFIX: &str = "n";
|
||||
|
||||
pub fn mock_api() -> MockApi {
|
||||
MockApi::default().with_prefix(TEST_PREFIX)
|
||||
}
|
||||
|
||||
pub fn mock_dependencies() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
OwnedDeps {
|
||||
storage: MockStorage::default(),
|
||||
api: mock_api(),
|
||||
querier: MockQuerier::default(),
|
||||
custom_query_type: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
|
||||
pub fn deps_with_balance(env: &Env) -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
let mut deps = mock_dependencies();
|
||||
deps.querier = MockQuerier::<Empty>::new(&[(
|
||||
env.contract.address.as_str(),
|
||||
coins(100000000000, TEST_DENOM).as_slice(),
|
||||
)]);
|
||||
deps
|
||||
}
|
||||
|
||||
pub fn generate_sorted_addresses(n: usize) -> Vec<Addr> {
|
||||
let mut rng = test_rng();
|
||||
let mut addrs = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64())));
|
||||
}
|
||||
addrs.sort();
|
||||
addrs
|
||||
}
|
||||
|
||||
pub fn addr<S: AsRef<str>>(raw: S) -> Addr {
|
||||
mock_api().addr_make(raw.as_ref())
|
||||
}
|
||||
|
||||
pub fn sender<S: AsRef<str>>(raw: S) -> MessageInfo {
|
||||
message_info(&addr(raw), &[])
|
||||
}
|
||||
|
||||
pub trait ExtractBankMsg {
|
||||
fn unwrap_bank_msg(self) -> Option<BankMsg>;
|
||||
}
|
||||
|
||||
impl ExtractBankMsg for Response {
|
||||
fn unwrap_bank_msg(self) -> Option<BankMsg> {
|
||||
for msg in self.messages {
|
||||
match msg.msg {
|
||||
CosmosMsg::Bank(bank_msg) => return Some(bank_msg),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FullReader<'a> {
|
||||
type Key;
|
||||
type Value: Serialize + DeserializeOwned;
|
||||
|
||||
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>>;
|
||||
|
||||
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>>;
|
||||
}
|
||||
|
||||
impl<'a, K, T> FullReader<'a> for Map<K, T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: PrimaryKey<'a> + KeyDeserialize,
|
||||
K::Output: 'static,
|
||||
{
|
||||
type Key = K::Output;
|
||||
type Value = T;
|
||||
|
||||
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> {
|
||||
self.range(store, None, None, Order::Ascending)
|
||||
.map(|record| record.map(|r| r.1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> {
|
||||
self.range(store, None, None, Order::Ascending).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, T, B> FullReader<'a> for Prefix<K, T, B>
|
||||
where
|
||||
K: KeyDeserialize + 'static,
|
||||
T: Serialize + DeserializeOwned,
|
||||
B: PrimaryKey<'a>,
|
||||
{
|
||||
type Key = K::Output;
|
||||
type Value = T;
|
||||
|
||||
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> {
|
||||
self.range(store, None, None, Order::Ascending)
|
||||
.map(|record| record.map(|r| r.1))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> {
|
||||
self.range(store, None, None, Order::Ascending).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// those are all used exclusively for testing thus unwraps, et al. are allowed
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::expect_used)]
|
||||
#![allow(clippy::panic)]
|
||||
|
||||
pub mod helpers;
|
||||
pub mod tester;
|
||||
|
||||
pub use helpers::*;
|
||||
pub use tester::*;
|
||||
@@ -0,0 +1,239 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{ContractTester, TestableNymContract};
|
||||
use cosmwasm_std::testing::{message_info, mock_env};
|
||||
use cosmwasm_std::{
|
||||
from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
|
||||
Storage, Timestamp,
|
||||
};
|
||||
use cw_multi_test::{next_block, AppResponse, Executor};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::any::type_name;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub trait ContractOpts {
|
||||
type ExecuteMsg;
|
||||
type QueryMsg;
|
||||
type ContractError;
|
||||
|
||||
fn deps(&self) -> Deps<'_>;
|
||||
|
||||
fn deps_mut(&mut self) -> DepsMut<'_>;
|
||||
|
||||
fn env(&self) -> Env;
|
||||
|
||||
fn addr_make(&self, input: &str) -> Addr;
|
||||
|
||||
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
|
||||
let env = self.env().clone();
|
||||
(self.deps_mut(), env)
|
||||
}
|
||||
|
||||
fn storage(&self) -> &dyn Storage;
|
||||
|
||||
fn storage_mut(&mut self) -> &mut dyn Storage;
|
||||
|
||||
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>;
|
||||
|
||||
fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>);
|
||||
|
||||
fn unchecked_read_from_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> T {
|
||||
let typ = type_name::<T>();
|
||||
self.read_from_contract_storage(key)
|
||||
.unwrap_or_else(|| panic!("value of type {typ} not present in the storage"))
|
||||
}
|
||||
|
||||
fn execute_raw(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
message: Self::ExecuteMsg,
|
||||
) -> Result<Response, Self::ContractError> {
|
||||
self.execute_raw_with_balance(sender, &[], message)
|
||||
}
|
||||
|
||||
fn execute_raw_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: Self::ExecuteMsg,
|
||||
) -> Result<Response, Self::ContractError>;
|
||||
}
|
||||
|
||||
impl<C> ContractOpts for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
type ExecuteMsg = C::ExecuteMsg;
|
||||
type QueryMsg = C::QueryMsg;
|
||||
type ContractError = C::ContractError;
|
||||
|
||||
fn deps(&self) -> Deps<'_> {
|
||||
Deps {
|
||||
storage: &self.storage,
|
||||
api: self.app.api(),
|
||||
querier: self.app.wrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deps_mut(&mut self) -> DepsMut<'_> {
|
||||
DepsMut {
|
||||
storage: &mut self.storage,
|
||||
api: self.app.api(),
|
||||
querier: self.app.wrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn env(&self) -> Env {
|
||||
Env {
|
||||
block: self.app.block_info(),
|
||||
contract: ContractInfo {
|
||||
address: self.contract_address.clone(),
|
||||
},
|
||||
..mock_env()
|
||||
}
|
||||
}
|
||||
|
||||
fn addr_make(&self, input: &str) -> Addr {
|
||||
self.app.api().addr_make(input)
|
||||
}
|
||||
|
||||
fn storage(&self) -> &dyn Storage {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
fn storage_mut(&mut self) -> &mut dyn Storage {
|
||||
&mut self.storage
|
||||
}
|
||||
|
||||
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> {
|
||||
let raw = self.deps().storage.get(key.as_ref())?;
|
||||
from_json(&raw).ok()
|
||||
}
|
||||
|
||||
fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
|
||||
self.deps_mut().storage.set(key.as_ref(), value.as_ref());
|
||||
}
|
||||
|
||||
fn execute_raw_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: C::ExecuteMsg,
|
||||
) -> Result<Response, C::ContractError> {
|
||||
let env = self.env();
|
||||
let info = message_info(&sender, coins);
|
||||
|
||||
C::execute()(self.deps_mut(), env, info, message)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ChainOpts: ContractOpts {
|
||||
fn set_contract_balance(&mut self, balance: Coin);
|
||||
|
||||
fn next_block(&mut self);
|
||||
|
||||
fn set_block_time(&mut self, time: Timestamp);
|
||||
|
||||
fn execute_msg(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
message: &Self::ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.execute_msg_with_balance(sender, &[], message)
|
||||
}
|
||||
|
||||
fn execute_msg_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: &Self::ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse>;
|
||||
|
||||
fn execute_arbitrary_contract<T: Serialize + Debug>(
|
||||
&mut self,
|
||||
contract: Addr,
|
||||
sender: MessageInfo,
|
||||
message: &T,
|
||||
) -> anyhow::Result<AppResponse>;
|
||||
|
||||
fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>(
|
||||
&self,
|
||||
contract: Addr,
|
||||
message: &Q,
|
||||
) -> StdResult<T>;
|
||||
|
||||
fn query<T: DeserializeOwned>(&self, message: &Self::QueryMsg) -> StdResult<T>;
|
||||
}
|
||||
|
||||
impl<C> ChainOpts for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn set_contract_balance(&mut self, balance: Coin) {
|
||||
let contract_address = &self.contract_address;
|
||||
self.app
|
||||
.router()
|
||||
.bank
|
||||
.init_balance(
|
||||
&mut self.storage.inner_storage(),
|
||||
contract_address,
|
||||
vec![balance],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
fn next_block(&mut self) {
|
||||
self.app.update_block(next_block)
|
||||
}
|
||||
|
||||
fn set_block_time(&mut self, time: Timestamp) {
|
||||
self.app.update_block(|b| b.time = time)
|
||||
}
|
||||
|
||||
fn execute_msg(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
message: &C::ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.execute_msg_with_balance(sender, &[], message)
|
||||
}
|
||||
|
||||
fn execute_msg_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: &C::ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.app
|
||||
.execute_contract(sender, self.contract_address.clone(), message, coins)
|
||||
}
|
||||
|
||||
fn execute_arbitrary_contract<T: Serialize + Debug>(
|
||||
&mut self,
|
||||
contract: Addr,
|
||||
sender: MessageInfo,
|
||||
message: &T,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
let coins = &sender.funds;
|
||||
let sender = sender.sender;
|
||||
self.app.execute_contract(sender, contract, message, coins)
|
||||
}
|
||||
|
||||
fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>(
|
||||
&self,
|
||||
contract: Addr,
|
||||
message: &Q,
|
||||
) -> StdResult<T> {
|
||||
self.app.wrap().query_wasm_smart(contract, message)
|
||||
}
|
||||
|
||||
fn query<T: DeserializeOwned>(&self, message: &C::QueryMsg) -> StdResult<T> {
|
||||
self.app
|
||||
.wrap()
|
||||
.query_wasm_smart(self.contract_address.as_str(), message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{
|
||||
CommonStorageKeys, ContractOpts, ContractTester, StorageWrapper, TestableNymContract,
|
||||
TEST_DENOM,
|
||||
};
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::{
|
||||
coin, coins, from_json, to_json_vec, Addr, Coin, MessageInfo, StdError, StdResult, Storage,
|
||||
};
|
||||
use cw_multi_test::Executor;
|
||||
use cw_storage_plus::{Key, Path, PrimaryKey};
|
||||
use rand::RngCore;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::any::type_name;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub trait StorageReader {
|
||||
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>;
|
||||
|
||||
fn read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> Option<T> {
|
||||
self.read_from_contract_storage(self.common_key(key)?)
|
||||
}
|
||||
|
||||
fn unchecked_read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> T {
|
||||
self.unchecked_read_from_contract_storage(
|
||||
self.common_key(key)
|
||||
.unwrap_or_else(|| panic!("no key set for {key:?}")),
|
||||
)
|
||||
}
|
||||
|
||||
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>;
|
||||
|
||||
fn unchecked_read_from_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> T {
|
||||
let typ = type_name::<T>();
|
||||
self.read_from_contract_storage(key)
|
||||
.unwrap_or_else(|| panic!("value of type {typ} not present in the storage"))
|
||||
}
|
||||
}
|
||||
|
||||
// technically it shouldn't rely on `StorageReader` and `common_key` should be extracted
|
||||
// but this makes it a tad easier and it's only testing code so it's fine
|
||||
pub trait StorageWriter: StorageReader {
|
||||
fn set_common_value<T: Serialize>(
|
||||
&mut self,
|
||||
key: CommonStorageKeys,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
let key = self
|
||||
.common_key(key)
|
||||
.ok_or(StdError::not_found("key not found"))?
|
||||
.to_vec();
|
||||
self.set_storage_value(key, value)
|
||||
}
|
||||
|
||||
fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>);
|
||||
|
||||
fn set_storage_value<T: Serialize>(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
self.set_storage(key, &to_json_vec(value)?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArbitraryContractStorageReader {
|
||||
fn may_read_from_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> Option<Vec<u8>>;
|
||||
|
||||
fn must_read_from_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<Vec<u8>> {
|
||||
let key = key.as_ref();
|
||||
self.may_read_from_contract_storage(address, key)
|
||||
.ok_or(StdError::not_found(format!("no data under {key:?}")))
|
||||
}
|
||||
|
||||
fn may_read_value_from_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<Option<T>> {
|
||||
let Some(bytes) = self.may_read_from_contract_storage(address, key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
from_json(&bytes).map(Some)
|
||||
}
|
||||
|
||||
fn must_read_value_from_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<T> {
|
||||
let bytes = self.must_read_from_contract_storage(address, key)?;
|
||||
from_json(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArbitraryContractStorageWriter {
|
||||
fn set_contract_storage(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
);
|
||||
|
||||
fn set_contract_storage_value<T: Serialize>(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
self.set_contract_storage(address, key, &to_json_vec(value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// attempts to write to an arbitrary contract `cw_storage_plus::Map`
|
||||
fn set_contract_map_value<'a, K, T>(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
namespace: impl AsRef<[u8]>,
|
||||
key: K,
|
||||
value: &T,
|
||||
) -> StdResult<()>
|
||||
where
|
||||
K: PrimaryKey<'a>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
let key_path: Path<T> = Path::new(
|
||||
namespace.as_ref(),
|
||||
&key.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
);
|
||||
let storage_key = key_path.deref();
|
||||
self.set_contract_storage_value(address, storage_key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// contract that has an admin
|
||||
pub trait AdminExt: StorageReader + StorageWriter {
|
||||
fn admin(&self) -> Option<Addr> {
|
||||
self.read_common_value(CommonStorageKeys::Admin)
|
||||
}
|
||||
|
||||
fn update_admin(&mut self, admin: &Option<Addr>) -> StdResult<()> {
|
||||
self.set_common_value(CommonStorageKeys::Admin, admin)
|
||||
}
|
||||
|
||||
fn admin_unchecked(&self) -> Addr {
|
||||
self.admin().expect("no admin set")
|
||||
}
|
||||
|
||||
fn admin_msg(&self) -> MessageInfo {
|
||||
message_info(&self.admin_unchecked(), &[])
|
||||
}
|
||||
}
|
||||
|
||||
// contract that operates on some specific coin denom
|
||||
pub trait DenomExt: StorageReader {
|
||||
fn denom(&self) -> String {
|
||||
self.unchecked_read_common_value(CommonStorageKeys::Denom)
|
||||
}
|
||||
|
||||
fn coin(&self, amount: u128) -> Coin {
|
||||
coin(amount, self.denom())
|
||||
}
|
||||
|
||||
fn coins(&self, amount: u128) -> Vec<Coin> {
|
||||
coins(amount, self.denom())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RandExt {
|
||||
fn raw_rng(&mut self) -> &mut ChaCha20Rng;
|
||||
|
||||
fn generate_account(&mut self) -> Addr;
|
||||
|
||||
fn generate_account_with_balance(&mut self) -> Addr
|
||||
where
|
||||
Self: BankExt;
|
||||
}
|
||||
|
||||
pub trait BankExt {
|
||||
fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl<T> AdminExt for T where T: StorageReader + StorageWriter {}
|
||||
impl<T> DenomExt for T where T: StorageReader {}
|
||||
|
||||
impl<C: TestableNymContract> StorageReader for ContractTester<C> {
|
||||
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]> {
|
||||
self.common_storage_keys.get(&key).map(|v| &**v)
|
||||
}
|
||||
|
||||
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> {
|
||||
<Self as ContractOpts>::read_from_contract_storage(self, key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TestableNymContract> StorageWriter for ContractTester<C> {
|
||||
fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
|
||||
<Self as ContractOpts>::set_contract_storage(self, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TestableNymContract> BankExt for ContractTester<C> {
|
||||
fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()> {
|
||||
self.app
|
||||
.send_tokens(self.master_address.clone(), to, &[amount])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TestableNymContract> RandExt for ContractTester<C> {
|
||||
fn raw_rng(&mut self) -> &mut ChaCha20Rng {
|
||||
&mut self.rng
|
||||
}
|
||||
|
||||
fn generate_account(&mut self) -> Addr {
|
||||
self.app
|
||||
.api()
|
||||
.addr_make(&format!("foomp{}", self.rng.next_u64()))
|
||||
}
|
||||
|
||||
fn generate_account_with_balance(&mut self) -> Addr
|
||||
where
|
||||
Self: BankExt,
|
||||
{
|
||||
let addr = self.generate_account();
|
||||
let million = 1_000_000_000_000;
|
||||
self.send_tokens(addr.clone(), coin(million, TEST_DENOM))
|
||||
.unwrap();
|
||||
addr
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryContractStorageReader for StorageWrapper {
|
||||
fn may_read_from_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> Option<Vec<u8>> {
|
||||
self.contract_storage_wrapper(&Addr::unchecked(address))
|
||||
.get(key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryContractStorageWriter for StorageWrapper {
|
||||
fn set_contract_storage(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) {
|
||||
// yeah, we're unnecessarily cloning a Rc pointer, but this is a test code, so this inefficiency is fine
|
||||
let mut wrapped_storage = self
|
||||
.clone()
|
||||
.contract_storage_wrapper(&Addr::unchecked(address));
|
||||
wrapped_storage.set(key.as_ref(), value.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ArbitraryContractStorageReader for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn may_read_from_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> Option<Vec<u8>> {
|
||||
self.storage
|
||||
.as_inner_storage()
|
||||
.may_read_from_contract_storage(address, key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ArbitraryContractStorageWriter for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn set_contract_storage(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) {
|
||||
self.storage
|
||||
.as_inner_storage_mut()
|
||||
.set_contract_storage(address, key, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{mock_api, test_rng, TEST_DENOM};
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::{
|
||||
coin, coins, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, QuerierWrapper,
|
||||
Record, Response, Storage,
|
||||
};
|
||||
use cw_multi_test::{App, AppBuilder, BankKeeper, Contract, ContractWrapper, Executor};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub use basic_traits::*;
|
||||
pub use extensions::*;
|
||||
|
||||
pub use crate::tester::storage_wrapper::{ContractStorageWrapper, StorageWrapper};
|
||||
|
||||
mod basic_traits;
|
||||
mod extensions;
|
||||
mod storage_wrapper;
|
||||
// copied from cw-multi-test (but removed generics for custom messages and querier for we don't need them for now)
|
||||
|
||||
pub type ContractFn<T, E> =
|
||||
fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result<Response, E>;
|
||||
pub type QueryFn<T, E> = fn(deps: Deps, env: Env, msg: T) -> Result<Binary, E>;
|
||||
pub type PermissionedFn<T, E> = fn(deps: DepsMut, env: Env, msg: T) -> Result<Response, E>;
|
||||
|
||||
pub type ContractClosure<T, E> = Box<dyn Fn(DepsMut, Env, MessageInfo, T) -> Result<Response, E>>;
|
||||
pub type QueryClosure<T, E> = Box<dyn Fn(Deps, Env, T) -> Result<Binary, E>>;
|
||||
|
||||
pub trait TestableNymContract {
|
||||
const NAME: &'static str;
|
||||
|
||||
type InitMsg: DeserializeOwned + Serialize + Debug + 'static;
|
||||
type ExecuteMsg: DeserializeOwned + Serialize + Debug + 'static;
|
||||
type QueryMsg: DeserializeOwned + Serialize + Debug + 'static;
|
||||
type MigrateMsg: DeserializeOwned + Serialize + Debug + 'static;
|
||||
type ContractError: Display + Debug + Send + Sync + 'static;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError>;
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError>;
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError>;
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError>;
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg;
|
||||
|
||||
// // for now we don't care about custom queriers
|
||||
// fn contract_wrapper() -> ContractWrapper<
|
||||
// Self::ExecuteMsg,
|
||||
// Self::InitMsg,
|
||||
// Self::QueryMsg,
|
||||
// Self::ContractError,
|
||||
// anyhow::Error,
|
||||
// anyhow::Error,
|
||||
// Empty,
|
||||
// Empty,
|
||||
// Empty,
|
||||
// Self::ContractError,
|
||||
// Self::ContractError,
|
||||
// Self::MigrateMsg,
|
||||
// Self::ContractError,
|
||||
// > {
|
||||
// ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
|
||||
// .with_migrate(Self::migrate())
|
||||
// }
|
||||
|
||||
fn dyn_contract() -> Box<dyn Contract<Empty>> {
|
||||
Box::new(
|
||||
ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
|
||||
.with_migrate(Self::migrate()),
|
||||
)
|
||||
}
|
||||
|
||||
fn init() -> ContractTester<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
ContractTesterBuilder::new()
|
||||
.instantiate::<Self>(None)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContractTesterBuilder<C> {
|
||||
contract: PhantomData<C>,
|
||||
master_address: Addr,
|
||||
app: App<BankKeeper, MockApi, StorageWrapper>,
|
||||
storage: StorageWrapper,
|
||||
pub well_known_contracts: HashMap<&'static str, Addr>,
|
||||
}
|
||||
|
||||
impl<C> ContractTesterBuilder<C> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
let storage = StorageWrapper::new();
|
||||
|
||||
let api = mock_api();
|
||||
let master_address = api.addr_make("master-owner");
|
||||
|
||||
let app = AppBuilder::new()
|
||||
.with_api(api)
|
||||
.with_storage(storage.clone())
|
||||
.build(|router, _api, storage| {
|
||||
router
|
||||
.bank
|
||||
.init_balance(
|
||||
storage,
|
||||
&master_address,
|
||||
coins(1000000000000000, TEST_DENOM),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
ContractTesterBuilder {
|
||||
contract: Default::default(),
|
||||
master_address,
|
||||
app,
|
||||
storage,
|
||||
well_known_contracts: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instantiate<D: TestableNymContract>(
|
||||
mut self,
|
||||
custom_init_msg: Option<D::InitMsg>,
|
||||
) -> ContractTesterBuilder<C> {
|
||||
let code_id = self.app.store_code(D::dyn_contract());
|
||||
let contract_address = self
|
||||
.app
|
||||
.instantiate_contract(
|
||||
code_id,
|
||||
self.master_address.clone(),
|
||||
&custom_init_msg.unwrap_or(D::base_init_msg()),
|
||||
&[],
|
||||
D::NAME,
|
||||
Some(self.master_address.to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// send some tokens to the contract
|
||||
self.app
|
||||
.send_tokens(
|
||||
self.master_address.clone(),
|
||||
contract_address.clone(),
|
||||
&[coin(100000000, TEST_DENOM)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.well_known_contracts.insert(D::NAME, contract_address);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
if !self.well_known_contracts.contains_key(C::NAME) {
|
||||
panic!("{} contract has not been instantiated", C::NAME);
|
||||
}
|
||||
|
||||
let contract_address = self.well_known_contracts[C::NAME].clone();
|
||||
|
||||
ContractTester {
|
||||
contract: self.contract,
|
||||
app: self.app,
|
||||
rng: test_rng(),
|
||||
master_address: self.master_address,
|
||||
storage: self.storage.contract_storage_wrapper(&contract_address),
|
||||
contract_address,
|
||||
common_storage_keys: Default::default(),
|
||||
well_known_contracts: self.well_known_contracts,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
|
||||
self.storage.contract_storage_wrapper(contract)
|
||||
}
|
||||
|
||||
pub fn api(&self) -> MockApi {
|
||||
*self.app.api()
|
||||
}
|
||||
|
||||
pub fn querier(&self) -> QuerierWrapper {
|
||||
self.app.wrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
|
||||
pub enum CommonStorageKeys {
|
||||
Admin,
|
||||
Denom,
|
||||
}
|
||||
|
||||
pub struct ContractTester<C: TestableNymContract> {
|
||||
contract: PhantomData<C>,
|
||||
pub app: App<BankKeeper, MockApi, StorageWrapper>,
|
||||
pub rng: ChaCha20Rng,
|
||||
pub contract_address: Addr,
|
||||
pub master_address: Addr,
|
||||
pub(crate) storage: ContractStorageWrapper,
|
||||
pub common_storage_keys: HashMap<CommonStorageKeys, Vec<u8>>,
|
||||
|
||||
// TODO: limitation: doesn't allow multiple contracts of the same type (but that's fine for the time being)
|
||||
pub well_known_contracts: HashMap<&'static str, Addr>,
|
||||
}
|
||||
|
||||
impl<C> ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
pub fn insert_common_storage_key(&mut self, key: CommonStorageKeys, value: impl AsRef<[u8]>) {
|
||||
self.common_storage_keys
|
||||
.insert(key, value.as_ref().to_vec());
|
||||
}
|
||||
|
||||
pub fn with_common_storage_key(
|
||||
mut self,
|
||||
key: CommonStorageKeys,
|
||||
value: impl AsRef<[u8]>,
|
||||
) -> Self {
|
||||
self.insert_common_storage_key(key, value);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Storage for ContractTester<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
|
||||
self.storage.get(key)
|
||||
}
|
||||
|
||||
fn range<'a>(
|
||||
&'a self,
|
||||
start: Option<&[u8]>,
|
||||
end: Option<&[u8]>,
|
||||
order: Order,
|
||||
) -> Box<dyn Iterator<Item = Record> + 'a> {
|
||||
self.storage.range(start, end, order)
|
||||
}
|
||||
|
||||
fn range_keys<'a>(
|
||||
&'a self,
|
||||
start: Option<&[u8]>,
|
||||
end: Option<&[u8]>,
|
||||
order: Order,
|
||||
) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> {
|
||||
self.storage.range_keys(start, end, order)
|
||||
}
|
||||
|
||||
fn range_values<'a>(
|
||||
&'a self,
|
||||
start: Option<&[u8]>,
|
||||
end: Option<&[u8]>,
|
||||
order: Order,
|
||||
) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> {
|
||||
self.storage.range_values(start, end, order)
|
||||
}
|
||||
|
||||
fn set(&mut self, key: &[u8], value: &[u8]) {
|
||||
self.storage.set(key, value)
|
||||
}
|
||||
|
||||
fn remove(&mut self, key: &[u8]) {
|
||||
self.storage.remove(key)
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -11,7 +11,7 @@ use std::rc::Rc;
|
||||
pub struct StorageWrapper(Rc<RefCell<MemoryStorage>>);
|
||||
|
||||
impl StorageWrapper {
|
||||
pub(super) fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
|
||||
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
|
||||
ContractStorageWrapper {
|
||||
address: contract.clone(),
|
||||
inner: self.clone(),
|
||||
@@ -24,7 +24,7 @@ impl StorageWrapper {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ContractStorageWrapper {
|
||||
pub struct ContractStorageWrapper {
|
||||
address: Addr,
|
||||
inner: StorageWrapper,
|
||||
}
|
||||
@@ -33,6 +33,22 @@ impl ContractStorageWrapper {
|
||||
pub fn inner_storage(&self) -> StorageWrapper {
|
||||
self.inner.clone()
|
||||
}
|
||||
|
||||
pub fn as_inner_storage(&self) -> &StorageWrapper {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn as_inner_storage_mut(&mut self) -> &mut StorageWrapper {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
#[must_use = "this returns the result of the operation, without modifying the original"]
|
||||
pub fn change_contract(&self, contract: &Addr) -> Self {
|
||||
ContractStorageWrapper {
|
||||
address: contract.clone(),
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for StorageWrapper {
|
||||
@@ -18,6 +18,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -35,7 +35,7 @@ pub enum ContractsCommonError {
|
||||
/// Percent represents a value between 0 and 100%
|
||||
/// (i.e. between 0.0 and 1.0)
|
||||
#[cw_serde]
|
||||
#[derive(Copy, Default, PartialOrd)]
|
||||
#[derive(Copy, Default, PartialOrd, Ord, Eq)]
|
||||
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
|
||||
|
||||
impl Percent {
|
||||
@@ -80,6 +80,44 @@ impl Percent {
|
||||
pub fn checked_pow(&self, exp: u32) -> Result<Self, OverflowError> {
|
||||
self.0.checked_pow(exp).map(Percent)
|
||||
}
|
||||
|
||||
// truncate provided percent to only have 2 decimal places,
|
||||
// e.g. convert "0.1234567" into "0.12"
|
||||
// the purpose of it is to reduce storage space, in particular for the performance contract
|
||||
// since that extra precision gains us nothing
|
||||
#[must_use = "this returns the result of the operation, without modifying the original"]
|
||||
pub fn round_to_two_decimal_places(&self) -> Self {
|
||||
let raw = self.0;
|
||||
|
||||
const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18
|
||||
const THRESHOLD: Decimal = Decimal::permille(5); // 0.005
|
||||
|
||||
// in case it ever changes since it's not exposed in the public API
|
||||
debug_assert_eq!(
|
||||
DECIMAL_FRACTIONAL,
|
||||
Uint128::new(10).pow(Decimal::DECIMAL_PLACES)
|
||||
);
|
||||
|
||||
let int = (raw.atomics() * Uint128::new(100)) / DECIMAL_FRACTIONAL;
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let floored = Decimal::from_atomics(int, 2).unwrap();
|
||||
let diff = raw - floored;
|
||||
let rounded = if diff >= THRESHOLD {
|
||||
// ceil
|
||||
floored + Decimal::percent(1)
|
||||
} else {
|
||||
floored
|
||||
};
|
||||
Percent(rounded)
|
||||
}
|
||||
|
||||
#[must_use = "this returns the result of the operation, without modifying the original"]
|
||||
pub fn average(&self, other: &Self) -> Self {
|
||||
let sum = self.0 + other.0;
|
||||
let inner = Decimal::from_ratio(sum.numerator(), sum.denominator() * Uint128::new(2));
|
||||
Percent(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Percent {
|
||||
@@ -334,6 +372,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "naive_float")]
|
||||
fn naive_float_conversion() {
|
||||
// around 15 decimal places is the maximum precision we can handle
|
||||
// which is still way more than enough for what we use it for
|
||||
@@ -347,4 +386,41 @@ mod tests {
|
||||
|
||||
assert!(converted.0 - converted.0 < epsilon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounding_percent() {
|
||||
let test_cases = vec![
|
||||
("0", "0"),
|
||||
("0.1", "0.1"),
|
||||
("0.12", "0.12"),
|
||||
("0.12", "0.123"),
|
||||
("0.12", "0.123456789"),
|
||||
("0.13", "0.125"),
|
||||
("0.13", "0.126"),
|
||||
("0.13", "0.126436545676"),
|
||||
("0.99", "0.99"),
|
||||
("0.99", "0.994"),
|
||||
("1", "0.999"),
|
||||
("1", "0.995"),
|
||||
];
|
||||
for (expected, input) in test_cases {
|
||||
let expected: Percent = expected.parse().unwrap();
|
||||
let pre_truncated: Percent = input.parse().unwrap();
|
||||
assert_eq!(expected, pre_truncated.round_to_two_decimal_places())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculating_average() -> anyhow::Result<()> {
|
||||
fn p(raw: &str) -> Percent {
|
||||
raw.parse().unwrap()
|
||||
}
|
||||
|
||||
assert_eq!(p("0.1").average(&p("0.1")), p("0.1"));
|
||||
assert_eq!(p("0.1").average(&p("0.2")), p("0.15"));
|
||||
assert_eq!(p("1").average(&p("0")), p("0.5"));
|
||||
assert_eq!(p("0.123").average(&p("0.456")), p("0.2895"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ semver = { workspace = true, features = ["serde"] }
|
||||
schemars = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" }
|
||||
serde-json-wasm = { workspace = true }
|
||||
humantime-serde = { workspace = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::{IdentityKey, NodeId, SphinxKey};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use cosmwasm_std::{to_json_string, Addr, Coin};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
|
||||
@@ -154,7 +154,7 @@ pub struct GatewayConfigUpdate {
|
||||
|
||||
impl GatewayConfigUpdate {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
Percent, ProfitMarginRange, SphinxKey,
|
||||
};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
|
||||
use cosmwasm_std::{to_json_string, Addr, Coin, Decimal, StdResult, Uint128};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
@@ -604,7 +604,7 @@ pub struct NodeCostParams {
|
||||
|
||||
impl NodeCostParams {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,7 +773,7 @@ pub struct MixNodeConfigUpdate {
|
||||
|
||||
impl MixNodeConfigUpdate {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::helpers::IntoBaseDecimal;
|
||||
use crate::nym_node::Role;
|
||||
use crate::{error::MixnetContractError, Percent};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::{to_json_string, Decimal};
|
||||
|
||||
pub type Performance = Percent;
|
||||
pub type WorkFactor = Decimal;
|
||||
@@ -84,7 +84,7 @@ pub struct IntervalRewardParams {
|
||||
|
||||
impl IntervalRewardParams {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@ impl IntervalRewardingParamsUpdate {
|
||||
}
|
||||
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
|
||||
nym-contracts-common = { path = "../contracts-common" }
|
||||
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod storage_keys {
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id";
|
||||
pub const MIXNET_CONTRACT: &str = "mixnet-contract";
|
||||
pub const AUTHORISED_COUNT: &str = "authorised-count";
|
||||
pub const AUTHORISED: &str = "authorised";
|
||||
pub const RETIRED: &str = "retired";
|
||||
pub const PERFORMANCE_RESULTS: &str = "performance-results";
|
||||
pub const SUBMISSION_METADATA: &str = "submission-metadata";
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{EpochId, NodeId};
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_controllers::AdminError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NymPerformanceContractError {
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
|
||||
#[error("{address} is already an authorised network monitor")]
|
||||
AlreadyAuthorised { address: Addr },
|
||||
|
||||
#[error("{address} is not an authorised network monitor")]
|
||||
NotAuthorised { address: Addr },
|
||||
|
||||
#[error("attempted to submit performance data for epoch {epoch_id} and node {node_id} whilst last submitted was {last_epoch_id} for node {last_node_id}")]
|
||||
StalePerformanceSubmission {
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
last_epoch_id: EpochId,
|
||||
last_node_id: NodeId,
|
||||
},
|
||||
|
||||
#[error("the batch performance data has not been sorted")]
|
||||
UnsortedBatchSubmission,
|
||||
|
||||
#[error("node {node_id} does not appear to be bonded")]
|
||||
NodeNotBonded { node_id: NodeId },
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod helpers;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{EpochId, NodeId, NodePerformance};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::types::{
|
||||
EpochMeasurementsPagedResponse, EpochPerformancePagedResponse,
|
||||
FullHistoricalPerformancePagedResponse, NetworkMonitorResponse, NetworkMonitorsPagedResponse,
|
||||
NodeMeasurementsResponse, NodePerformancePagedResponse, NodePerformanceResponse,
|
||||
RetiredNetworkMonitorsPagedResponse,
|
||||
};
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
pub mixnet_contract_address: String,
|
||||
pub authorised_network_monitors: Vec<String>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Change the admin
|
||||
UpdateAdmin { admin: String },
|
||||
|
||||
/// Attempt to submit performance data of a particular node for given epoch
|
||||
Submit {
|
||||
epoch: EpochId,
|
||||
data: NodePerformance,
|
||||
},
|
||||
|
||||
/// Attempt to submit performance data of a batch of nodes for given epoch
|
||||
BatchSubmit {
|
||||
epoch: EpochId,
|
||||
data: Vec<NodePerformance>,
|
||||
},
|
||||
|
||||
/// Attempt to authorise new network monitor for submitting performance data
|
||||
AuthoriseNetworkMonitor { address: String },
|
||||
|
||||
/// Attempt to retire an existing network monitor and forbid it from submitting any future performance data
|
||||
RetireNetworkMonitor { address: String },
|
||||
|
||||
/// An admin method to remove submitted node measurements. Used as an escape hatch should
|
||||
/// the data stored get too unwieldy.
|
||||
RemoveNodeMeasurements { epoch_id: EpochId, node_id: NodeId },
|
||||
|
||||
/// An admin method to remove submitted nodes measurements. Used as an escape hatch should
|
||||
/// the data stored get too unwieldy. Note: it is expected to get called multiple times
|
||||
/// until the response indicates all the epoch data has been removed.
|
||||
RemoveEpochMeasurements { epoch_id: EpochId },
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
|
||||
Admin {},
|
||||
|
||||
/// Returns performance of particular node for the provided epoch
|
||||
#[cfg_attr(feature = "schema", returns(NodePerformanceResponse))]
|
||||
NodePerformance { epoch_id: EpochId, node_id: NodeId },
|
||||
|
||||
/// Returns historical performance for particular node
|
||||
#[cfg_attr(feature = "schema", returns(NodePerformancePagedResponse))]
|
||||
NodePerformancePaged {
|
||||
node_id: NodeId,
|
||||
start_after: Option<EpochId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns all submitted measurements for the particular node
|
||||
#[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))]
|
||||
NodeMeasurements { epoch_id: EpochId, node_id: NodeId },
|
||||
|
||||
/// Returns (paged) measurements for particular epoch
|
||||
#[cfg_attr(feature = "schema", returns(EpochMeasurementsPagedResponse))]
|
||||
EpochMeasurementsPaged {
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns (paged) performance for particular epoch
|
||||
#[cfg_attr(feature = "schema", returns(EpochPerformancePagedResponse))]
|
||||
EpochPerformancePaged {
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns full (paged) historical performance of the whole network
|
||||
#[cfg_attr(feature = "schema", returns(FullHistoricalPerformancePagedResponse))]
|
||||
FullHistoricalPerformancePaged {
|
||||
start_after: Option<(EpochId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns information about particular network monitor
|
||||
#[cfg_attr(feature = "schema", returns(NetworkMonitorResponse))]
|
||||
NetworkMonitor { address: String },
|
||||
|
||||
/// Returns information about all network monitors
|
||||
#[cfg_attr(feature = "schema", returns(NetworkMonitorsPagedResponse))]
|
||||
NetworkMonitorsPaged {
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
|
||||
/// Returns information about all retired network monitors
|
||||
#[cfg_attr(feature = "schema", returns(RetiredNetworkMonitorsPagedResponse))]
|
||||
RetiredNetworkMonitorsPaged {
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Env};
|
||||
use nym_contracts_common::Percent;
|
||||
|
||||
pub type EpochId = u32;
|
||||
pub type NodeId = u32;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorDetails {
|
||||
pub address: Addr,
|
||||
pub authorised_by: Addr,
|
||||
pub authorised_at_height: u64,
|
||||
}
|
||||
|
||||
impl NetworkMonitorDetails {
|
||||
pub fn retire(self, env: &Env, sender: &Addr) -> RetiredNetworkMonitor {
|
||||
RetiredNetworkMonitor {
|
||||
details: self,
|
||||
retired_by: sender.clone(),
|
||||
retired_at_height: env.block.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct RetiredNetworkMonitor {
|
||||
pub details: NetworkMonitorDetails,
|
||||
pub retired_by: Addr,
|
||||
pub retired_at_height: u64,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
pub struct NodePerformance {
|
||||
#[serde(rename = "n")]
|
||||
pub node_id: NodeId,
|
||||
|
||||
// note: value is rounded to 2 decimal places.
|
||||
#[serde(rename = "p")]
|
||||
pub performance: Percent,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorSubmissionMetadata {
|
||||
pub last_submitted_epoch_id: EpochId,
|
||||
pub last_submitted_node_id: NodeId,
|
||||
}
|
||||
|
||||
// the internal values are always sorted
|
||||
#[cw_serde]
|
||||
pub struct NodeResults(Vec<Percent>);
|
||||
|
||||
impl NodeResults {
|
||||
pub fn new(initial: Percent) -> NodeResults {
|
||||
NodeResults(vec![initial.round_to_two_decimal_places()])
|
||||
}
|
||||
|
||||
// ASSUMPTION: number of NM will be relatively small, so loading the whole vector of values
|
||||
// to insert new one and resave is cheap
|
||||
pub fn insert_new(&mut self, result: Percent) {
|
||||
let result = result.round_to_two_decimal_places();
|
||||
let pos = self.0.binary_search(&result).unwrap_or_else(|e| e);
|
||||
self.0.insert(pos, result);
|
||||
}
|
||||
|
||||
// SAFETY: there are no codepaths that allow constructing empty struct
|
||||
pub fn median(&self) -> Percent {
|
||||
let len = self.0.len();
|
||||
if len % 2 == 1 {
|
||||
// odd number of elements: return the middle one
|
||||
self.0[len / 2]
|
||||
} else {
|
||||
// even number: average the two middle elements
|
||||
let mid1 = self.0[len / 2 - 1];
|
||||
let mid2 = self.0[len / 2];
|
||||
mid1.average(&mid2).round_to_two_decimal_places()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &[Percent] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NodePerformanceResponse {
|
||||
pub performance: Option<Percent>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NodeMeasurementsResponse {
|
||||
pub measurements: Option<NodeResults>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
pub struct EpochNodePerformance {
|
||||
pub epoch: EpochId,
|
||||
pub performance: Option<Percent>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NodePerformancePagedResponse {
|
||||
pub node_id: NodeId,
|
||||
pub performance: Vec<EpochNodePerformance>,
|
||||
pub start_next_after: Option<EpochId>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct EpochPerformancePagedResponse {
|
||||
pub epoch_id: EpochId,
|
||||
pub performance: Vec<NodePerformance>,
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NodeMeasurement {
|
||||
pub node_id: NodeId,
|
||||
pub measurements: NodeResults,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct EpochMeasurementsPagedResponse {
|
||||
pub epoch_id: EpochId,
|
||||
pub measurements: Vec<NodeMeasurement>,
|
||||
pub start_next_after: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
pub struct HistoricalPerformance {
|
||||
pub epoch_id: EpochId,
|
||||
pub node_id: NodeId,
|
||||
pub performance: Percent,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct FullHistoricalPerformancePagedResponse {
|
||||
pub performance: Vec<HistoricalPerformance>,
|
||||
pub start_next_after: Option<(EpochId, NodeId)>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorInformation {
|
||||
pub details: NetworkMonitorDetails,
|
||||
pub current_submission_metadata: NetworkMonitorSubmissionMetadata,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorResponse {
|
||||
pub info: Option<NetworkMonitorInformation>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct NetworkMonitorsPagedResponse {
|
||||
pub info: Vec<NetworkMonitorInformation>,
|
||||
pub start_next_after: Option<String>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct RetiredNetworkMonitorsPagedResponse {
|
||||
pub info: Vec<RetiredNetworkMonitor>,
|
||||
pub start_next_after: Option<String>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct RemoveEpochMeasurementsResponse {
|
||||
pub additional_entries_to_remove_remaining: bool,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Default)]
|
||||
pub struct BatchSubmissionResult {
|
||||
pub accepted_scores: u64,
|
||||
pub non_existent_nodes: Vec<NodeId>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn p(raw: impl AsRef<str>) -> Percent {
|
||||
raw.as_ref().parse().unwrap()
|
||||
}
|
||||
|
||||
fn ps(raw: &[&str]) -> Vec<Percent> {
|
||||
raw.iter().map(p).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_results_insertion() {
|
||||
let initial = NodeResults::new(p("0.5"));
|
||||
|
||||
let mut smaller = initial.clone();
|
||||
let mut greater = initial.clone();
|
||||
|
||||
smaller.insert_new(p("0.4"));
|
||||
greater.insert_new(p("0.6"));
|
||||
|
||||
assert_eq!(smaller.0, ps(&["0.4", "0.5"]));
|
||||
assert_eq!(greater.0, ps(&["0.5", "0.6"]));
|
||||
|
||||
let mut another = NodeResults(ps(&["0.1", "0.4", "0.5", "0.6", "0.6", "1.0"]));
|
||||
another.insert_new(p("0.6"));
|
||||
another.insert_new(p("0.2"));
|
||||
another.insert_new(p("0.7"));
|
||||
another.insert_new(p("0.3"));
|
||||
another.insert_new(p("0.3"));
|
||||
another.insert_new(p("0.55"));
|
||||
|
||||
assert_eq!(
|
||||
another.0,
|
||||
ps(&[
|
||||
"0.1", "0.2", "0.3", "0.3", "0.4", "0.5", "0.55", "0.6", "0.6", "0.6", "0.7", "1.0"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_results_median() {
|
||||
let results = NodeResults(ps(&["0.1"]));
|
||||
assert_eq!(results.median(), p("0.1"));
|
||||
|
||||
let results = NodeResults(ps(&["0.1", "0.2"]));
|
||||
assert_eq!(results.median(), p("0.15"));
|
||||
|
||||
let results = NodeResults(ps(&["0.1", "0.2", "0.3"]));
|
||||
assert_eq!(results.median(), p("0.2"));
|
||||
|
||||
let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4"]));
|
||||
assert_eq!(results.median(), p("0.25"));
|
||||
|
||||
let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4", "0.5"]));
|
||||
assert_eq!(results.median(), p("0.3"));
|
||||
|
||||
let results = NodeResults(ps(&["0", "0", "1", "1", "1", "1", "1"]));
|
||||
assert_eq!(results.median(), p("1"));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ nym-credentials = { path = "../credentials" }
|
||||
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
|
||||
nym-ecash-time = { path = "../ecash-time" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
path = "../../sqlx-pool-guard"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
@@ -31,8 +33,13 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
persistent-storage = ["bincode", "serde"]
|
||||
persistent-storage = ["bincode", "serde"]
|
||||
|
||||
@@ -7,10 +7,11 @@ use crate::models::{
|
||||
};
|
||||
use nym_ecash_time::Date;
|
||||
use sqlx::{Executor, Sqlite, Transaction};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SqliteEcashTicketbookManager {
|
||||
connection_pool: sqlx::SqlitePool,
|
||||
connection_pool: SqlitePoolGuard,
|
||||
}
|
||||
|
||||
impl SqliteEcashTicketbookManager {
|
||||
@@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `connection_pool`: database connection pool to use.
|
||||
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
|
||||
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
|
||||
SqliteEcashTicketbookManager { connection_pool }
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
|
||||
deadline
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager {
|
||||
data,
|
||||
expiration_date,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager {
|
||||
epoch_id,
|
||||
total_tickets,
|
||||
used_tickets,
|
||||
).execute(&self.connection_pool).await?;
|
||||
).execute(&*self.connection_pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
)
|
||||
.bind(data)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await?
|
||||
.is_some();
|
||||
|
||||
@@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager {
|
||||
FROM ecash_ticketbook
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager {
|
||||
ticketbook_id,
|
||||
expected_current_total_spent
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
Ok(affected > 0)
|
||||
@@ -153,7 +154,7 @@ impl SqliteEcashTicketbookManager {
|
||||
&self,
|
||||
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
|
||||
sqlx::query_as("SELECT * FROM pending_issuance")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM pending_issuance WHERE deposit_id = ?",
|
||||
pending_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
expiration_date
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
expiration_date
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
use std::path::Path;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -54,15 +55,15 @@ impl PersistentStorage {
|
||||
/// * `database_path`: path to the database.
|
||||
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(database_path)
|
||||
.filename(&database_path)
|
||||
.create_if_missing(true)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -74,13 +75,17 @@ impl PersistentStorage {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
|
||||
error!("Failed to perform migration on the SQLx database: {err}");
|
||||
connection_pool.close().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
Ok(PersistentStorage {
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,8 @@ impl BandwidthStorageManager {
|
||||
debug!(available = available_bi2, required = required_bi2);
|
||||
|
||||
self.consume_bandwidth(required_bandwidth).await?;
|
||||
Ok(available_bandwidth)
|
||||
let remaining_bandwidth = self.client_bandwidth.available().await;
|
||||
Ok(remaining_bandwidth)
|
||||
}
|
||||
|
||||
async fn expire_bandwidth(&mut self) -> Result<()> {
|
||||
|
||||
@@ -14,6 +14,7 @@ use nym_dkg::bte::{
|
||||
};
|
||||
use nym_dkg::interpolation::polynomial::Polynomial;
|
||||
use nym_dkg::{combine_shares, Dealing, NodeIndex, Share, Threshold};
|
||||
use rand::CryptoRng;
|
||||
use rand_core::{RngCore, SeedableRng};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -31,7 +32,7 @@ pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn prepare_keys(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
nodes: usize,
|
||||
) -> (BTreeMap<NodeIndex, PublicKey>, Vec<DecryptionKey>) {
|
||||
let params = setup();
|
||||
@@ -50,7 +51,7 @@ fn prepare_keys(
|
||||
}
|
||||
|
||||
fn prepare_resharing(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
params: &Params,
|
||||
nodes: usize,
|
||||
threshold: Threshold,
|
||||
@@ -68,7 +69,7 @@ fn prepare_resharing(
|
||||
for (i, ref mut dk) in dks.iter_mut().enumerate() {
|
||||
let shares = first_dealings
|
||||
.iter()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(params, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -154,7 +155,9 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -237,7 +240,9 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -320,7 +325,9 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -547,7 +554,7 @@ pub fn share_decryption(c: &mut Criterion) {
|
||||
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng);
|
||||
|
||||
c.bench_function("single share decryption", |b| {
|
||||
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None)))
|
||||
b.iter(|| black_box(decrypt_share(¶ms, &dk, 0, &ciphertexts, None)))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{Chunk, ChunkedShare, Share};
|
||||
use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar};
|
||||
use ff::Field;
|
||||
use group::{Curve, Group, GroupEncoding};
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Neg;
|
||||
@@ -191,7 +192,7 @@ impl HazmatRandomness {
|
||||
pub fn encrypt_shares(
|
||||
shares: &[(&Share, &PublicKey)],
|
||||
params: &Params,
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (Ciphertexts, HazmatRandomness) {
|
||||
let g1 = G1Projective::generator();
|
||||
|
||||
@@ -262,6 +263,7 @@ pub fn encrypt_shares(
|
||||
}
|
||||
|
||||
pub fn decrypt_share(
|
||||
params: &Params,
|
||||
dk: &DecryptionKey,
|
||||
// in the case of multiple receivers, specifies which index of ciphertext chunks should be used
|
||||
i: usize,
|
||||
@@ -270,6 +272,10 @@ pub fn decrypt_share(
|
||||
) -> Result<Share, DkgError> {
|
||||
let mut plaintext = ChunkedShare::default();
|
||||
|
||||
if !ciphertext.verify_integrity(params) {
|
||||
return Err(DkgError::FailedCiphertextIntegrityCheck);
|
||||
}
|
||||
|
||||
if i >= ciphertext.ciphertext_chunks.len() {
|
||||
return Err(DkgError::UnavailableCiphertext(i));
|
||||
}
|
||||
@@ -461,10 +467,22 @@ mod tests {
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered1 =
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered2 =
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered1 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key1,
|
||||
0,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
let recovered2 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key2,
|
||||
1,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(m1, recovered1);
|
||||
assert_eq!(m2, recovered2);
|
||||
}
|
||||
@@ -490,10 +508,22 @@ mod tests {
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered1 =
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered2 =
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered1 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key1,
|
||||
0,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
let recovered2 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key2,
|
||||
1,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(m1, recovered1);
|
||||
assert_eq!(m2, recovered2);
|
||||
}
|
||||
@@ -574,7 +604,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ciphertexts_roundtrip() {
|
||||
fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts {
|
||||
fn random_ciphertexts(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
num_receivers: usize,
|
||||
) -> Ciphertexts {
|
||||
Ciphertexts {
|
||||
rr: (0..NUM_CHUNKS)
|
||||
.map(|_| G1Projective::random(&mut rng))
|
||||
|
||||
@@ -9,11 +9,15 @@ use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// produces public key and a decryption key for the root of the tree
|
||||
pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) {
|
||||
pub fn keygen(
|
||||
params: &Params,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (DecryptionKey, PublicKeyWithProof) {
|
||||
let g1 = G1Projective::generator();
|
||||
let g2 = G2Projective::generator();
|
||||
|
||||
@@ -244,7 +248,7 @@ pub struct KeyPair {
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new(params: &Params, rng: impl RngCore) -> Self {
|
||||
pub fn new(params: &Params, rng: impl RngCore + CryptoRng) -> Self {
|
||||
let (dk, pk) = keygen(params, rng);
|
||||
Self {
|
||||
private_key: dk,
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::utils::{deserialize_scalar, RandomOracleBuilder};
|
||||
use bls12_381::{G1Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::{Group, GroupEncoding};
|
||||
use rand::Rng;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use rand_core::{RngCore, SeedableRng};
|
||||
|
||||
const CHUNKING_ORACLE_DOMAIN: &[u8] =
|
||||
@@ -28,6 +28,7 @@ const SECURITY_PARAMETER: usize = 256;
|
||||
|
||||
/// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper
|
||||
const NUM_CHALLENGE_BITS: usize = SECURITY_PARAMETER.div_ceil(PARALLEL_RUNS);
|
||||
const EE: usize = 1 << NUM_CHALLENGE_BITS;
|
||||
|
||||
// type alias for ease of use
|
||||
type FirstChallenge = Vec<Vec<Vec<u64>>>;
|
||||
@@ -94,7 +95,7 @@ impl ProofOfChunking {
|
||||
// Scalar(-1) would in reality be Scalar(q - 1), which is greater than Scalar(1) and opposite to
|
||||
// what we wanted.
|
||||
pub fn construct(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
instance: Instance,
|
||||
witness_r: &[Scalar; NUM_CHUNKS],
|
||||
witnesses_s: &[Share],
|
||||
@@ -110,21 +111,20 @@ impl ProofOfChunking {
|
||||
// define bounds for the blinding factors
|
||||
let n = instance.public_keys.len();
|
||||
let m = NUM_CHUNKS;
|
||||
let ee = 1 << NUM_CHALLENGE_BITS;
|
||||
|
||||
// CHUNK_MAX corresponds to paper's B
|
||||
let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
|
||||
let zz = (2 * (PARALLEL_RUNS as u64))
|
||||
.checked_mul(ss)
|
||||
.expect("overflow in Z = 2 * l * S");
|
||||
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
|
||||
// Z = 2 * l * S
|
||||
let (ss, zz): (u64, u64) = compute_ss_zz(n, m)?;
|
||||
|
||||
let ss_scalar = Scalar::from(ss);
|
||||
|
||||
// rather than generating blinding factors in [-S, Z-1] directly,
|
||||
// do it via [0, Z - 1 + S + 1] and deal with the shift later.
|
||||
let combined_upper_range = (zz - 1)
|
||||
.checked_add(ss + 1)
|
||||
.expect("overflow in Z - 1 + S + 1");
|
||||
// combined_upper_range = Z - 1 + S + 1
|
||||
|
||||
let combined_upper_range = zz.checked_add(ss).ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::construct | Z - 1 + S + 1",
|
||||
})?;
|
||||
|
||||
let mut betas = Vec::with_capacity(PARALLEL_RUNS);
|
||||
let mut bs = Vec::with_capacity(PARALLEL_RUNS);
|
||||
@@ -178,12 +178,23 @@ impl ProofOfChunking {
|
||||
// I think this part is more readable with a range loop
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for l in 0..PARALLEL_RUNS {
|
||||
let mut sum = 0;
|
||||
let mut sum: u64 = 0;
|
||||
|
||||
for (i, witness_i) in witnesses_s.iter().enumerate() {
|
||||
for (j, witness_ij) in witness_i.to_chunks().chunks.iter().enumerate() {
|
||||
debug_assert!(std::mem::size_of::<Chunk>() <= std::mem::size_of::<u64>());
|
||||
sum += first_challenge[i][j][l] * (*witness_ij as u64)
|
||||
// sum += first_challenge[i][j][l] * (*witness_ij as u64)
|
||||
sum = sum
|
||||
.checked_add(
|
||||
first_challenge[i][j][l]
|
||||
.checked_mul(*witness_ij as u64)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::construct | first_challenge[i][j][l] * witness_ij",
|
||||
})?,
|
||||
)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::construct | sum + (first_challenge[i][j][l] * witness_ij)",
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +202,18 @@ impl ProofOfChunking {
|
||||
continue 'retry_loop;
|
||||
}
|
||||
// shifted_blinding_factors[l] - ss restores it to "proper" [-S, Z - 1] range
|
||||
let response = sum + shifted_blinding_factors[l] - ss;
|
||||
// let response = sum + shifted_blinding_factors[l] - ss;
|
||||
let response = sum
|
||||
.checked_add(shifted_blinding_factors[l])
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info:
|
||||
"ProofOfChunking::construct | sum + (shifted_blinding_factors[l] - ss)",
|
||||
})?
|
||||
.checked_sub(ss)
|
||||
.ok_or(DkgError::ArithmeticUnderflow {
|
||||
info: "ProofOfChunking::construct | shifted_blinding_factors[l] - ss",
|
||||
})?;
|
||||
|
||||
if response < zz {
|
||||
responses_chunks.push(response)
|
||||
} else {
|
||||
@@ -276,11 +298,13 @@ impl ProofOfChunking {
|
||||
ensure_len!(&self.responses_r, n);
|
||||
ensure_len!(&self.responses_chunks, PARALLEL_RUNS);
|
||||
|
||||
let ee = 1 << NUM_CHALLENGE_BITS;
|
||||
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
|
||||
// Z = 2 * l * S
|
||||
|
||||
// CHUNK_MAX corresponds to paper's B
|
||||
let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
|
||||
let zz = 2 * (PARALLEL_RUNS as u64) * ss;
|
||||
let zz: u64 = match compute_ss_zz(n, m) {
|
||||
Ok((_, zz_res)) => zz_res,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
for response_chunk in &self.responses_chunks {
|
||||
if response_chunk >= &zz {
|
||||
@@ -411,7 +435,7 @@ impl ProofOfChunking {
|
||||
random_oracle_builder.update(lambda_e.to_be_bytes());
|
||||
|
||||
let mut oracle = rand_chacha::ChaCha20Rng::from_seed(random_oracle_builder.finalize());
|
||||
let range_max_excl = 1 << NUM_CHALLENGE_BITS;
|
||||
let range_max_excl = EE as u64;
|
||||
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
@@ -637,6 +661,50 @@ impl ProofOfChunking {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_ss_zz(n: usize, m: usize) -> Result<(u64, u64), DkgError> {
|
||||
// let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
|
||||
// CHUNK_MAX corresponds to paper's B
|
||||
|
||||
let ee = EE;
|
||||
|
||||
let ss = n
|
||||
.checked_mul(m)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | n * m",
|
||||
})?
|
||||
.checked_mul(
|
||||
CHUNK_SIZE
|
||||
.checked_sub(1)
|
||||
.ok_or(DkgError::ArithmeticUnderflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1)",
|
||||
})?
|
||||
.checked_mul(ee.checked_sub(1).ok_or(DkgError::ArithmeticUnderflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | (ee - 1)",
|
||||
})?)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1) * (ee - 1)",
|
||||
})?,
|
||||
)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | ss_lhs * ss_rhs",
|
||||
})? as u64;
|
||||
|
||||
// let zz = 2 * PARALLEL_RUNS as u64 * ss;
|
||||
// Z = 2 * l * S
|
||||
|
||||
let zz = 2u64
|
||||
.checked_mul(PARALLEL_RUNS as u64)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | 2 * l",
|
||||
})?
|
||||
.checked_mul(ss)
|
||||
.ok_or(DkgError::ArithmeticOverflow {
|
||||
info: "ProofOfChunking::compute_ss_zz | (2 * l) * S",
|
||||
})?;
|
||||
|
||||
Ok((ss, zz))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -652,7 +720,9 @@ mod tests {
|
||||
ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>,
|
||||
}
|
||||
|
||||
fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
|
||||
fn setup(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
|
||||
let g1 = G1Projective::generator();
|
||||
|
||||
let mut pks = Vec::with_capacity(NODES);
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::utils::hash_to_scalar;
|
||||
use bls12_381::{G1Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -20,7 +21,11 @@ pub struct ProofOfDiscreteLog {
|
||||
}
|
||||
|
||||
impl ProofOfDiscreteLog {
|
||||
pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self {
|
||||
pub fn construct(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
public: &G1Projective,
|
||||
witness: &Scalar,
|
||||
) -> Self {
|
||||
let mut rand_x = Scalar::random(&mut rng);
|
||||
let rand_commitment = G1Projective::generator() * rand_x;
|
||||
let challenge = Self::compute_challenge(public, &rand_commitment);
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{NodeIndex, Share};
|
||||
use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -87,7 +88,7 @@ pub struct ProofOfSecretSharing {
|
||||
|
||||
impl ProofOfSecretSharing {
|
||||
pub fn construct(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
instance: Instance,
|
||||
witness_r: &Scalar,
|
||||
witnesses_s: &[Share],
|
||||
@@ -309,13 +310,14 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::interpolation::polynomial::Polynomial;
|
||||
use group::Group;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::SeedableRng;
|
||||
|
||||
const NODES: u64 = 50;
|
||||
const THRESHOLD: u64 = 40;
|
||||
|
||||
fn setup(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (
|
||||
BTreeMap<NodeIndex, PublicKey>,
|
||||
PublicCoefficients,
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::utils::deserialize_g2;
|
||||
use crate::{NodeIndex, Share, Threshold};
|
||||
use bls12_381::{G2Projective, Scalar};
|
||||
use group::GroupEncoding;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use std::collections::BTreeMap;
|
||||
use zeroize::Zeroize;
|
||||
@@ -94,7 +95,7 @@ impl Dealing {
|
||||
// I'm not a big fan of this function signature, but I'm not clear on how to improve it while
|
||||
// allowing the dealer to skip decryption of its own share if it was also one of the receivers
|
||||
pub fn create(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng + CryptoRng,
|
||||
params: &Params,
|
||||
dealer_index: NodeIndex,
|
||||
threshold: Threshold,
|
||||
@@ -484,7 +485,7 @@ mod tests {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
derived_secrets.push(
|
||||
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(),
|
||||
@@ -593,7 +594,7 @@ mod tests {
|
||||
for (i, (dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret = combine_shares(shares, &dealer_indices).unwrap();
|
||||
|
||||
@@ -99,6 +99,12 @@ pub enum DkgError {
|
||||
"The reshared dealing has different public constant coefficient than its prior variant"
|
||||
)]
|
||||
InvalidResharing,
|
||||
|
||||
#[error("Arithmetic Overflow: {info}")]
|
||||
ArithmeticOverflow { info: &'static str },
|
||||
|
||||
#[error("Arithmetic Underflow: {info}")]
|
||||
ArithmeticUnderflow { info: &'static str },
|
||||
}
|
||||
|
||||
impl DkgError {
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::utils::deserialize_g2;
|
||||
use bls12_381::{G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use rand::CryptoRng;
|
||||
use rand_core::RngCore;
|
||||
use std::ops::{Add, Index, IndexMut};
|
||||
use zeroize::Zeroize;
|
||||
@@ -120,7 +121,7 @@ impl Polynomial {
|
||||
// for polynomial of degree n, we generate n+1 values
|
||||
// (for example for degree 1, like y = x + 2, we need [2,1])
|
||||
/// Creates new pseudorandom polynomial of specified degree.
|
||||
pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self {
|
||||
pub fn new_random(mut rng: impl RngCore + CryptoRng + CryptoRng, degree: u64) -> Self {
|
||||
Polynomial {
|
||||
coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(),
|
||||
}
|
||||
|
||||
@@ -53,11 +53,12 @@ fn single_sender() {
|
||||
|
||||
// make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check)
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap();
|
||||
let _recovered = decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap();
|
||||
}
|
||||
|
||||
// and for good measure, check that the dealer's share matches decryption result
|
||||
let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
|
||||
let recovered_dealer =
|
||||
decrypt_share(¶ms, &full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
|
||||
assert_eq!(recovered_dealer, dealer_share.unwrap());
|
||||
}
|
||||
|
||||
@@ -115,7 +116,7 @@ fn full_threshold_secret_sharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
// we know dealer_share matches, but it would be inconvenient to try to put them in here,
|
||||
@@ -189,7 +190,7 @@ fn full_threshold_secret_resharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -240,7 +241,7 @@ fn full_threshold_secret_resharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -305,7 +306,7 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -369,7 +370,7 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret = combine_shares(shares, &node_indices).unwrap();
|
||||
|
||||
@@ -33,8 +33,8 @@ impl PersistentStatsStorage {
|
||||
/// * `database_path`: path to the database.
|
||||
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -82,8 +82,8 @@ impl GatewayStorage {
|
||||
message_retrieval_limit: i64,
|
||||
) -> Result<Self, GatewayStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -23,7 +23,6 @@ fn main() {
|
||||
"REWARDING_VALIDATOR_ADDRESS",
|
||||
"NYM_API",
|
||||
"NYXD_WS",
|
||||
"EXPLORER_API",
|
||||
"NYM_VPN_API",
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(feature = "network")]
|
||||
use crate::{DenomDetails, ValidatorDetails};
|
||||
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
|
||||
|
||||
pub const NETWORK_NAME: &str = "mainnet";
|
||||
|
||||
@@ -17,6 +17,11 @@ pub const MIXNET_CONTRACT_ADDRESS: &str =
|
||||
"n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr";
|
||||
pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
@@ -29,10 +34,37 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str =
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
|
||||
|
||||
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
|
||||
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_API,
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-fronntdoor.vercel.app/api/",
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
];
|
||||
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_VPN_API,
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
];
|
||||
|
||||
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
|
||||
pub const EXIT_POLICY_URL: &str =
|
||||
@@ -123,7 +155,6 @@ pub fn export_to_env() {
|
||||
set_var_to_default(var_names::NYXD, NYXD_URL);
|
||||
set_var_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_to_default(var_names::EXPLORER_API, EXPLORER_API);
|
||||
set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
|
||||
}
|
||||
@@ -165,6 +196,5 @@ pub fn export_to_env_if_not_set() {
|
||||
set_var_conditionally_to_default(var_names::NYXD, NYXD_URL);
|
||||
set_var_conditionally_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API);
|
||||
set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ pub struct ChainDetails {
|
||||
pub struct NymContracts {
|
||||
pub mixnet_contract_address: Option<String>,
|
||||
pub vesting_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
@@ -35,8 +37,38 @@ pub struct NymNetworkDetails {
|
||||
pub chain_details: ChainDetails,
|
||||
pub endpoints: Vec<ValidatorDetails>,
|
||||
pub contracts: NymContracts,
|
||||
pub explorer_api: Option<String>,
|
||||
pub nym_vpn_api_url: Option<String>,
|
||||
pub nym_api_urls: Option<Vec<ApiUrl>>,
|
||||
pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct ApiUrl {
|
||||
/// Expects a string formatted Url
|
||||
///
|
||||
/// see https://docs.rs/url/latest/url/struct.Url.html
|
||||
pub url: String,
|
||||
/// Optional alternative equivalent hostnames. Each entry must parse as valid Host
|
||||
///
|
||||
/// see https://docs.rs/url/latest/url/enum.Host.html
|
||||
pub front_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub struct ApiUrlConst<'a> {
|
||||
pub url: &'a str,
|
||||
pub front_hosts: Option<&'a [&'a str]>,
|
||||
}
|
||||
|
||||
impl From<ApiUrlConst<'_>> for ApiUrl {
|
||||
fn from(value: ApiUrlConst) -> Self {
|
||||
ApiUrl {
|
||||
url: value.url.to_string(),
|
||||
front_hosts: value
|
||||
.front_hosts
|
||||
.map(|slice| slice.iter().map(|s| s.to_string()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
|
||||
@@ -65,8 +97,9 @@ impl NymNetworkDetails {
|
||||
},
|
||||
endpoints: Default::default(),
|
||||
contracts: Default::default(),
|
||||
explorer_api: Default::default(),
|
||||
nym_vpn_api_url: Default::default(),
|
||||
nym_api_urls: Default::default(),
|
||||
nym_vpn_api_urls: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +157,6 @@ impl NymNetworkDetails {
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
|
||||
.with_explorer_api(get_optional_env(var_names::EXPLORER_API))
|
||||
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
|
||||
}
|
||||
|
||||
@@ -145,6 +177,9 @@ impl NymNetworkDetails {
|
||||
contracts: NymContracts {
|
||||
mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS),
|
||||
vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS),
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
||||
@@ -152,8 +187,9 @@ impl NymNetworkDetails {
|
||||
mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
),
|
||||
},
|
||||
explorer_api: parse_optional_str(mainnet::EXPLORER_API),
|
||||
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
|
||||
nym_api_urls: None,
|
||||
nym_vpn_api_urls: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +229,6 @@ impl NymNetworkDetails {
|
||||
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
||||
set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address);
|
||||
|
||||
set_optional_var(var_names::EXPLORER_API, self.explorer_api);
|
||||
set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url);
|
||||
}
|
||||
|
||||
@@ -297,12 +332,6 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_explorer_api<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.explorer_api = endpoint.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.nym_vpn_api_url = endpoint.map(Into::into);
|
||||
|
||||
@@ -22,7 +22,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
pub const NYM_API: &str = "NYM_API";
|
||||
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
|
||||
pub const EXPLORER_API: &str = "EXPLORER_API";
|
||||
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
|
||||
pub const NYM_VPN_API: &str = "NYM_VPN_API";
|
||||
pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER";
|
||||
|
||||
Generated
+50
-7
@@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.97"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "ark-bls12-381"
|
||||
@@ -1133,6 +1133,19 @@ dependencies = [
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "0.4.0"
|
||||
@@ -1214,7 +1227,9 @@ dependencies = [
|
||||
"cw2",
|
||||
"easy-addr",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"rand",
|
||||
@@ -1238,7 +1253,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde_repr",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -1276,6 +1290,38 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-performance-contract-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"nym-contracts-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract"
|
||||
version = "0.1.0"
|
||||
@@ -1284,14 +1330,11 @@ dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-pool-contract-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"multisig/cw3-flex-multisig",
|
||||
"multisig/cw4-group",
|
||||
"vesting",
|
||||
"performance",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -64,4 +65,4 @@ dbg_macro = "deny"
|
||||
exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
@@ -26,13 +26,13 @@ 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.6.0" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.7.0" }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" }
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
|
||||
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
|
||||
cw-controllers = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
@@ -41,16 +41,22 @@ bs58 = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
semver = { workspace = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
rand_chacha = "0.3"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
|
||||
# activate the `testable-mixnet-contract` in tests (weird workaround, but it does the trick)
|
||||
nym-mixnet-contract = { path = ".", features = ["testable-mixnet-contract"] }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
contract-testing = ["mixnet-contract-common/contract-testing"]
|
||||
testable-mixnet-contract = ["nym-contracts-common-testing"]
|
||||
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -649,7 +649,7 @@ pub fn migrate(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
|
||||
use cosmwasm_std::testing::{message_info, mock_env};
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
use mixnet_contract_common::reward_params::{
|
||||
IntervalRewardParams, RewardedSetParams, RewardingParams,
|
||||
@@ -657,6 +657,7 @@ mod tests {
|
||||
use mixnet_contract_common::{
|
||||
InitialRewardingParams, OperatingCostRange, Percent, ProfitMarginRange,
|
||||
};
|
||||
use nym_contracts_common_testing::mock_dependencies;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,3 +22,6 @@ mod support;
|
||||
#[cfg(feature = "contract-testing")]
|
||||
mod testing;
|
||||
mod vesting_migration;
|
||||
|
||||
#[cfg(feature = "testable-mixnet-contract")]
|
||||
pub mod testable_mixnet_contract;
|
||||
|
||||
@@ -130,20 +130,22 @@ pub mod tests {
|
||||
use crate::mixnet_contract_settings::queries::query_rewarding_validator_address;
|
||||
use crate::mixnet_contract_settings::storage::rewarding_denom;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::testing::{message_info, MockApi};
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::{Coin, Uint128};
|
||||
use cw_controllers::AdminError::NotAdmin;
|
||||
use mixnet_contract_common::OperatorsParamsUpdate;
|
||||
use nym_contracts_common_testing::mock_api;
|
||||
|
||||
#[test]
|
||||
fn update_contract_rewarding_validator_address() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mock_api = mock_api();
|
||||
|
||||
let info = message_info(&deps.api.addr_make("not-the-creator"), &[]);
|
||||
let res = try_update_rewarding_validator_address(
|
||||
deps.as_mut(),
|
||||
info,
|
||||
MockApi::default().addr_make("not-the-creator").to_string(),
|
||||
mock_api.addr_make("not-the-creator").to_string(),
|
||||
);
|
||||
assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {})));
|
||||
|
||||
@@ -151,14 +153,14 @@ pub mod tests {
|
||||
let res = try_update_rewarding_validator_address(
|
||||
deps.as_mut(),
|
||||
info,
|
||||
MockApi::default().addr_make("new-good-address").to_string(),
|
||||
mock_api.addr_make("new-good-address").to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Ok(
|
||||
Response::default().add_event(new_rewarding_validator_address_update_event(
|
||||
MockApi::default().addr_make("rewarder"),
|
||||
MockApi::default().addr_make("new-good-address")
|
||||
mock_api.addr_make("rewarder"),
|
||||
mock_api.addr_make("new-good-address")
|
||||
))
|
||||
)
|
||||
);
|
||||
@@ -166,7 +168,7 @@ pub mod tests {
|
||||
let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap();
|
||||
assert_eq!(
|
||||
state.rewarding_validator_address,
|
||||
MockApi::default().addr_make("new-good-address")
|
||||
mock_api.addr_make("new-good-address")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -51,11 +51,11 @@ pub mod test_helpers {
|
||||
use crate::support::helpers::ensure_no_existing_bond;
|
||||
use crate::support::tests;
|
||||
use crate::support::tests::fixtures::{
|
||||
good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM,
|
||||
good_gateway_pledge, good_mixnode_pledge, good_node_plegge,
|
||||
};
|
||||
use crate::support::tests::{legacy, test_helpers};
|
||||
use crate::testable_mixnet_contract::MixnetContract;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
@@ -74,22 +74,24 @@ pub mod test_helpers {
|
||||
use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role};
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
|
||||
use mixnet_contract_common::reward_params::{
|
||||
NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor,
|
||||
NodeRewardingParameters, Performance, RewardingParams, WorkFactor,
|
||||
};
|
||||
use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode;
|
||||
use mixnet_contract_common::rewarding::simulator::Simulator;
|
||||
use mixnet_contract_common::rewarding::RewardDistribution;
|
||||
use mixnet_contract_common::{
|
||||
ContractStateParamsUpdate, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg,
|
||||
Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg,
|
||||
Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode,
|
||||
NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange,
|
||||
OperatorsParamsUpdate, Percent, ProfitMarginRange, RoleAssignment,
|
||||
SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg,
|
||||
Gateway, GatewayBondingPayload, IdentityKey, Interval, MixNode, MixNodeBond,
|
||||
MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, NymNodeBondingPayload,
|
||||
NymNodeDetails, OperatingCostRange, OperatorsParamsUpdate, ProfitMarginRange,
|
||||
RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg,
|
||||
SignableNymNodeBondingMsg,
|
||||
};
|
||||
use nym_contracts_common::signing::{
|
||||
ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose,
|
||||
};
|
||||
use nym_contracts_common_testing::TestableNymContract;
|
||||
use nym_contracts_common_testing::{mock_api, mock_dependencies};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_crypto::asymmetric::ed25519::KeyPair;
|
||||
use rand::distributions::WeightedIndex;
|
||||
@@ -100,13 +102,12 @@ pub mod test_helpers {
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) fn sorted_addresses(n: usize) -> Vec<Addr> {
|
||||
let mut rng = test_rng();
|
||||
let mut addrs = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
addrs.push(MockApi::default().addr_make(&format!("addr{i}{}", rng.next_u64())));
|
||||
addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64())));
|
||||
}
|
||||
addrs.sort();
|
||||
addrs
|
||||
@@ -1820,46 +1821,9 @@ pub mod test_helpers {
|
||||
SignableGatewayBondingMsg::new(nonce, content)
|
||||
}
|
||||
|
||||
fn intial_rewarded_set_params() -> RewardedSetParams {
|
||||
RewardedSetParams {
|
||||
entry_gateways: 50,
|
||||
exit_gateways: 70,
|
||||
mixnodes: 120,
|
||||
standby: 50,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_rewarding_params() -> InitialRewardingParams {
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
|
||||
InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
||||
rewarded_set_params: intial_rewarded_set_params(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
|
||||
rewarding_denom: TEST_COIN_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
initial_rewarding_params: initial_rewarding_params(),
|
||||
current_nym_node_version: "1.1.10".to_string(),
|
||||
version_score_weights: Default::default(),
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
};
|
||||
let msg = MixnetContract::base_init_msg();
|
||||
let env = mock_env();
|
||||
let info = sender("creator");
|
||||
instantiate(deps.as_mut(), env, info, msg).unwrap();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// fine in test code
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use cosmwasm_std::Decimal;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::reward_params::RewardedSetParams;
|
||||
use mixnet_contract_common::{
|
||||
ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg,
|
||||
};
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_contracts_common_testing::{
|
||||
mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
|
||||
pub struct MixnetContract;
|
||||
|
||||
fn initial_rewarded_set_params() -> RewardedSetParams {
|
||||
RewardedSetParams {
|
||||
entry_gateways: 50,
|
||||
exit_gateways: 70,
|
||||
mixnodes: 120,
|
||||
standby: 50,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_rewarding_params() -> InitialRewardingParams {
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
|
||||
InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
||||
rewarded_set_params: initial_rewarded_set_params(),
|
||||
}
|
||||
}
|
||||
|
||||
impl TestableNymContract for MixnetContract {
|
||||
const NAME: &'static str = "mixnet-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = MixnetContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
query
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
let deps = mock_dependencies();
|
||||
InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
|
||||
rewarding_denom: TEST_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
initial_rewarding_params: initial_rewarding_params(),
|
||||
current_nym_node_version: "1.1.10".to_string(),
|
||||
version_score_weights: Default::default(),
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,9 @@ cosmwasm-schema = { workspace = true, optional = true }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-pool-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-pool-contract" }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
cw-multi-test = { workspace = true }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-pool-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
@@ -193,8 +193,8 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod setting_initial_grants {
|
||||
use super::*;
|
||||
use crate::testing::deps_with_balance;
|
||||
use cosmwasm_std::{coin, Order, Storage};
|
||||
use nym_contracts_common_testing::deps_with_balance;
|
||||
use nym_pool_contract_common::{Allowance, BasicAllowance, Grant, GranteeAddress};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
@@ -26,12 +26,13 @@ pub fn validate_usage_coin(storage: &dyn Storage, coin: &Coin) -> Result<(), Nym
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::storage::NymPoolStorage;
|
||||
use crate::testing::TestSetup;
|
||||
use crate::testing::init_contract_tester;
|
||||
use cosmwasm_std::coin;
|
||||
use nym_contracts_common_testing::ContractOpts;
|
||||
|
||||
#[test]
|
||||
fn validating_coin_usage() -> anyhow::Result<()> {
|
||||
let test = TestSetup::init();
|
||||
let test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let denom = storage.pool_denomination.load(test.storage())?;
|
||||
|
||||
|
||||
@@ -182,20 +182,22 @@ pub fn query_granters_paged(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::contract::instantiate;
|
||||
use crate::testing::{TestSetup, TEST_DENOM};
|
||||
use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM};
|
||||
use cosmwasm_std::testing::{message_info, mock_dependencies_with_balance, mock_env};
|
||||
use cosmwasm_std::{coin, Uint128};
|
||||
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt};
|
||||
use nym_pool_contract_common::{Allowance, BasicAllowance, GranterInformation, InstantiateMsg};
|
||||
|
||||
#[cfg(test)]
|
||||
mod admin_query {
|
||||
use super::*;
|
||||
use crate::testing::TestSetup;
|
||||
use crate::testing::init_contract_tester;
|
||||
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt};
|
||||
use nym_pool_contract_common::ExecuteMsg;
|
||||
|
||||
#[test]
|
||||
fn returns_current_admin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let initial_admin = test.admin_unchecked();
|
||||
|
||||
@@ -255,7 +257,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn total_locked_tokens_query() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let locked = query_total_locked_tokens(test.deps()).unwrap().locked;
|
||||
assert!(locked.amount.is_zero());
|
||||
@@ -271,7 +273,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn locked_tokens_query() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let grantee1 = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(grantee1.as_str(), Uint128::new(1234));
|
||||
@@ -295,8 +297,13 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod locked_tokens_paged_query {
|
||||
use super::*;
|
||||
use crate::testing::NymPoolContract;
|
||||
use nym_contracts_common_testing::ContractTester;
|
||||
|
||||
fn lock_sorted(test: &mut TestSetup, count: usize) -> Vec<LockedTokens> {
|
||||
fn lock_sorted(
|
||||
test: &mut ContractTester<NymPoolContract>,
|
||||
count: usize,
|
||||
) -> Vec<LockedTokens> {
|
||||
let mut grantees = Vec::new();
|
||||
|
||||
for _ in 0..count {
|
||||
@@ -314,7 +321,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn obeys_limits() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _locked = lock_sorted(&mut test, 1000);
|
||||
|
||||
let limit = 42;
|
||||
@@ -324,7 +331,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn has_default_limit() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _locked = lock_sorted(&mut test, 1000);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
@@ -337,7 +344,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn has_max_limit() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _locked = lock_sorted(&mut test, 1000);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
@@ -352,7 +359,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let locked = lock_sorted(&mut test, 1000);
|
||||
|
||||
// first page should return 2 results...
|
||||
@@ -371,7 +378,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn grant_query() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let env = test.env();
|
||||
|
||||
// bad address
|
||||
@@ -433,7 +440,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn granter_query() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let admin = test.admin_unchecked();
|
||||
let env = test.env();
|
||||
|
||||
@@ -482,8 +489,13 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod granters_paged_query {
|
||||
use super::*;
|
||||
use crate::testing::NymPoolContract;
|
||||
use nym_contracts_common_testing::ContractTester;
|
||||
|
||||
fn granters_sorted(test: &mut TestSetup, count: usize) -> Vec<GranterDetails> {
|
||||
fn granters_sorted(
|
||||
test: &mut ContractTester<NymPoolContract>,
|
||||
count: usize,
|
||||
) -> Vec<GranterDetails> {
|
||||
let mut granters = Vec::new();
|
||||
|
||||
for _ in 0..count {
|
||||
@@ -504,7 +516,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn obeys_limits() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _granters = granters_sorted(&mut test, 1000);
|
||||
|
||||
let limit = 42;
|
||||
@@ -514,7 +526,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn has_default_limit() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _granters = granters_sorted(&mut test, 1000);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
@@ -527,7 +539,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn has_max_limit() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _granters = granters_sorted(&mut test, 1000);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
@@ -542,7 +554,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let locked = granters_sorted(&mut test, 1000);
|
||||
|
||||
// first page should return 2 results...
|
||||
@@ -562,8 +574,13 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod grants_paged_query {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, NymPoolContract};
|
||||
use nym_contracts_common_testing::{ContractOpts, ContractTester};
|
||||
|
||||
fn grants_sorted(test: &mut TestSetup, count: usize) -> Vec<GrantInformation> {
|
||||
fn grants_sorted(
|
||||
test: &mut ContractTester<NymPoolContract>,
|
||||
count: usize,
|
||||
) -> Vec<GrantInformation> {
|
||||
let mut grantees = Vec::new();
|
||||
|
||||
for _ in 0..count {
|
||||
@@ -580,7 +597,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn obeys_limits() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _grantees = grants_sorted(&mut test, 1000);
|
||||
|
||||
let limit = 42;
|
||||
@@ -590,7 +607,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn has_default_limit() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _grantees = grants_sorted(&mut test, 1000);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
@@ -603,7 +620,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn has_max_limit() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let _grantees = grants_sorted(&mut test, 1000);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
@@ -619,7 +636,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grants = grants_sorted(&mut test, 1000);
|
||||
|
||||
// first page should return 2 results...
|
||||
|
||||
@@ -489,19 +489,21 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod nympool_storage {
|
||||
use super::*;
|
||||
use crate::testing::{TestSetup, TEST_DENOM};
|
||||
use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM};
|
||||
use cosmwasm_std::testing::{
|
||||
mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage,
|
||||
};
|
||||
use cosmwasm_std::{coin, coins, Empty, OwnedDeps};
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
|
||||
use nym_pool_contract_common::BasicAllowance;
|
||||
|
||||
#[cfg(test)]
|
||||
mod initialisation {
|
||||
use super::*;
|
||||
use crate::testing::{deps_with_balance, TEST_DENOM};
|
||||
use crate::testing::TEST_DENOM;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env};
|
||||
use cosmwasm_std::{coin, Order};
|
||||
use nym_contracts_common_testing::deps_with_balance;
|
||||
use nym_pool_contract_common::BasicAllowance;
|
||||
|
||||
fn all_grants(storage: &dyn Storage) -> HashMap<GranteeAddress, Grant> {
|
||||
@@ -914,7 +916,7 @@ mod tests {
|
||||
#[test]
|
||||
fn loading_granter_information() -> anyhow::Result<()> {
|
||||
let storage = NymPoolStorage::new();
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let granter = test.generate_account();
|
||||
|
||||
@@ -941,7 +943,7 @@ mod tests {
|
||||
#[test]
|
||||
fn checking_granter_permission() -> anyhow::Result<()> {
|
||||
let storage = NymPoolStorage::new();
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let granter = test.generate_account();
|
||||
test.add_granter(&granter);
|
||||
@@ -957,7 +959,7 @@ mod tests {
|
||||
#[test]
|
||||
fn ensuring_granter_permission() -> anyhow::Result<()> {
|
||||
let storage = NymPoolStorage::new();
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let granter = test.generate_account();
|
||||
test.add_granter(&granter);
|
||||
@@ -1047,7 +1049,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn attempting_to_load_grant() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
// doesn't exist...
|
||||
@@ -1070,7 +1072,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn loading_grant() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
// doesn't exist...
|
||||
@@ -1094,11 +1096,12 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod adding_new_granter {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
use cw_controllers::AdminError;
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1121,7 +1124,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_if_account_is_not_already_a_granter() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1143,7 +1146,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn saves_basic_metadata() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1194,10 +1197,11 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod removing_granter {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
|
||||
#[test]
|
||||
fn requires_granter_to_exist() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1217,7 +1221,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_by_admin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let random_address = test.generate_account();
|
||||
@@ -1259,7 +1263,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn removes_it_from_granter_list() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1284,11 +1288,12 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod adding_new_grant {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
use nym_pool_contract_common::ClassicPeriodicAllowance;
|
||||
|
||||
#[test]
|
||||
fn can_only_be_done_by_whitelisted_granter() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let not_valid_granter = test.generate_account();
|
||||
@@ -1319,7 +1324,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cant_be_done_if_grant_already_existed() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1340,7 +1345,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn only_accepts_valid_allowances() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
// allowance with 0 limit and wrong denom
|
||||
@@ -1364,7 +1369,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn explicit_limit_cant_be_larger_than_available_tokens() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1401,7 +1406,7 @@ mod tests {
|
||||
assert!(res.is_ok());
|
||||
|
||||
// and below the available
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let mut limit = available.clone();
|
||||
limit.amount -= Uint128::new(1);
|
||||
let allowance = Allowance::Basic(BasicAllowance {
|
||||
@@ -1418,7 +1423,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn updates_allowances_initial_state_and_saves_it_to_storage() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1456,10 +1461,11 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod spending_part_of_grant {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_exist() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let grantee = test.generate_account();
|
||||
@@ -1485,7 +1491,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_be_spendable() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1514,7 +1520,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn updates_stored_grant() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1548,7 +1554,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn removes_grant_from_storage_if_its_used_up() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1612,7 +1618,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn removing_grant() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let grantee = test.generate_account();
|
||||
@@ -1656,10 +1662,11 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod revoking_grant {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_exist() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1684,7 +1691,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn can_always_be_called_by_current_admin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
@@ -1717,7 +1724,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn can_be_called_by_original_granter_if_its_still_whitelisted() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1761,7 +1768,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn removes_the_underlying_grant() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
let admin = test.admin_unchecked();
|
||||
@@ -1780,10 +1787,12 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod locking_part_of_allowance {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
use nym_contracts_common_testing::DenomExt;
|
||||
|
||||
#[test]
|
||||
fn requires_providing_valid_coin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
@@ -1804,7 +1813,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_exist() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let grantee = test.generate_account();
|
||||
let env = test.env();
|
||||
@@ -1826,7 +1835,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn does_not_allow_locking_more_than_spend_limit() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let admin = test.admin_unchecked();
|
||||
let env = test.env();
|
||||
@@ -1853,7 +1862,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn deducts_locked_amount_from_the_allowance() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let admin = test.admin_unchecked();
|
||||
let env = test.env();
|
||||
@@ -1892,7 +1901,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn preserves_grant_even_if_resultant_allowance_is_zero() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let admin = test.admin_unchecked();
|
||||
let env = test.env();
|
||||
@@ -1918,7 +1927,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn updates_internal_locked_counter() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let env = test.env();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
@@ -1953,8 +1962,10 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod unlocking_part_of_allowance {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, NymPoolContract};
|
||||
use nym_contracts_common_testing::{ContractTester, DenomExt};
|
||||
|
||||
fn setup_locked_grant(test: &mut TestSetup) -> Addr {
|
||||
fn setup_locked_grant(test: &mut ContractTester<NymPoolContract>) -> Addr {
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(100));
|
||||
grantee
|
||||
@@ -1962,7 +1973,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_providing_valid_coin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let grantee = setup_locked_grant(&mut test);
|
||||
|
||||
@@ -1981,7 +1992,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn does_not_allow_unlocking_more_than_currently_locked() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let grantee = setup_locked_grant(&mut test);
|
||||
|
||||
@@ -1999,7 +2010,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_exist() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let grantee = test.generate_account();
|
||||
|
||||
@@ -2018,7 +2029,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_having_locked_coins() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
@@ -2036,7 +2047,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn increases_internal_grant_spend_limit() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
let admin = test.admin_unchecked();
|
||||
let env = test.env();
|
||||
@@ -2082,7 +2093,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn updates_internal_locked_counter() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = NymPoolStorage::new();
|
||||
|
||||
// 100tokens locked
|
||||
@@ -2116,8 +2127,9 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod locked_storage {
|
||||
use super::*;
|
||||
use crate::testing::TestSetup;
|
||||
use crate::testing::{init_contract_tester, NymPoolContractTesterExt};
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use nym_contracts_common_testing::{ContractOpts, RandExt};
|
||||
|
||||
#[test]
|
||||
fn is_initialised_with_zero_total_locked() -> anyhow::Result<()> {
|
||||
@@ -2136,7 +2148,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn getting_grantee_locked() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.generate_account();
|
||||
|
||||
let storage = LockedStorage::new();
|
||||
@@ -2167,7 +2179,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn getting_maybe_grantee_locked() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.generate_account();
|
||||
|
||||
let storage = LockedStorage::new();
|
||||
@@ -2198,7 +2210,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn locking_tokens() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = LockedStorage::new();
|
||||
|
||||
let grantee1 = test.generate_account();
|
||||
@@ -2259,7 +2271,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unlocking_tokens() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let storage = LockedStorage::new();
|
||||
|
||||
let grantee1 = test.generate_account();
|
||||
|
||||
@@ -1,226 +1,70 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract;
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use crate::storage::NYM_POOL_STORAGE;
|
||||
use crate::testing::storage::{ContractStorageWrapper, StorageWrapper};
|
||||
use cosmwasm_std::testing::{message_info, mock_env, MockApi, MockQuerier, MockStorage};
|
||||
use cosmwasm_std::{
|
||||
coin, coins, Addr, Coin, ContractInfo, Deps, DepsMut, Empty, Env, MemoryStorage, MessageInfo,
|
||||
Order, OwnedDeps, Response, StdResult, Storage, Uint128,
|
||||
};
|
||||
use cw_multi_test::{
|
||||
next_block, App, AppBuilder, AppResponse, BankKeeper, Contract, ContractWrapper, Executor,
|
||||
use cosmwasm_std::{Addr, Order, Uint128};
|
||||
use nym_contracts_common_testing::{
|
||||
AdminExt, ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractTester, DenomExt,
|
||||
PermissionedFn, QueryFn, RandExt, TestableNymContract,
|
||||
};
|
||||
use nym_pool_contract_common::constants::storage_keys;
|
||||
use nym_pool_contract_common::{
|
||||
Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, NymPoolContractError, QueryMsg,
|
||||
Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, MigrateMsg, NymPoolContractError,
|
||||
QueryMsg,
|
||||
};
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod storage;
|
||||
pub use nym_contracts_common_testing::TEST_DENOM;
|
||||
|
||||
pub fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
pub struct NymPoolContract;
|
||||
|
||||
pub fn deps_with_balance(env: &Env) -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
OwnedDeps {
|
||||
storage: MockStorage::default(),
|
||||
api: MockApi::default(),
|
||||
querier: MockQuerier::<Empty>::new(&[(
|
||||
env.contract.address.as_str(),
|
||||
coins(100000000000, TEST_DENOM).as_slice(),
|
||||
)]),
|
||||
custom_query_type: Default::default(),
|
||||
impl TestableNymContract for NymPoolContract {
|
||||
const NAME: &'static str = "nym-pool-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = NymPoolContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
query
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
InstantiateMsg {
|
||||
pool_denomination: TEST_DENOM.to_string(),
|
||||
grants: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const TEST_DENOM: &str = "unym";
|
||||
|
||||
pub struct TestSetup {
|
||||
pub app: App<BankKeeper, MockApi, StorageWrapper>,
|
||||
pub rng: ChaCha20Rng,
|
||||
pub contract_address: Addr,
|
||||
pub master_address: Addr,
|
||||
pub(crate) storage: ContractStorageWrapper,
|
||||
pub fn init_contract_tester() -> ContractTester<NymPoolContract> {
|
||||
NymPoolContract::init()
|
||||
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
|
||||
.with_common_storage_key(CommonStorageKeys::Denom, storage_keys::POOL_DENOMINATION)
|
||||
}
|
||||
|
||||
pub fn contract() -> Box<dyn Contract<Empty>> {
|
||||
let contract = ContractWrapper::new(execute, instantiate, query).with_migrate(migrate);
|
||||
Box::new(contract)
|
||||
}
|
||||
|
||||
impl TestSetup {
|
||||
pub fn init() -> TestSetup {
|
||||
let storage = StorageWrapper::new();
|
||||
|
||||
let api = MockApi::default().with_prefix("n");
|
||||
let master_address = api.addr_make("master-owner");
|
||||
|
||||
let mut app = AppBuilder::new()
|
||||
.with_api(api)
|
||||
.with_storage(storage.clone())
|
||||
.build(|router, _api, storage| {
|
||||
router
|
||||
.bank
|
||||
.init_balance(
|
||||
storage,
|
||||
&master_address,
|
||||
coins(1000000000000000, TEST_DENOM),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
let code_id = app.store_code(contract());
|
||||
let contract_address = app
|
||||
.instantiate_contract(
|
||||
code_id,
|
||||
master_address.clone(),
|
||||
&InstantiateMsg {
|
||||
pool_denomination: TEST_DENOM.to_string(),
|
||||
grants: Default::default(),
|
||||
},
|
||||
&[],
|
||||
"nym-pool-contract",
|
||||
Some(master_address.to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// send some tokens to the contract
|
||||
app.send_tokens(
|
||||
master_address.clone(),
|
||||
contract_address.clone(),
|
||||
&[coin(100000000, TEST_DENOM)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
TestSetup {
|
||||
app,
|
||||
rng: test_rng(),
|
||||
storage: storage.contract_storage_wrapper(&contract_address),
|
||||
contract_address,
|
||||
master_address,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_contract_balance(&mut self, balance: Coin) {
|
||||
let contract_address = &self.contract_address;
|
||||
self.app
|
||||
.router()
|
||||
.bank
|
||||
.init_balance(
|
||||
&mut self.storage.inner_storage(),
|
||||
contract_address,
|
||||
vec![balance],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> Deps<'_> {
|
||||
Deps {
|
||||
storage: &self.storage,
|
||||
api: self.app.api(),
|
||||
querier: self.app.wrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deps_mut(&mut self) -> DepsMut<'_> {
|
||||
DepsMut {
|
||||
storage: &mut self.storage,
|
||||
api: self.app.api(),
|
||||
querier: self.app.wrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
|
||||
let env = self.env().clone();
|
||||
(self.deps_mut(), env)
|
||||
}
|
||||
|
||||
pub fn storage(&self) -> &dyn Storage {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
pub fn storage_mut(&mut self) -> &mut dyn Storage {
|
||||
&mut self.storage
|
||||
}
|
||||
|
||||
pub fn env(&self) -> Env {
|
||||
Env {
|
||||
block: self.app.block_info(),
|
||||
contract: ContractInfo {
|
||||
address: self.contract_address.clone(),
|
||||
},
|
||||
..mock_env()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_block(&mut self) {
|
||||
self.app.update_block(next_block)
|
||||
}
|
||||
|
||||
pub fn execute_raw(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
message: ExecuteMsg,
|
||||
) -> Result<Response, NymPoolContractError> {
|
||||
self.execute_raw_with_balance(sender, &[], message)
|
||||
}
|
||||
|
||||
pub fn execute_raw_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: ExecuteMsg,
|
||||
) -> Result<Response, NymPoolContractError> {
|
||||
let env = self.env();
|
||||
let info = message_info(&sender, coins);
|
||||
contract::execute(self.deps_mut(), env, info, message)
|
||||
}
|
||||
|
||||
pub fn execute_msg(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
message: &ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.execute_msg_with_balance(sender, &[], message)
|
||||
}
|
||||
|
||||
pub fn execute_msg_with_balance(
|
||||
&mut self,
|
||||
sender: Addr,
|
||||
coins: &[Coin],
|
||||
message: &ExecuteMsg,
|
||||
) -> anyhow::Result<AppResponse> {
|
||||
self.app
|
||||
.execute_contract(sender, self.contract_address.clone(), message, coins)
|
||||
}
|
||||
|
||||
pub fn query<T: DeserializeOwned>(&self, message: &QueryMsg) -> StdResult<T> {
|
||||
self.app
|
||||
.wrap()
|
||||
.query_wasm_smart(self.contract_address.as_str(), message)
|
||||
}
|
||||
|
||||
pub fn generate_account(&mut self) -> Addr {
|
||||
self.app
|
||||
.api()
|
||||
.addr_make(&format!("foomp{}", self.rng.next_u64()))
|
||||
}
|
||||
|
||||
pub fn admin_unchecked(&self) -> Addr {
|
||||
NYM_POOL_STORAGE
|
||||
.contract_admin
|
||||
.get(self.deps())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn change_admin(&mut self, new_admin: &Addr) {
|
||||
pub trait NymPoolContractTesterExt:
|
||||
ContractOpts<ExecuteMsg = ExecuteMsg, QueryMsg = QueryMsg, ContractError = NymPoolContractError>
|
||||
+ ChainOpts
|
||||
+ AdminExt
|
||||
+ DenomExt
|
||||
+ RandExt
|
||||
{
|
||||
fn change_admin(&mut self, new_admin: &Addr) {
|
||||
self.execute_msg(
|
||||
self.admin_unchecked(),
|
||||
&ExecuteMsg::UpdateAdmin {
|
||||
@@ -231,33 +75,14 @@ impl TestSetup {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn admin_msg(&self) -> MessageInfo {
|
||||
message_info(&self.admin_unchecked(), &[])
|
||||
}
|
||||
|
||||
pub fn denom(&self) -> String {
|
||||
NYM_POOL_STORAGE
|
||||
.pool_denomination
|
||||
.load(self.storage())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn coin(&self, amount: u128) -> Coin {
|
||||
coin(amount, self.denom())
|
||||
}
|
||||
|
||||
pub fn coins(&self, amount: u128) -> Vec<Coin> {
|
||||
coins(amount, self.denom())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn add_dummy_grant(&mut self) -> Grant {
|
||||
fn add_dummy_grant(&mut self) -> Grant {
|
||||
let grantee = self.generate_account();
|
||||
self.add_dummy_grant_for(&grantee)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn add_dummy_grant_for(&mut self, grantee: impl Into<String>) -> Grant {
|
||||
fn add_dummy_grant_for(&mut self, grantee: impl Into<String>) -> Grant {
|
||||
let grantee = Addr::unchecked(grantee);
|
||||
let granter = self.admin_unchecked();
|
||||
let env = self.env();
|
||||
@@ -275,23 +100,18 @@ impl TestSetup {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn lock_allowance(&mut self, grantee: impl Into<String>, amount: impl Into<Uint128>) {
|
||||
let denom = NYM_POOL_STORAGE
|
||||
.pool_denomination
|
||||
.load(self.deps().storage)
|
||||
.unwrap();
|
||||
|
||||
fn lock_allowance(&mut self, grantee: impl Into<String>, amount: impl Into<Uint128>) {
|
||||
self.execute_msg(
|
||||
Addr::unchecked(grantee),
|
||||
&ExecuteMsg::LockAllowance {
|
||||
amount: coin(amount.into().u128(), denom),
|
||||
amount: self.coin(amount.into().u128()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn full_locked_map(&self) -> HashMap<Addr, Uint128> {
|
||||
fn full_locked_map(&self) -> HashMap<Addr, Uint128> {
|
||||
NYM_POOL_STORAGE
|
||||
.locked
|
||||
.grantees
|
||||
@@ -301,7 +121,7 @@ impl TestSetup {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn add_granter(&mut self, granter: &Addr) {
|
||||
fn add_granter(&mut self, granter: &Addr) {
|
||||
let env = self.env();
|
||||
let admin = self.admin_unchecked();
|
||||
NYM_POOL_STORAGE
|
||||
@@ -309,3 +129,5 @@ impl TestSetup {
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl NymPoolContractTesterExt for ContractTester<NymPoolContract> {}
|
||||
|
||||
@@ -262,21 +262,22 @@ pub fn try_remove_expired(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testing::TestSetup;
|
||||
use crate::testing::{init_contract_tester, NymPoolContractTesterExt};
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts, DenomExt, RandExt};
|
||||
use nym_pool_contract_common::ExecuteMsg;
|
||||
|
||||
#[cfg(test)]
|
||||
mod updating_contract_admin {
|
||||
use super::*;
|
||||
use crate::testing::TestSetup;
|
||||
use cosmwasm_std::{Deps, Order};
|
||||
use cw_controllers::AdminError;
|
||||
use nym_contracts_common_testing::{AdminExt, RandExt};
|
||||
use nym_pool_contract_common::{ExecuteMsg, GranterAddress, GranterInformation};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let random_acc = test.generate_account();
|
||||
let new_admin = test.generate_account();
|
||||
@@ -310,7 +311,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_providing_valid_address() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let bad_account = "definitely-not-valid-account";
|
||||
let res = test.execute_raw(
|
||||
@@ -347,7 +348,7 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let current_admin = test.admin_unchecked();
|
||||
let new_admin = test.generate_account();
|
||||
|
||||
@@ -369,7 +370,7 @@ mod tests {
|
||||
//
|
||||
//
|
||||
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let current_admin = test.admin_unchecked();
|
||||
let new_admin = test.generate_account();
|
||||
let old_granters = granters(test.deps());
|
||||
@@ -392,13 +393,13 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod granting_allowance {
|
||||
use super::*;
|
||||
use crate::testing::TestSetup;
|
||||
use cosmwasm_std::StdError;
|
||||
use nym_contracts_common_testing::{AdminExt, RandExt};
|
||||
use nym_pool_contract_common::BasicAllowance;
|
||||
|
||||
#[test]
|
||||
fn requires_providing_valid_grantee_address() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let env = test.env();
|
||||
let admin = test.admin_msg();
|
||||
@@ -433,12 +434,12 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod revoking_allowance {
|
||||
use super::*;
|
||||
use crate::testing::TestSetup;
|
||||
use cosmwasm_std::StdError;
|
||||
use nym_contracts_common_testing::{AdminExt, RandExt};
|
||||
|
||||
#[test]
|
||||
fn requires_providing_valid_grantee_address() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let env = test.env();
|
||||
let admin = test.admin_msg();
|
||||
@@ -487,12 +488,12 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod using_allowance {
|
||||
use super::*;
|
||||
use crate::testing::TestSetup;
|
||||
use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt};
|
||||
use nym_pool_contract_common::{BasicAllowance, ExecuteMsg};
|
||||
|
||||
#[test]
|
||||
fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
let res = test.execute_raw(grantee, ExecuteMsg::UseAllowance { recipients: vec![] });
|
||||
@@ -504,7 +505,7 @@ mod tests {
|
||||
#[test]
|
||||
fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> {
|
||||
// 1 bad receiver
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
let res = test.execute_raw(
|
||||
@@ -519,7 +520,7 @@ mod tests {
|
||||
assert!(res.is_err());
|
||||
|
||||
// 3 receivers, one invalid
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
let addr1 = test.generate_account();
|
||||
@@ -547,7 +548,7 @@ mod tests {
|
||||
assert!(res.is_err());
|
||||
|
||||
// all fine
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
let res = test.execute_raw(
|
||||
@@ -576,7 +577,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_the_total_to_be_available_for_spending() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let recipient = test.generate_account();
|
||||
|
||||
// contract balance < required
|
||||
@@ -665,7 +666,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_the_total_to_be_within_spend_limit() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let allowance = Allowance::Basic(BasicAllowance {
|
||||
spend_limit: Some(test.coin(100)),
|
||||
expiration_unix_timestamp: None,
|
||||
@@ -712,7 +713,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
@@ -774,7 +775,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_not_be_expired() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let env = test.env();
|
||||
let allowance = Allowance::Basic(BasicAllowance {
|
||||
spend_limit: None,
|
||||
@@ -810,13 +811,14 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod withdrawing_from_allowance {
|
||||
use super::*;
|
||||
use crate::testing::TestSetup;
|
||||
use crate::testing::{init_contract_tester, NymPoolContractTesterExt};
|
||||
use cosmwasm_std::coin;
|
||||
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt};
|
||||
use nym_pool_contract_common::{BasicAllowance, ExecuteMsg};
|
||||
|
||||
#[test]
|
||||
fn requires_valid_coin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
let res = test.execute_raw(
|
||||
@@ -848,7 +850,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_the_amount_to_be_available_for_spending() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
// contract balance < required
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
@@ -912,7 +914,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_the_amount_to_be_within_spend_limit() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let allowance = Allowance::Basic(BasicAllowance {
|
||||
spend_limit: Some(test.coin(100)),
|
||||
expiration_unix_timestamp: None,
|
||||
@@ -952,7 +954,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn attaches_appropriate_bank_message() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
@@ -978,7 +980,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_not_be_expired() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let env = test.env();
|
||||
let allowance = Allowance::Basic(BasicAllowance {
|
||||
spend_limit: None,
|
||||
@@ -1011,7 +1013,7 @@ mod tests {
|
||||
#[test]
|
||||
fn locking_allowance() -> anyhow::Result<()> {
|
||||
// internals got tested in storage tests, so this is mostly about checking events (TODO)
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
let res = test.execute_raw(
|
||||
@@ -1035,7 +1037,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unlocking_allowance() -> anyhow::Result<()> {
|
||||
// internals got tested in storage tests, so this is mostly about checking events (TODO)
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(100));
|
||||
|
||||
@@ -1060,11 +1062,12 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod using_locked_allowance {
|
||||
use super::*;
|
||||
use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt};
|
||||
use nym_pool_contract_common::BasicAllowance;
|
||||
|
||||
#[test]
|
||||
fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
|
||||
let res = test.execute_raw(
|
||||
@@ -1079,7 +1082,7 @@ mod tests {
|
||||
#[test]
|
||||
fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> {
|
||||
// 1 bad receiver
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(10000));
|
||||
|
||||
@@ -1095,7 +1098,7 @@ mod tests {
|
||||
assert!(res.is_err());
|
||||
|
||||
// 3 receivers, one invalid
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(10000));
|
||||
|
||||
@@ -1124,7 +1127,7 @@ mod tests {
|
||||
assert!(res.is_err());
|
||||
|
||||
// all fine
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(10000));
|
||||
|
||||
@@ -1154,7 +1157,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_the_total_to_be_locked_by_grantee() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(100));
|
||||
|
||||
@@ -1193,7 +1196,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(10000));
|
||||
@@ -1256,7 +1259,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_not_be_expired() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let env = test.env();
|
||||
let allowance = Allowance::Basic(BasicAllowance {
|
||||
spend_limit: None,
|
||||
@@ -1294,11 +1297,12 @@ mod tests {
|
||||
mod withdrawing_from_locked_allowance {
|
||||
use super::*;
|
||||
use cosmwasm_std::coin;
|
||||
use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt};
|
||||
use nym_pool_contract_common::BasicAllowance;
|
||||
|
||||
#[test]
|
||||
fn requires_valid_coin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(10000));
|
||||
|
||||
@@ -1331,7 +1335,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn attaches_appropriate_bank_message() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(10000));
|
||||
@@ -1358,7 +1362,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_not_be_expired() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let env = test.env();
|
||||
let allowance = Allowance::Basic(BasicAllowance {
|
||||
spend_limit: None,
|
||||
@@ -1391,7 +1395,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_the_amount_to_be_locked_by_grantee() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
test.lock_allowance(&grantee, Uint128::new(100));
|
||||
|
||||
@@ -1424,7 +1428,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn adding_new_granter() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let bad_address = "foomp";
|
||||
let good_address = test.generate_account();
|
||||
|
||||
@@ -1456,7 +1460,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn revoking_granter() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let bad_address = "foomp";
|
||||
let good_address = test.generate_account();
|
||||
let granter_address = test.generate_account();
|
||||
@@ -1500,10 +1504,12 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod removing_expired {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, NymPoolContract, NymPoolContractTesterExt};
|
||||
use nym_contracts_common_testing::{ChainOpts, ContractOpts, ContractTester, RandExt};
|
||||
use nym_pool_contract_common::{BasicAllowance, GranteeAddress};
|
||||
|
||||
fn setup_with_expired_grant() -> (TestSetup, GranteeAddress) {
|
||||
let mut test = TestSetup::init();
|
||||
fn setup_with_expired_grant() -> (ContractTester<NymPoolContract>, GranteeAddress) {
|
||||
let mut test = init_contract_tester();
|
||||
let env = test.env();
|
||||
let allowance = Allowance::Basic(BasicAllowance {
|
||||
spend_limit: None,
|
||||
@@ -1543,7 +1549,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requires_grant_to_actually_exist_and_be_expired() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let mut test = init_contract_tester();
|
||||
let sender = test.generate_account();
|
||||
let grantee = test.add_dummy_grant().grantee;
|
||||
let not_grantee = test.generate_account();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[alias]
|
||||
wasm = "build --release --lib --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib"
|
||||
schema = "run --bin schema --features=schema-gen"
|
||||
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "nym-performance-contract"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "schema"
|
||||
required-features = ["schema-gen"]
|
||||
|
||||
[lib]
|
||||
name = "nym_performance_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-performance-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
nym-mixnet-contract = { path = "../mixnet", features = ["testable-mixnet-contract"] }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-performance-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,5 @@
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
|
||||
|
||||
generate-schema:
|
||||
cargo schema
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::write_api;
|
||||
use nym_performance_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
fn main() {
|
||||
write_api! {
|
||||
instantiate: InstantiateMsg,
|
||||
query: QueryMsg,
|
||||
execute: ExecuteMsg,
|
||||
migrate: MigrateMsg,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::queries::{
|
||||
query_admin, query_epoch_measurements_paged, query_epoch_performance_paged,
|
||||
query_full_historical_performance_paged, query_network_monitor_details,
|
||||
query_network_monitors_paged, query_node_measurements, query_node_performance,
|
||||
query_node_performance_paged, query_retired_network_monitors_paged,
|
||||
};
|
||||
use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE;
|
||||
use crate::transactions::{
|
||||
try_authorise_network_monitor, try_batch_submit_performance_results,
|
||||
try_remove_epoch_measurements, try_remove_node_measurements, try_retire_network_monitor,
|
||||
try_submit_performance_results, try_update_contract_admin,
|
||||
};
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response,
|
||||
};
|
||||
use nym_contracts_common::set_build_information;
|
||||
use nym_performance_contract_common::{
|
||||
ExecuteMsg, InstantiateMsg, MigrateMsg, NymPerformanceContractError, QueryMsg,
|
||||
};
|
||||
|
||||
const CONTRACT_NAME: &str = "crate:nym-performance-contract";
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[entry_point]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
set_build_information!(deps.storage)?;
|
||||
|
||||
let mixnet_contract_address = deps.api.addr_validate(&msg.mixnet_contract_address)?;
|
||||
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE.initialise(
|
||||
deps,
|
||||
env,
|
||||
info.sender,
|
||||
mixnet_contract_address.clone(),
|
||||
msg.authorised_network_monitors,
|
||||
)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn execute(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
match msg {
|
||||
ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin),
|
||||
ExecuteMsg::Submit { epoch, data } => {
|
||||
try_submit_performance_results(deps, info, epoch, data)
|
||||
}
|
||||
ExecuteMsg::BatchSubmit { epoch, data } => {
|
||||
try_batch_submit_performance_results(deps, info, epoch, data)
|
||||
}
|
||||
ExecuteMsg::AuthoriseNetworkMonitor { address } => {
|
||||
try_authorise_network_monitor(deps, env, info, address)
|
||||
}
|
||||
ExecuteMsg::RetireNetworkMonitor { address } => {
|
||||
try_retire_network_monitor(deps, env, info, address)
|
||||
}
|
||||
ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => {
|
||||
try_remove_node_measurements(deps, info, epoch_id, node_id)
|
||||
}
|
||||
ExecuteMsg::RemoveEpochMeasurements { epoch_id } => {
|
||||
try_remove_epoch_measurements(deps, info, epoch_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result<Binary, NymPerformanceContractError> {
|
||||
match msg {
|
||||
QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?),
|
||||
QueryMsg::NodePerformance { epoch_id, node_id } => Ok(to_json_binary(
|
||||
&query_node_performance(deps, epoch_id, node_id)?,
|
||||
)?),
|
||||
QueryMsg::NodePerformancePaged {
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_node_performance_paged(
|
||||
deps,
|
||||
node_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
QueryMsg::EpochPerformancePaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_epoch_performance_paged(
|
||||
deps,
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
QueryMsg::FullHistoricalPerformancePaged { start_after, limit } => Ok(to_json_binary(
|
||||
&query_full_historical_performance_paged(deps, start_after, limit)?,
|
||||
)?),
|
||||
QueryMsg::NetworkMonitor { address } => Ok(to_json_binary(
|
||||
&query_network_monitor_details(deps, address)?,
|
||||
)?),
|
||||
QueryMsg::NetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary(
|
||||
&query_network_monitors_paged(deps, start_after, limit)?,
|
||||
)?),
|
||||
QueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary(
|
||||
&query_retired_network_monitors_paged(deps, start_after, limit)?,
|
||||
)?),
|
||||
QueryMsg::NodeMeasurements { epoch_id, node_id } => Ok(to_json_binary(
|
||||
&query_node_measurements(deps, epoch_id, node_id)?,
|
||||
)?),
|
||||
QueryMsg::EpochMeasurementsPaged {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_epoch_measurements_paged(
|
||||
deps,
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?),
|
||||
}
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn migrate(
|
||||
deps: DepsMut,
|
||||
_: Env,
|
||||
_msg: MigrateMsg,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
set_build_information!(deps.storage)?;
|
||||
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod contract_instantiation {
|
||||
use super::*;
|
||||
use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE;
|
||||
use crate::testing::PreInitContract;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
|
||||
#[test]
|
||||
fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> {
|
||||
// we need to mock dependencies in a state where mixnet contract has already been instantiated
|
||||
// (we query it at init)
|
||||
let mut pre_init = PreInitContract::new();
|
||||
let env = pre_init.env();
|
||||
let mixnet_contract_address = pre_init.mixnet_contract_address.to_string();
|
||||
let some_sender = pre_init.addr_make("some_sender");
|
||||
let deps = pre_init.deps_mut();
|
||||
|
||||
instantiate(
|
||||
deps,
|
||||
env,
|
||||
message_info(&some_sender, &[]),
|
||||
InstantiateMsg {
|
||||
mixnet_contract_address,
|
||||
authorised_network_monitors: vec![],
|
||||
},
|
||||
)?;
|
||||
|
||||
let deps = pre_init.deps();
|
||||
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.contract_admin
|
||||
.assert_admin(deps, &some_sender)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdError, StdResult};
|
||||
use cw_storage_plus::{Key, Namespace, Path, PrimaryKey};
|
||||
use nym_mixnet_contract_common::{Interval, MixNodeBond, NymNodeBond};
|
||||
use nym_performance_contract_common::{EpochId, NodeId};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub(crate) trait MixnetContractQuerier {
|
||||
#[allow(dead_code)]
|
||||
fn query_mixnet_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &nym_mixnet_contract_common::QueryMsg,
|
||||
) -> StdResult<T>;
|
||||
|
||||
fn query_mixnet_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>>;
|
||||
|
||||
fn query_mixnet_contract_storage_value<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<T>> {
|
||||
match self.query_mixnet_contract_storage(address, key)? {
|
||||
None => Ok(None),
|
||||
Some(value) => Ok(Some(from_json(&value)?)),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_current_mixnet_interval(&self, address: impl Into<String>) -> StdResult<Interval> {
|
||||
self.query_mixnet_contract_storage_value(address, b"ci")?
|
||||
.ok_or(StdError::not_found(
|
||||
"unable to retrieve interval information from the mixnet contract storage",
|
||||
))
|
||||
}
|
||||
|
||||
fn query_current_absolute_mixnet_epoch_id(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
) -> StdResult<EpochId> {
|
||||
self.query_current_mixnet_interval(address)
|
||||
.map(|interval| interval.current_epoch_absolute_id())
|
||||
}
|
||||
|
||||
fn check_node_existence(&self, address: impl Into<String>, node_id: NodeId) -> StdResult<bool> {
|
||||
let mixnet_contract_address = address.into();
|
||||
|
||||
// 1. check if it's a nym-node
|
||||
if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? {
|
||||
return Ok(!nym_node.is_unbonding);
|
||||
}
|
||||
|
||||
// 2. try a legacy mixnode
|
||||
if let Some(nym_node) = self.query_mixnode_bond(mixnet_contract_address, node_id)? {
|
||||
return Ok(!nym_node.is_unbonding);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn query_nymnode_bond(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
node_id: NodeId,
|
||||
) -> StdResult<Option<NymNodeBond>> {
|
||||
// construct proper map key
|
||||
let pk_namespace = "nn";
|
||||
let path: Path<NymNodeBond> = Path::new(
|
||||
Namespace::from_static_str(pk_namespace).as_slice(),
|
||||
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
);
|
||||
let storage_key = path.deref();
|
||||
|
||||
self.query_mixnet_contract_storage_value(address, storage_key)
|
||||
}
|
||||
|
||||
fn query_mixnode_bond(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
node_id: NodeId,
|
||||
) -> StdResult<Option<MixNodeBond>> {
|
||||
// construct proper map key
|
||||
let pk_namespace = "mnn";
|
||||
let path: Path<MixNodeBond> = Path::new(
|
||||
Namespace::from_static_str(pk_namespace).as_slice(),
|
||||
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
);
|
||||
let storage_key = path.deref();
|
||||
|
||||
self.query_mixnet_contract_storage_value(address, storage_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> MixnetContractQuerier for QuerierWrapper<'_, C>
|
||||
where
|
||||
C: CustomQuery,
|
||||
{
|
||||
fn query_mixnet_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &nym_mixnet_contract_common::QueryMsg,
|
||||
) -> StdResult<T> {
|
||||
self.query_wasm_smart(address, msg)
|
||||
}
|
||||
|
||||
fn query_mixnet_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>> {
|
||||
self.query_wasm_raw(address, key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod contract;
|
||||
pub mod queued_migrations;
|
||||
pub mod storage;
|
||||
|
||||
mod helpers;
|
||||
mod queries;
|
||||
mod transactions;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod testing;
|
||||
@@ -0,0 +1,588 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::{retrieval_limits, NYM_PERFORMANCE_CONTRACT_STORAGE};
|
||||
use cosmwasm_std::{Addr, Deps, Order, StdResult};
|
||||
use cw_controllers::AdminResponse;
|
||||
use cw_storage_plus::Bound;
|
||||
use nym_performance_contract_common::{
|
||||
EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse,
|
||||
FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation,
|
||||
NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement,
|
||||
NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse,
|
||||
NodePerformanceResponse, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse,
|
||||
};
|
||||
|
||||
pub fn query_admin(deps: Deps) -> Result<AdminResponse, NymPerformanceContractError> {
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.contract_admin
|
||||
.query_admin(deps)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn query_node_performance(
|
||||
deps: Deps,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodePerformanceResponse, NymPerformanceContractError> {
|
||||
let performance =
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance(deps.storage, epoch_id, node_id)?;
|
||||
Ok(NodePerformanceResponse { performance })
|
||||
}
|
||||
|
||||
pub fn query_node_measurements(
|
||||
deps: Deps,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodeMeasurementsResponse, NymPerformanceContractError> {
|
||||
let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.performance_results
|
||||
.results
|
||||
.may_load(deps.storage, (epoch_id, node_id))?;
|
||||
Ok(NodeMeasurementsResponse { measurements })
|
||||
}
|
||||
|
||||
pub fn query_node_performance_paged(
|
||||
deps: Deps,
|
||||
node_id: NodeId,
|
||||
start_after: Option<EpochId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<NodePerformancePagedResponse, NymPerformanceContractError> {
|
||||
let current_epoch_id = NYM_PERFORMANCE_CONTRACT_STORAGE.current_mixnet_epoch_id(deps)?;
|
||||
|
||||
let start = match start_after {
|
||||
None => NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.mixnet_epoch_id_at_creation
|
||||
.load(deps.storage)?,
|
||||
Some(start_after) => start_after + 1,
|
||||
};
|
||||
|
||||
let mut performance = Vec::new();
|
||||
|
||||
if current_epoch_id < start {
|
||||
return Ok(NodePerformancePagedResponse {
|
||||
node_id,
|
||||
performance,
|
||||
start_next_after: None,
|
||||
});
|
||||
}
|
||||
|
||||
let limit = limit
|
||||
.unwrap_or(retrieval_limits::NODE_PERFORMANCE_DEFAULT_LIMIT)
|
||||
.min(retrieval_limits::NODE_PERFORMANCE_MAX_LIMIT) as usize;
|
||||
|
||||
for epoch_id in (start..=current_epoch_id).take(limit) {
|
||||
performance.push(EpochNodePerformance {
|
||||
epoch: epoch_id,
|
||||
performance: NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance(
|
||||
deps.storage,
|
||||
epoch_id,
|
||||
node_id,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
let start_next_after = performance.last().and_then(|last| {
|
||||
if last.epoch != current_epoch_id {
|
||||
Some(last.epoch)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Ok(NodePerformancePagedResponse {
|
||||
node_id,
|
||||
performance,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_epoch_performance_paged(
|
||||
deps: Deps,
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<EpochPerformancePagedResponse, NymPerformanceContractError> {
|
||||
let limit = limit
|
||||
.unwrap_or(retrieval_limits::NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT)
|
||||
.min(retrieval_limits::NODE_EPOCH_PERFORMANCE_MAX_LIMIT) as usize;
|
||||
|
||||
let start = start_after.map(Bound::exclusive);
|
||||
|
||||
let performance = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.performance_results
|
||||
.results
|
||||
.prefix(epoch_id)
|
||||
.range(deps.storage, start, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|record| {
|
||||
record.map(|(node_id, results)| NodePerformance {
|
||||
node_id,
|
||||
performance: results.median(),
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = performance.last().map(|last| last.node_id);
|
||||
|
||||
Ok(EpochPerformancePagedResponse {
|
||||
epoch_id,
|
||||
performance,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_epoch_measurements_paged(
|
||||
deps: Deps,
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<EpochMeasurementsPagedResponse, NymPerformanceContractError> {
|
||||
let limit = limit
|
||||
.unwrap_or(retrieval_limits::NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT)
|
||||
.min(retrieval_limits::NODE_EPOCH_MEASUREMENTS_MAX_LIMIT) as usize;
|
||||
|
||||
let start = start_after.map(Bound::exclusive);
|
||||
|
||||
let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.performance_results
|
||||
.results
|
||||
.prefix(epoch_id)
|
||||
.range(deps.storage, start, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|record| {
|
||||
record.map(|(node_id, measurements)| NodeMeasurement {
|
||||
node_id,
|
||||
measurements,
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = measurements.last().map(|last| last.node_id);
|
||||
|
||||
Ok(EpochMeasurementsPagedResponse {
|
||||
epoch_id,
|
||||
measurements,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_full_historical_performance_paged(
|
||||
deps: Deps,
|
||||
start_after: Option<(EpochId, NodeId)>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<FullHistoricalPerformancePagedResponse, NymPerformanceContractError> {
|
||||
let limit = limit
|
||||
.unwrap_or(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT)
|
||||
.min(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT) as usize;
|
||||
|
||||
let start = start_after.map(Bound::exclusive);
|
||||
|
||||
let performance = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.performance_results
|
||||
.results
|
||||
.range(deps.storage, start, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|record| {
|
||||
record.map(|((epoch_id, node_id), results)| HistoricalPerformance {
|
||||
epoch_id,
|
||||
node_id,
|
||||
performance: results.median(),
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = performance.last().map(|last| (last.epoch_id, last.node_id));
|
||||
|
||||
Ok(FullHistoricalPerformancePagedResponse {
|
||||
performance,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_network_monitor_information(
|
||||
deps: Deps,
|
||||
address: &Addr,
|
||||
) -> Result<Option<NetworkMonitorInformation>, NymPerformanceContractError> {
|
||||
let Some(details) = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.network_monitors
|
||||
.authorised
|
||||
.may_load(deps.storage, address)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let current_submission_metadata = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.performance_results
|
||||
.submission_metadata
|
||||
.load(deps.storage, address)?;
|
||||
|
||||
Ok(Some(NetworkMonitorInformation {
|
||||
details,
|
||||
current_submission_metadata,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn query_network_monitor_details(
|
||||
deps: Deps,
|
||||
address: String,
|
||||
) -> Result<NetworkMonitorResponse, NymPerformanceContractError> {
|
||||
let address = deps.api.addr_validate(&address)?;
|
||||
|
||||
Ok(NetworkMonitorResponse {
|
||||
info: get_network_monitor_information(deps, &address)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_network_monitors_paged(
|
||||
deps: Deps,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<NetworkMonitorsPagedResponse, NymPerformanceContractError> {
|
||||
let limit = limit
|
||||
.unwrap_or(retrieval_limits::NETWORK_MONITORS_DEFAULT_LIMIT)
|
||||
.min(retrieval_limits::NETWORK_MONITORS_MAX_LIMIT) as usize;
|
||||
|
||||
let addr = start_after
|
||||
.map(|addr| deps.api.addr_validate(&addr))
|
||||
.transpose()?;
|
||||
let start = addr.as_ref().map(Bound::exclusive);
|
||||
|
||||
let info = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.network_monitors
|
||||
.authorised
|
||||
.range(deps.storage, start, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|record| {
|
||||
record.and_then(|(address, details)| {
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.performance_results
|
||||
.submission_metadata
|
||||
.load(deps.storage, &address)
|
||||
.map(|current_submission_metadata| NetworkMonitorInformation {
|
||||
details,
|
||||
current_submission_metadata,
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = info.last().map(|last| last.details.address.to_string());
|
||||
|
||||
Ok(NetworkMonitorsPagedResponse {
|
||||
info,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_retired_network_monitors_paged(
|
||||
deps: Deps,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<RetiredNetworkMonitorsPagedResponse, NymPerformanceContractError> {
|
||||
let limit = limit
|
||||
.unwrap_or(retrieval_limits::RETIRED_NETWORK_MONITORS_DEFAULT_LIMIT)
|
||||
.min(retrieval_limits::RETIRED_NETWORK_MONITORS_MAX_LIMIT) as usize;
|
||||
|
||||
let addr = start_after
|
||||
.map(|addr| deps.api.addr_validate(&addr))
|
||||
.transpose()?;
|
||||
let start = addr.as_ref().map(Bound::exclusive);
|
||||
|
||||
let info = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.network_monitors
|
||||
.retired
|
||||
.range(deps.storage, start, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|record| record.map(|(_, details)| details))
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = info.last().map(|last| last.details.address.to_string());
|
||||
|
||||
Ok(RetiredNetworkMonitorsPagedResponse {
|
||||
info,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
|
||||
use nym_contracts_common_testing::{ContractOpts, RandExt};
|
||||
|
||||
#[cfg(test)]
|
||||
mod admin_query {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt};
|
||||
use nym_performance_contract_common::ExecuteMsg;
|
||||
|
||||
#[test]
|
||||
fn returns_current_admin() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let initial_admin = test.admin_unchecked();
|
||||
|
||||
// initial
|
||||
let res = query_admin(test.deps())?;
|
||||
assert_eq!(res.admin, Some(initial_admin.to_string()));
|
||||
|
||||
let new_admin = test.generate_account();
|
||||
|
||||
// sanity check
|
||||
assert_ne!(initial_admin, new_admin);
|
||||
|
||||
// after update
|
||||
test.execute_msg(
|
||||
initial_admin.clone(),
|
||||
&ExecuteMsg::UpdateAdmin {
|
||||
admin: new_admin.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let updated_admin = query_admin(test.deps())?;
|
||||
assert_eq!(updated_admin.admin, Some(new_admin.to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn querying_node_performance_paged() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let node_id = test.bond_dummy_nymnode()?;
|
||||
let nm = test.generate_account();
|
||||
test.authorise_network_monitor(&nm)?;
|
||||
|
||||
// epoch 0
|
||||
test.insert_raw_performance(&nm, node_id, "0")?;
|
||||
|
||||
// epoch 1
|
||||
test.advance_mixnet_epoch()?;
|
||||
test.insert_raw_performance(&nm, node_id, "0.1")?;
|
||||
|
||||
// epoch 2
|
||||
test.advance_mixnet_epoch()?;
|
||||
test.insert_raw_performance(&nm, node_id, "0.2")?;
|
||||
|
||||
// epoch 3
|
||||
test.advance_mixnet_epoch()?;
|
||||
test.insert_raw_performance(&nm, node_id, "0.3")?;
|
||||
|
||||
// epoch 4
|
||||
test.advance_mixnet_epoch()?;
|
||||
test.insert_raw_performance(&nm, node_id, "0.4")?;
|
||||
|
||||
// epoch 5
|
||||
test.advance_mixnet_epoch()?;
|
||||
test.insert_raw_performance(&nm, node_id, "0.5")?;
|
||||
|
||||
let deps = test.deps();
|
||||
let res = query_node_performance_paged(deps, node_id, Some(5), None)?;
|
||||
assert!(res.start_next_after.is_none());
|
||||
assert!(res.performance.is_empty());
|
||||
|
||||
let res = query_node_performance_paged(deps, node_id, Some(42), None)?;
|
||||
assert!(res.start_next_after.is_none());
|
||||
assert!(res.performance.is_empty());
|
||||
|
||||
let res = query_node_performance_paged(deps, node_id, Some(4), None)?;
|
||||
assert!(res.start_next_after.is_none());
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![EpochNodePerformance {
|
||||
epoch: 5,
|
||||
performance: Some("0.5".parse()?),
|
||||
}]
|
||||
);
|
||||
|
||||
let res = query_node_performance_paged(deps, node_id, Some(2), None)?;
|
||||
assert!(res.start_next_after.is_none());
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![
|
||||
EpochNodePerformance {
|
||||
epoch: 3,
|
||||
performance: Some("0.3".parse()?),
|
||||
},
|
||||
EpochNodePerformance {
|
||||
epoch: 4,
|
||||
performance: Some("0.4".parse()?),
|
||||
},
|
||||
EpochNodePerformance {
|
||||
epoch: 5,
|
||||
performance: Some("0.5".parse()?),
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let res = query_node_performance_paged(deps, node_id, None, None)?;
|
||||
assert!(res.start_next_after.is_none());
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![
|
||||
EpochNodePerformance {
|
||||
epoch: 0,
|
||||
performance: Some("0".parse()?),
|
||||
},
|
||||
EpochNodePerformance {
|
||||
epoch: 1,
|
||||
performance: Some("0.1".parse()?),
|
||||
},
|
||||
EpochNodePerformance {
|
||||
epoch: 2,
|
||||
performance: Some("0.2".parse()?),
|
||||
},
|
||||
EpochNodePerformance {
|
||||
epoch: 3,
|
||||
performance: Some("0.3".parse()?),
|
||||
},
|
||||
EpochNodePerformance {
|
||||
epoch: 4,
|
||||
performance: Some("0.4".parse()?),
|
||||
},
|
||||
EpochNodePerformance {
|
||||
epoch: 5,
|
||||
performance: Some("0.5".parse()?),
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let res = query_node_performance_paged(deps, node_id, Some(2), Some(1))?;
|
||||
assert_eq!(res.start_next_after, Some(3));
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![EpochNodePerformance {
|
||||
epoch: 3,
|
||||
performance: Some("0.3".parse()?),
|
||||
}]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn querying_epoch_performance_paged() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let nm = test.generate_account();
|
||||
test.authorise_network_monitor(&nm)?;
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
for _ in 0..10 {
|
||||
nodes.push(test.bond_dummy_nymnode()?);
|
||||
}
|
||||
|
||||
let epoch_id = 5;
|
||||
test.set_mixnet_epoch(epoch_id)?;
|
||||
|
||||
test.insert_raw_performance(&nm, nodes[1], "0.1")?;
|
||||
test.insert_raw_performance(&nm, nodes[2], "0.2")?;
|
||||
test.insert_raw_performance(&nm, nodes[3], "0.3")?;
|
||||
// 4 is missing
|
||||
test.insert_raw_performance(&nm, nodes[5], "0.5")?;
|
||||
test.insert_raw_performance(&nm, nodes[6], "0.6")?;
|
||||
|
||||
let deps = test.deps();
|
||||
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[6]), None)?;
|
||||
assert!(res.start_next_after.is_none());
|
||||
assert!(res.performance.is_empty());
|
||||
|
||||
let res = query_epoch_performance_paged(deps, epoch_id, Some(42), None)?;
|
||||
assert!(res.start_next_after.is_none());
|
||||
assert!(res.performance.is_empty());
|
||||
|
||||
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[4]), None)?;
|
||||
assert_eq!(res.start_next_after, Some(nodes[6]));
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![
|
||||
NodePerformance {
|
||||
node_id: nodes[5],
|
||||
performance: "0.5".parse()?,
|
||||
},
|
||||
NodePerformance {
|
||||
node_id: nodes[6],
|
||||
performance: "0.6".parse()?,
|
||||
}
|
||||
]
|
||||
);
|
||||
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[3]), None)?;
|
||||
assert_eq!(res.start_next_after, Some(nodes[6]));
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![
|
||||
NodePerformance {
|
||||
node_id: nodes[5],
|
||||
performance: "0.5".parse()?,
|
||||
},
|
||||
NodePerformance {
|
||||
node_id: nodes[6],
|
||||
performance: "0.6".parse()?,
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), None)?;
|
||||
assert_eq!(res.start_next_after, Some(nodes[6]));
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![
|
||||
NodePerformance {
|
||||
node_id: nodes[3],
|
||||
performance: "0.3".parse()?,
|
||||
},
|
||||
NodePerformance {
|
||||
node_id: nodes[5],
|
||||
performance: "0.5".parse()?,
|
||||
},
|
||||
NodePerformance {
|
||||
node_id: nodes[6],
|
||||
performance: "0.6".parse()?,
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let res = query_epoch_performance_paged(deps, epoch_id, None, None)?;
|
||||
assert_eq!(res.start_next_after, Some(nodes[6]));
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![
|
||||
NodePerformance {
|
||||
node_id: nodes[1],
|
||||
performance: "0.1".parse()?,
|
||||
},
|
||||
NodePerformance {
|
||||
node_id: nodes[2],
|
||||
performance: "0.2".parse()?,
|
||||
},
|
||||
NodePerformance {
|
||||
node_id: nodes[3],
|
||||
performance: "0.3".parse()?,
|
||||
},
|
||||
NodePerformance {
|
||||
node_id: nodes[5],
|
||||
performance: "0.5".parse()?,
|
||||
},
|
||||
NodePerformance {
|
||||
node_id: nodes[6],
|
||||
performance: "0.6".parse()?,
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), Some(1))?;
|
||||
assert_eq!(res.start_next_after, Some(nodes[3]));
|
||||
assert_eq!(
|
||||
res.performance,
|
||||
vec![NodePerformance {
|
||||
node_id: nodes[3],
|
||||
performance: "0.3".parse()?,
|
||||
}]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,606 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use crate::helpers::MixnetContractQuerier;
|
||||
use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE;
|
||||
use cosmwasm_std::testing::{message_info, mock_env, MockApi};
|
||||
use cosmwasm_std::{
|
||||
coin, coins, Addr, Binary, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper,
|
||||
StdError, StdResult,
|
||||
};
|
||||
use cw_storage_plus::PrimaryKey;
|
||||
use mixnet_contract::testable_mixnet_contract::MixnetContract;
|
||||
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature};
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_contracts_common_testing::{
|
||||
addr, AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt,
|
||||
ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractStorageWrapper, ContractTester,
|
||||
ContractTesterBuilder, DenomExt, PermissionedFn, QueryFn, RandExt, TestableNymContract,
|
||||
TEST_DENOM,
|
||||
};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role};
|
||||
use nym_mixnet_contract_common::{
|
||||
CurrentIntervalResponse, EpochId, Interval, MixNode, MixNodeBond, MixnodeDetailsResponse,
|
||||
NodeCostParams, NodeRewarding, NymNode, NymNodeBondingPayload, RoleAssignment,
|
||||
SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT,
|
||||
DEFAULT_PROFIT_MARGIN_PERCENT,
|
||||
};
|
||||
use nym_performance_contract_common::constants::storage_keys;
|
||||
use nym_performance_contract_common::{
|
||||
ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformance, NodeResults,
|
||||
NymPerformanceContractError, QueryMsg,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct PerformanceContract;
|
||||
|
||||
impl TestableNymContract for PerformanceContract {
|
||||
const NAME: &'static str = "performance-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = NymPerformanceContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
query
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
InstantiateMsg {
|
||||
mixnet_contract_address: addr("mixnet-contract").to_string(),
|
||||
authorised_network_monitors: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn init() -> ContractTester<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let builder = ContractTesterBuilder::new().instantiate::<MixnetContract>(None);
|
||||
|
||||
// we just instantiated it
|
||||
let mixnet_address = builder
|
||||
.well_known_contracts
|
||||
.get(MixnetContract::NAME)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
builder
|
||||
.instantiate::<Self>(Some(InstantiateMsg {
|
||||
mixnet_contract_address: mixnet_address.to_string(),
|
||||
authorised_network_monitors: vec![],
|
||||
}))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_contract_tester() -> ContractTester<PerformanceContract> {
|
||||
PerformanceContract::init()
|
||||
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
|
||||
}
|
||||
|
||||
// we need to be able to test instantiation, but for that we require
|
||||
// deps in a state that already includes instantiated mixnet contract
|
||||
pub(crate) struct PreInitContract {
|
||||
tester_builder: ContractTesterBuilder<PerformanceContract>,
|
||||
pub(crate) mixnet_contract_address: Addr,
|
||||
pub(crate) api: MockApi,
|
||||
storage: ContractStorageWrapper,
|
||||
placeholder_address: Addr,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl PreInitContract {
|
||||
pub(crate) fn new() -> PreInitContract {
|
||||
let tester_builder =
|
||||
ContractTesterBuilder::<PerformanceContract>::new().instantiate::<MixnetContract>(None);
|
||||
|
||||
let mixnet_contract = tester_builder
|
||||
.well_known_contracts
|
||||
.get(&MixnetContract::NAME)
|
||||
.unwrap();
|
||||
|
||||
let api = tester_builder.api();
|
||||
let placeholder_address = api.addr_make("to-be-performance-contract");
|
||||
|
||||
let storage = tester_builder.contract_storage_wrapper(&placeholder_address);
|
||||
|
||||
PreInitContract {
|
||||
mixnet_contract_address: mixnet_contract.clone(),
|
||||
tester_builder,
|
||||
api,
|
||||
storage,
|
||||
placeholder_address,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deps(&self) -> Deps {
|
||||
Deps {
|
||||
storage: &self.storage,
|
||||
api: &self.api,
|
||||
querier: self.tester_builder.querier(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deps_mut(&mut self) -> DepsMut {
|
||||
DepsMut {
|
||||
storage: &mut self.storage,
|
||||
api: &self.api,
|
||||
querier: self.tester_builder.querier(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn querier(&self) -> QuerierWrapper {
|
||||
self.tester_builder.querier()
|
||||
}
|
||||
|
||||
pub(crate) fn env(&self) -> Env {
|
||||
Env {
|
||||
contract: ContractInfo {
|
||||
address: self.placeholder_address.clone(),
|
||||
},
|
||||
..mock_env()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn addr_make(&self, input: &str) -> Addr {
|
||||
self.api.addr_make(input)
|
||||
}
|
||||
|
||||
pub(crate) fn write_to_mixnet_contract_storage(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) -> StdResult<()> {
|
||||
let address = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.mixnet_contract_address
|
||||
.load(self.deps().storage)?;
|
||||
|
||||
self.set_contract_storage(address, key, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_to_mixnet_contract_storage_value<T: Serialize>(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
let address = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.mixnet_contract_address
|
||||
.load(self.deps().storage)?;
|
||||
|
||||
self.set_contract_storage_value(address, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryContractStorageWriter for PreInitContract {
|
||||
fn set_contract_storage(
|
||||
&mut self,
|
||||
address: impl Into<String>,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) {
|
||||
self.storage
|
||||
.as_inner_storage_mut()
|
||||
.set_contract_storage(address, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) trait PerformanceContractTesterExt:
|
||||
ContractOpts<
|
||||
ExecuteMsg = ExecuteMsg,
|
||||
QueryMsg = QueryMsg,
|
||||
ContractError = NymPerformanceContractError,
|
||||
> + ChainOpts
|
||||
+ AdminExt
|
||||
+ DenomExt
|
||||
+ RandExt
|
||||
+ BankExt
|
||||
+ ArbitraryContractStorageReader
|
||||
+ ArbitraryContractStorageWriter
|
||||
{
|
||||
fn mixnet_contract_address(&self) -> StdResult<Addr> {
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.mixnet_contract_address
|
||||
.load(self.deps().storage)
|
||||
}
|
||||
|
||||
fn execute_mixnet_contract(
|
||||
&mut self,
|
||||
sender: MessageInfo,
|
||||
msg: &nym_mixnet_contract_common::ExecuteMsg,
|
||||
) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.execute_arbitrary_contract(address, sender, msg)
|
||||
.map_err(|err| {
|
||||
StdError::generic_err(format!("mixnet contract execution failure: {err}"))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_from_mixnet_contract_storage<T: DeserializeOwned>(
|
||||
&self,
|
||||
key: impl AsRef<[u8]>,
|
||||
) -> StdResult<T> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.must_read_value_from_contract_storage(address, key)
|
||||
}
|
||||
|
||||
fn write_to_mixnet_contract_storage(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: impl AsRef<[u8]>,
|
||||
) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
<Self as ArbitraryContractStorageWriter>::set_contract_storage(self, address, key, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_to_mixnet_contract_storage_value<T: Serialize>(
|
||||
&mut self,
|
||||
key: impl AsRef<[u8]>,
|
||||
value: &T,
|
||||
) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
self.set_contract_storage_value(address, key, value)
|
||||
}
|
||||
|
||||
fn current_mixnet_epoch(&self) -> StdResult<EpochId> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
Ok(self
|
||||
.deps()
|
||||
.querier
|
||||
.query_current_mixnet_interval(address.clone())?
|
||||
.current_epoch_absolute_id())
|
||||
}
|
||||
|
||||
fn advance_mixnet_epoch(&mut self) -> StdResult<()> {
|
||||
let interval_details: CurrentIntervalResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&nym_mixnet_contract_common::QueryMsg::GetCurrentIntervalDetails {},
|
||||
)?;
|
||||
let until_end = interval_details.time_until_current_epoch_end().as_secs();
|
||||
let timestamp = self.env().block.time.plus_seconds(until_end + 1);
|
||||
self.set_block_time(timestamp);
|
||||
self.next_block();
|
||||
|
||||
// this was hardcoded in mixnet init
|
||||
let mixnet_rewarder = self.addr_make("rewarder");
|
||||
let rewarder = message_info(&mixnet_rewarder, &[]);
|
||||
self.execute_mixnet_contract(
|
||||
rewarder.clone(),
|
||||
&nym_mixnet_contract_common::ExecuteMsg::BeginEpochTransition {},
|
||||
)?;
|
||||
self.execute_mixnet_contract(
|
||||
rewarder.clone(),
|
||||
&nym_mixnet_contract_common::ExecuteMsg::ReconcileEpochEvents { limit: None },
|
||||
)?;
|
||||
|
||||
for role in [
|
||||
Role::ExitGateway,
|
||||
Role::EntryGateway,
|
||||
Role::Layer1,
|
||||
Role::Layer2,
|
||||
Role::Layer3,
|
||||
Role::Standby,
|
||||
] {
|
||||
self.execute_mixnet_contract(
|
||||
rewarder.clone(),
|
||||
&nym_mixnet_contract_common::ExecuteMsg::AssignRoles {
|
||||
assignment: RoleAssignment {
|
||||
role,
|
||||
nodes: vec![],
|
||||
},
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_mixnet_epoch(&mut self, epoch_id: EpochId) -> StdResult<()> {
|
||||
let address = self.mixnet_contract_address()?;
|
||||
|
||||
let interval = self
|
||||
.deps()
|
||||
.querier
|
||||
.query_current_mixnet_interval(address.clone())?;
|
||||
|
||||
let mut to_update = if interval.current_epoch_absolute_id() <= epoch_id {
|
||||
interval
|
||||
} else {
|
||||
Interval::init_interval(
|
||||
interval.epochs_in_interval(),
|
||||
interval.epoch_length(),
|
||||
&mock_env(),
|
||||
)
|
||||
};
|
||||
|
||||
let current = to_update.current_epoch_absolute_id();
|
||||
let diff = epoch_id - current;
|
||||
for _ in 0..diff {
|
||||
to_update = to_update.advance_epoch();
|
||||
}
|
||||
self.set_contract_storage_value(&address, b"ci", &to_update)
|
||||
}
|
||||
|
||||
fn authorise_network_monitor(
|
||||
&mut self,
|
||||
addr: &Addr,
|
||||
) -> Result<(), NymPerformanceContractError> {
|
||||
let admin = self.admin_unchecked();
|
||||
self.execute_raw(
|
||||
admin,
|
||||
ExecuteMsg::AuthoriseNetworkMonitor {
|
||||
address: addr.to_string(),
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dummy_node_performance(&mut self) -> NodePerformance {
|
||||
let node_id = self.bond_dummy_nymnode().unwrap();
|
||||
NodePerformance {
|
||||
node_id,
|
||||
performance: Percent::from_percentage_value(69).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn retire_network_monitor(&mut self, addr: &Addr) -> Result<(), NymPerformanceContractError> {
|
||||
let admin = self.admin_unchecked();
|
||||
self.execute_raw(
|
||||
admin,
|
||||
ExecuteMsg::RetireNetworkMonitor {
|
||||
address: addr.to_string(),
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_epoch_performance(
|
||||
&mut self,
|
||||
addr: &Addr,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
performance: Percent,
|
||||
) -> Result<(), NymPerformanceContractError> {
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data(
|
||||
self.deps_mut(),
|
||||
addr,
|
||||
epoch_id,
|
||||
NodePerformance {
|
||||
node_id,
|
||||
performance,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn insert_performance(
|
||||
&mut self,
|
||||
addr: &Addr,
|
||||
node_id: NodeId,
|
||||
performance: Percent,
|
||||
) -> Result<(), NymPerformanceContractError> {
|
||||
let epoch_id = self.current_mixnet_epoch()?;
|
||||
|
||||
self.insert_epoch_performance(addr, epoch_id, node_id, performance)
|
||||
}
|
||||
|
||||
// makes testing easier
|
||||
fn insert_raw_performance(
|
||||
&mut self,
|
||||
addr: &Addr,
|
||||
node_id: NodeId,
|
||||
raw: &str,
|
||||
) -> Result<(), NymPerformanceContractError> {
|
||||
self.insert_performance(
|
||||
addr,
|
||||
node_id,
|
||||
Percent::from_str(raw).map_err(|err| {
|
||||
NymPerformanceContractError::StdErr(StdError::parse_err("Percent", err.to_string()))
|
||||
})?,
|
||||
)
|
||||
}
|
||||
|
||||
fn read_raw_scores(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
) -> Result<NodeResults, NymPerformanceContractError> {
|
||||
let scores = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.performance_results
|
||||
.results
|
||||
.load(self.deps().storage, (epoch_id, node_id))?;
|
||||
Ok(scores)
|
||||
}
|
||||
|
||||
fn bond_dummy_nymnode(&mut self) -> Result<NodeId, NymPerformanceContractError> {
|
||||
let node_owner = self.generate_account_with_balance();
|
||||
let pledge = coins(100_000000, TEST_DENOM);
|
||||
let keypair = ed25519::KeyPair::new(self.raw_rng());
|
||||
let identity_key = keypair.public_key().to_base58_string();
|
||||
|
||||
let node = NymNode {
|
||||
host: "1.2.3.4".to_string(),
|
||||
custom_http_port: None,
|
||||
identity_key,
|
||||
};
|
||||
let cost_params = NodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT)
|
||||
.unwrap(),
|
||||
interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM),
|
||||
};
|
||||
// initial signing nonce is 0 for a new address
|
||||
let signing_nonce = 0;
|
||||
|
||||
let payload = NymNodeBondingPayload::new(node.clone(), cost_params.clone());
|
||||
let content = ContractMessageContent::new(node_owner.clone(), pledge.clone(), payload);
|
||||
let msg = SignableNymNodeBondingMsg::new(signing_nonce, content);
|
||||
|
||||
let owner_signature = keypair.private_key().sign(msg.to_plaintext()?);
|
||||
let owner_signature = MessageSignature::from(owner_signature.to_bytes().as_ref());
|
||||
|
||||
self.execute_mixnet_contract(
|
||||
message_info(&node_owner, &pledge),
|
||||
&nym_mixnet_contract_common::ExecuteMsg::BondNymNode {
|
||||
node,
|
||||
cost_params,
|
||||
owner_signature,
|
||||
},
|
||||
)?;
|
||||
|
||||
let bond: NodeOwnershipResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&nym_mixnet_contract_common::QueryMsg::GetOwnedNymNode {
|
||||
address: node_owner.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(bond.details.unwrap().bond_information.node_id)
|
||||
}
|
||||
|
||||
fn unbond_nymnode(&mut self, node_id: NodeId) -> Result<(), NymPerformanceContractError> {
|
||||
let bond: NodeDetailsResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&nym_mixnet_contract_common::QueryMsg::GetNymNodeDetails { node_id },
|
||||
)?;
|
||||
|
||||
let node_owner = bond.details.unwrap().bond_information.owner;
|
||||
|
||||
self.execute_mixnet_contract(
|
||||
message_info(&node_owner, &[]),
|
||||
&nym_mixnet_contract_common::ExecuteMsg::UnbondNymNode {},
|
||||
)?;
|
||||
|
||||
self.advance_mixnet_epoch()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bond_dummy_legacy_mixnode(&mut self) -> Result<NodeId, NymPerformanceContractError> {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct UniqueRef<T> {
|
||||
// note, we collapse the pk - combining everything under the namespace - even if it is composite
|
||||
pk: Binary,
|
||||
value: T,
|
||||
}
|
||||
|
||||
// there's no proper Execute flow for this anymore, so we have to "hack" the storage a bit,
|
||||
// ensuring all invariants still hold
|
||||
let owner = self.generate_account_with_balance();
|
||||
|
||||
let mixnode = MixNode {
|
||||
host: "1.2.3.4".to_string(),
|
||||
mix_port: 123,
|
||||
verloc_port: 123,
|
||||
http_api_port: 123,
|
||||
sphinx_key: "aaaa".to_string(),
|
||||
identity_key: "bbbbb".to_string(),
|
||||
version: "ccc".to_string(),
|
||||
};
|
||||
let cost_params = NodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT)
|
||||
.unwrap(),
|
||||
interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM),
|
||||
};
|
||||
|
||||
// adjust node counter
|
||||
let node_id_counter: u32 = self.read_from_mixnet_contract_storage("nic")?;
|
||||
let node_id = node_id_counter + 1;
|
||||
self.write_to_mixnet_contract_storage_value("nic", &node_id)?;
|
||||
|
||||
let current_epoch = self.current_mixnet_epoch()?;
|
||||
let pledge = coin(100_000000, TEST_DENOM);
|
||||
let mixnode_rewarding =
|
||||
NodeRewarding::initialise_new(cost_params, &pledge, current_epoch).unwrap();
|
||||
let env = self.env();
|
||||
let mixnode_bond = MixNodeBond {
|
||||
mix_id: node_id,
|
||||
owner,
|
||||
original_pledge: pledge,
|
||||
mix_node: mixnode,
|
||||
proxy: None,
|
||||
bonding_height: env.block.height,
|
||||
is_unbonding: false,
|
||||
};
|
||||
|
||||
// save to the main mixnode storage
|
||||
self.set_contract_map_value(
|
||||
self.mixnet_contract_address()?,
|
||||
"mnn",
|
||||
node_id,
|
||||
&mixnode_bond,
|
||||
)?;
|
||||
// update indices
|
||||
let pk = node_id.joined_key();
|
||||
let unique_ref = UniqueRef {
|
||||
pk: pk.into(),
|
||||
value: mixnode_bond.clone(),
|
||||
};
|
||||
|
||||
// owner index
|
||||
let idx = mixnode_bond.owner.clone();
|
||||
self.set_contract_map_value(self.mixnet_contract_address()?, "mno", idx, &unique_ref)?;
|
||||
|
||||
// identity key index
|
||||
let idx = mixnode_bond.mix_node.identity_key.clone();
|
||||
self.set_contract_map_value(self.mixnet_contract_address()?, "mni", idx, &unique_ref)?;
|
||||
|
||||
// sphinx key index
|
||||
let idx = mixnode_bond.mix_node.sphinx_key.clone();
|
||||
self.set_contract_map_value(self.mixnet_contract_address()?, "mns", idx, &unique_ref)?;
|
||||
|
||||
// update rewarding data
|
||||
self.set_contract_map_value(
|
||||
self.mixnet_contract_address()?,
|
||||
"mnr",
|
||||
node_id,
|
||||
&mixnode_rewarding,
|
||||
)?;
|
||||
|
||||
Ok(node_id)
|
||||
}
|
||||
|
||||
fn unbond_legacy_mixnode(
|
||||
&mut self,
|
||||
node_id: NodeId,
|
||||
) -> Result<(), NymPerformanceContractError> {
|
||||
let bond: MixnodeDetailsResponse = self.query_arbitrary_contract(
|
||||
self.mixnet_contract_address()?,
|
||||
&nym_mixnet_contract_common::QueryMsg::GetMixnodeDetails { mix_id: node_id },
|
||||
)?;
|
||||
|
||||
let node_owner = bond.mixnode_details.unwrap().bond_information.owner;
|
||||
|
||||
self.execute_mixnet_contract(
|
||||
message_info(&node_owner, &[]),
|
||||
&nym_mixnet_contract_common::ExecuteMsg::UnbondMixnode {},
|
||||
)?;
|
||||
|
||||
self.advance_mixnet_epoch()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PerformanceContractTesterExt for ContractTester<PerformanceContract> {}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE;
|
||||
use cosmwasm_std::{to_json_binary, DepsMut, Env, Event, MessageInfo, Response};
|
||||
use nym_performance_contract_common::{
|
||||
EpochId, NodeId, NodePerformance, NymPerformanceContractError,
|
||||
};
|
||||
|
||||
pub fn try_update_contract_admin(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
new_admin: String,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
let new_admin = deps.api.addr_validate(&new_admin)?;
|
||||
|
||||
let res = NYM_PERFORMANCE_CONTRACT_STORAGE
|
||||
.contract_admin
|
||||
.execute_update_admin(deps, info, Some(new_admin))?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn try_submit_performance_results(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
epoch_id: EpochId,
|
||||
data: NodePerformance,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data(deps, &info.sender, epoch_id, data)?;
|
||||
|
||||
// TODO: emit events
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub fn try_batch_submit_performance_results(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
epoch_id: EpochId,
|
||||
data: Vec<NodePerformance>,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
let res = NYM_PERFORMANCE_CONTRACT_STORAGE.batch_submit_performance_results(
|
||||
deps,
|
||||
&info.sender,
|
||||
epoch_id,
|
||||
data,
|
||||
)?;
|
||||
|
||||
let response = Response::new().set_data(to_json_binary(&res)?).add_event(
|
||||
Event::new("batch_performance_submission")
|
||||
.add_attribute("accepted_scores", res.accepted_scores.to_string())
|
||||
.add_attribute(
|
||||
"non_existent_nodes",
|
||||
format!("{:?}", res.non_existent_nodes),
|
||||
),
|
||||
);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn try_authorise_network_monitor(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
address: String,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
let address = deps.api.addr_validate(&address)?;
|
||||
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE.authorise_network_monitor(
|
||||
deps,
|
||||
&env,
|
||||
&info.sender,
|
||||
address,
|
||||
)?;
|
||||
|
||||
// TODO: emit events
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub fn try_retire_network_monitor(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
address: String,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
let address = deps.api.addr_validate(&address)?;
|
||||
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE.retire_network_monitor(deps, env, &info.sender, address)?;
|
||||
|
||||
// TODO: emit events
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub fn try_remove_node_measurements(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
epoch_id: EpochId,
|
||||
node_id: NodeId,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE.remove_node_measurements(
|
||||
deps,
|
||||
&info.sender,
|
||||
epoch_id,
|
||||
node_id,
|
||||
)?;
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub fn try_remove_epoch_measurements(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Response, NymPerformanceContractError> {
|
||||
let res =
|
||||
NYM_PERFORMANCE_CONTRACT_STORAGE.remove_epoch_measurements(deps, &info.sender, epoch_id)?;
|
||||
|
||||
Ok(Response::new().set_data(to_json_binary(&res)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::storage::retrieval_limits;
|
||||
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
|
||||
use cosmwasm_std::from_json;
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts};
|
||||
use nym_performance_contract_common::RemoveEpochMeasurementsResponse;
|
||||
|
||||
#[cfg(test)]
|
||||
mod updating_contract_admin {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
use cw_controllers::AdminError;
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
|
||||
use nym_performance_contract_common::ExecuteMsg;
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let random_acc = test.generate_account();
|
||||
let new_admin = test.generate_account();
|
||||
let res = test
|
||||
.execute_raw(
|
||||
random_acc,
|
||||
ExecuteMsg::UpdateAdmin {
|
||||
admin: new_admin.to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
NymPerformanceContractError::Admin(AdminError::NotAdmin {})
|
||||
);
|
||||
|
||||
let actual_admin = test.admin_unchecked();
|
||||
let res = test.execute_raw(
|
||||
actual_admin.clone(),
|
||||
ExecuteMsg::UpdateAdmin {
|
||||
admin: new_admin.to_string(),
|
||||
},
|
||||
);
|
||||
assert!(res.is_ok());
|
||||
|
||||
let updated_admin = test.admin_unchecked();
|
||||
assert_eq!(new_admin, updated_admin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_providing_valid_address() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let bad_account = "definitely-not-valid-account";
|
||||
let res = test.execute_raw(
|
||||
test.admin_unchecked(),
|
||||
ExecuteMsg::UpdateAdmin {
|
||||
admin: bad_account.to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
let empty_account = "";
|
||||
let res = test.execute_raw(
|
||||
test.admin_unchecked(),
|
||||
ExecuteMsg::UpdateAdmin {
|
||||
admin: empty_account.to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod authorising_network_monitor {
|
||||
use super::*;
|
||||
use crate::testing::init_contract_tester;
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
|
||||
|
||||
#[test]
|
||||
fn requires_valid_address() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let bad_address = "foomp".to_string();
|
||||
let good_address = test.generate_account();
|
||||
|
||||
let env = test.env();
|
||||
let admin = test.admin_msg();
|
||||
|
||||
assert!(try_authorise_network_monitor(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
admin.clone(),
|
||||
bad_address
|
||||
)
|
||||
.is_err());
|
||||
assert!(try_authorise_network_monitor(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
admin,
|
||||
good_address.to_string()
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod retiring_network_monitor {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, PerformanceContractTesterExt};
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt};
|
||||
|
||||
#[test]
|
||||
fn requires_valid_address() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let bad_address = "foomp".to_string();
|
||||
let good_address = test.generate_account();
|
||||
test.authorise_network_monitor(&good_address)?;
|
||||
|
||||
let env = test.env();
|
||||
let admin = test.admin_msg();
|
||||
|
||||
assert!(try_retire_network_monitor(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
admin.clone(),
|
||||
bad_address
|
||||
)
|
||||
.is_err());
|
||||
assert!(try_retire_network_monitor(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
admin,
|
||||
good_address.to_string()
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// panics in tests are fine...
|
||||
#[allow(clippy::panic)]
|
||||
#[test]
|
||||
fn removing_epoch_measurements_returns_binary_data() -> anyhow::Result<()> {
|
||||
let mut tester = init_contract_tester();
|
||||
|
||||
let nm = tester.addr_make("network-monitor");
|
||||
tester.authorise_network_monitor(&nm)?;
|
||||
|
||||
tester.advance_mixnet_epoch()?;
|
||||
for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT {
|
||||
let node_id = tester.bond_dummy_nymnode()?;
|
||||
tester.insert_raw_performance(&nm, node_id, "0.42")?;
|
||||
}
|
||||
|
||||
let admin = tester.admin_msg();
|
||||
let res = try_remove_epoch_measurements(tester.deps_mut(), admin.clone(), 0)?;
|
||||
|
||||
let Some(data) = res.data else {
|
||||
panic!("missing binary response");
|
||||
};
|
||||
let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?;
|
||||
assert!(!deserialised.additional_entries_to_remove_remaining);
|
||||
|
||||
let res = try_remove_epoch_measurements(tester.deps_mut(), admin, 1)?;
|
||||
|
||||
let Some(data) = res.data else {
|
||||
panic!("missing binary response");
|
||||
};
|
||||
let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?;
|
||||
assert!(deserialised.additional_entries_to_remove_remaining);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07
|
||||
MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9
|
||||
|
||||
EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/
|
||||
NYXD=https://rpc.canary-validator.performance.nymte.ch
|
||||
NYM_API=https://canary-api.performance.nymte.ch/api/
|
||||
NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket
|
||||
|
||||
@@ -23,5 +23,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
|
||||
NYXD="https://rpc.nymtech.net"
|
||||
NYM_API="http://127.0.0.1:8000"
|
||||
NYXD_WS="wss://rpc.nymtech.net/websocket"
|
||||
EXPLORER_API="https://explorer.nymtech.net/api/"
|
||||
NYM_VPN_API="https://nymvpn.com/api"
|
||||
|
||||
@@ -24,5 +24,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS=https://mainnet-stats.nymte.ch:8090
|
||||
NYXD=https://rpc.nymtech.net
|
||||
NYM_API=https://validator.nymtech.net/api/
|
||||
NYXD_WS=wss://rpc.nymtech.net/websocket
|
||||
EXPLORER_API=https://explorer.nymtech.net/api/
|
||||
NYM_VPN_API=https://nymvpn.com/api/
|
||||
|
||||
@@ -19,7 +19,6 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865
|
||||
ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9
|
||||
|
||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
||||
EXPLORER_API=https://sandbox-explorer.nymtech.net/api/
|
||||
NYXD=https://rpc.sandbox.nymtech.net
|
||||
NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket
|
||||
NYM_API=https://sandbox-nym-api1.nymtech.net/api/
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals"]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,36 +0,0 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Navbar } from './components/Nav/Navbar'
|
||||
import { Providers } from './providers'
|
||||
|
||||
const App = ({ children }: { children: React.ReactNode }) => (
|
||||
<Providers>
|
||||
<Navbar>{children}</Navbar>
|
||||
</Providers>
|
||||
)
|
||||
|
||||
export { App }
|
||||
@@ -1,407 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {Alert, AlertTitle, Box, Button, Chip, CircularProgress, Grid, Tooltip, Typography} from '@mui/material'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useMainContext } from '@/app/context/main'
|
||||
import { Title } from '@/app/components/Title'
|
||||
import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from "material-react-table";
|
||||
import { useMemo } from "react";
|
||||
import { humanReadableCurrencyToString } from "@/app/utils/currency";
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import { PieChart } from '@mui/x-charts/PieChart';
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useIsMobile } from "@/app/hooks";
|
||||
import { StyledLink } from "@/app/components";
|
||||
|
||||
const AccumulatedRewards = ({account}: { account?: any}) => {
|
||||
const columns = useMemo<
|
||||
MRT_ColumnDef<any>[]
|
||||
>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'accumulated-rewards-data',
|
||||
header: 'Accumulated Rewards Data',
|
||||
columns: [
|
||||
{
|
||||
id: 'node_id',
|
||||
accessorKey: 'node_id',
|
||||
header: 'Node ID',
|
||||
size: 150,
|
||||
Cell: ({ row }) => (<StyledLink
|
||||
color="text.primary"
|
||||
to={`/network-components/nodes/${row.original.node_id}`}>
|
||||
{row.original.node_id}
|
||||
</StyledLink>),
|
||||
},
|
||||
{
|
||||
id: 'node_still_fully_bonded',
|
||||
accessorKey: 'node_still_fully_bonded',
|
||||
header: 'Node still bonded?',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<>{row.original.node_still_fully_bonded ? <CheckCircleOutlineIcon/> :
|
||||
<Typography fontSize="inherit" alignItems="center" display="flex" sx={{ color: theme => theme.palette.warning.main }}>
|
||||
<WarningAmberIcon sx={{ mr: 1 }}/>
|
||||
Unbonded
|
||||
</Typography>}</>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'amount_staked',
|
||||
accessorKey: 'amount_staked',
|
||||
header: 'Amount',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<>{humanReadableCurrencyToString(row.original.amount_staked)}</>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'rewards',
|
||||
accessorKey: 'rewards',
|
||||
header: 'Rewards',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<Typography fontSize="inherit" color="success.main">{humanReadableCurrencyToString(row.original.rewards)}</Typography>
|
||||
)
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data: account?.accumulated_rewards || [],
|
||||
enableFullScreenToggle: false,
|
||||
})
|
||||
|
||||
return (<MaterialReactTable table={table} />);
|
||||
}
|
||||
|
||||
const DelegationHistory = ({account}: { account?: any}) => {
|
||||
const columns = useMemo<
|
||||
MRT_ColumnDef<any>[]
|
||||
>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'delegation-history-data',
|
||||
header: 'Delegation History',
|
||||
columns: [
|
||||
{
|
||||
id: 'node_id',
|
||||
accessorKey: 'node_id',
|
||||
header: 'Node ID',
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
id: 'delegated',
|
||||
accessorKey: 'delegated',
|
||||
header: 'Amount',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<>{humanReadableCurrencyToString(row.original.delegated)}</>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'height',
|
||||
accessorKey: 'height',
|
||||
header: 'Delegated at height',
|
||||
width: 150,
|
||||
Cell: ({ row }) => (
|
||||
<>{row.original.height}</>
|
||||
)
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data: account?.delegations || [],
|
||||
enableFullScreenToggle: false,
|
||||
})
|
||||
|
||||
return (<MaterialReactTable table={table} />);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows account details
|
||||
*/
|
||||
const PageAccountWithState = ({ account }: {
|
||||
account?: any;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const pieChartData = React.useMemo(() => {
|
||||
if(!account) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
|
||||
const nymBalance = Number.parseFloat(account.balances.find((b: any) => b.denom === "unym")?.amount || "0") / 1e6;
|
||||
|
||||
if(nymBalance > 0) {
|
||||
parts.push({label: "Spendable", value: nymBalance, color: theme.palette.primary.main});
|
||||
}
|
||||
|
||||
if(account.vesting_account) {
|
||||
if (`${account.vesting_account.locked?.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.vesting_account.locked.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Vesting locked",
|
||||
value,
|
||||
color: 'red'
|
||||
});
|
||||
}
|
||||
}
|
||||
if (`${account.vesting_account.spendable?.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.vesting_account.spendable.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Vesting spendable",
|
||||
value,
|
||||
color: theme.palette.primary.light
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (account.claimable_rewards &&`${account.claimable_rewards.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.claimable_rewards.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Claimable delegation rewards",
|
||||
value,
|
||||
color: theme.palette.success.light
|
||||
});
|
||||
}
|
||||
}
|
||||
if (account.operator_rewards && `${account.operator_rewards.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.operator_rewards.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Claimable operator rewards",
|
||||
value,
|
||||
color: theme.palette.success.dark
|
||||
});
|
||||
}
|
||||
}
|
||||
if (account.total_delegations && `${account.total_delegations.amount}` !== "0") {
|
||||
const value = Number.parseFloat(account.total_delegations.amount) / 1e6;
|
||||
if(value > 0) {
|
||||
parts.push({
|
||||
label: "Total delegations",
|
||||
value,
|
||||
color: '#888'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}, [account]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
<Box overflow="scroll">
|
||||
<Title text={`Account ${account.address}`} />
|
||||
</Box>
|
||||
|
||||
<Box mt={4} sx={{ maxWidth: "600px" }}>
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: pieChartData,
|
||||
innerRadius: 40,
|
||||
outerRadius: 80,
|
||||
cy: isMobile ? 200 : undefined,
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
slotProps={isMobile ? {
|
||||
legend: { position: { vertical: "top", horizontal: "right" } }
|
||||
} : undefined}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box mt={4}>
|
||||
<TableContainer component={Paper} sx={{ maxWidth: "400px" }}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow sx={{ color: theme => theme.palette.primary.main }}>
|
||||
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
|
||||
<strong>Spendable Balance</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{account.balances.map((b: any) => (<strong key={`balance-${b.denom}`}>{humanReadableCurrencyToString(b)}<br/></strong>))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row">
|
||||
Total delegations
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{humanReadableCurrencyToString(account.total_delegations)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{account.claimable_rewards && <TableRow sx={{ color: theme => theme.palette.success.light }}>
|
||||
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
|
||||
Claimable delegation rewards
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.claimable_rewards)}
|
||||
</TableCell>
|
||||
</TableRow>}
|
||||
{account.operator_rewards && `${account.operator_rewards.amount}` !== "0" && <TableRow sx={{ color: theme => theme.palette.success.light }}>
|
||||
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
|
||||
Claimable operator rewards
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.operator_rewards)}
|
||||
</TableCell>
|
||||
</TableRow>}
|
||||
{account.vesting_account && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" colSpan={2}>
|
||||
Vesting account
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{`${account.vesting_account.locked.amount}` !== "0" &&
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
|
||||
Locked
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.vesting_account.locked)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{`${account.vesting_account.vested.amount}` !== "0" &&
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
|
||||
Vested
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.vesting_account.vested)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{`${account.vesting_account.vesting.amount}` !== "0" &&
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
|
||||
Vesting
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.vesting_account.vesting)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{`${account.vesting_account.spendable.amount}` !== "0" &&
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ pl: 4 }}>
|
||||
Spendable
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
{humanReadableCurrencyToString(account.vesting_account.spendable)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" sx={{ color: "inherit" }}>
|
||||
<h3>Total value</h3>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: "inherit" }}>
|
||||
<h3>{humanReadableCurrencyToString(account.total_value)}</h3>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<AccumulatedRewards account={account}/>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DelegationHistory account={account}/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard component to handle loading and not found states
|
||||
*/
|
||||
const PageAccountDetailGuard = ({ account } : { account: string }) => {
|
||||
const [accountDetails, setAccountDetails] = React.useState<any>();
|
||||
const [isLoading, setLoading] = React.useState<boolean>(true);
|
||||
const [error, setError] = React.useState<string>();
|
||||
const { fetchAccountById } = useMainContext()
|
||||
const { id } = useParams()
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
if(typeof(id) === "string") {
|
||||
try {
|
||||
const res = await fetchAccountById(account);
|
||||
setAccountDetails(res);
|
||||
} catch(e: any) {
|
||||
setError(e.message);
|
||||
}
|
||||
finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [id])
|
||||
|
||||
if (isLoading) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Account not found</AlertTitle>
|
||||
Sorry, we could not find the account <code>{id || ''}</code>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return <PageAccountWithState account={accountDetails} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the account details based on the `id` in the address URL
|
||||
*/
|
||||
const PageAccountDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
if (!id || typeof id !== 'string') {
|
||||
return (
|
||||
<Alert severity="error">Oh no! Could not find that account</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageAccountDetailGuard account={id} />
|
||||
)
|
||||
}
|
||||
|
||||
export default PageAccountDetail
|
||||
@@ -1,39 +0,0 @@
|
||||
// master APIs
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_EXPLORER_API_URL || 'https://explorer.nymtech.net/api/v1';
|
||||
export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https://validator.nymtech.net';
|
||||
|
||||
export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net';
|
||||
|
||||
export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net';
|
||||
export const BLOCK_EXPLORER_BASE_URL = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru';
|
||||
|
||||
// specific API routes
|
||||
export const OVERVIEW_API = `${API_BASE_URL}/overview`;
|
||||
export const MIXNODE_PING = `${API_BASE_URL}/ping`;
|
||||
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
|
||||
export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
|
||||
export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`;
|
||||
export const BLOCK_API = `${NYX_RPC_BASE_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
||||
export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this
|
||||
export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`;
|
||||
export const TEMP_UNSTABLE_NYM_NODES = `${API_BASE_URL}/tmp/unstable/nym-nodes`;
|
||||
export const TEMP_UNSTABLE_ACCOUNT = `${API_BASE_URL}/tmp/unstable/account`;
|
||||
export const NYM_API_NODE_UPTIME = `${NYM_API_BASE_URL}/api/v1/nym-nodes/uptime-history`;
|
||||
export const NYM_API_NODE_PERFORMANCE = `${NYM_API_BASE_URL}/api/v1/nym-nodes/performance-history`;
|
||||
|
||||
export const LEGACY_MIXNODES_API = `${API_BASE_URL}/tmp/unstable/legacy-mixnode-bonds`;
|
||||
export const LEGACY_GATEWAYS_API = `${API_BASE_URL}/tmp/unstable/legacy-gateway-bonds`;
|
||||
|
||||
// errors
|
||||
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
|
||||
|
||||
export const NYM_WEBSITE = 'https://nymtech.net';
|
||||
|
||||
export const EXPLORER_FOR_ACCOUNTS = ''; // set to empty to use this Nym Explorer and NOT an external one
|
||||
|
||||
export const NYM_MIXNET_CONTRACT =
|
||||
process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr';
|
||||
export const COSMOS_KIT_USE_CHAIN = process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || 'sandbox';
|
||||
export const WALLET_CONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || '';
|
||||
@@ -1,217 +0,0 @@
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import {
|
||||
API_BASE_URL,
|
||||
BLOCK_API,
|
||||
COUNTRY_DATA_API,
|
||||
UPTIME_STORY_API_GATEWAY,
|
||||
MIXNODE_API,
|
||||
MIXNODE_PING,
|
||||
MIXNODES_API,
|
||||
OVERVIEW_API,
|
||||
UPTIME_STORY_API,
|
||||
VALIDATORS_API,
|
||||
SERVICE_PROVIDERS,
|
||||
TEMP_UNSTABLE_NYM_NODES,
|
||||
NYM_API_NODE_UPTIME,
|
||||
NYM_API_NODE_PERFORMANCE,
|
||||
TEMP_UNSTABLE_ACCOUNT,
|
||||
LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API,
|
||||
} from './constants';
|
||||
|
||||
import {
|
||||
CountryDataResponse,
|
||||
DelegationsResponse,
|
||||
UniqDelegationsResponse,
|
||||
GatewayReportResponse,
|
||||
UptimeStoryResponse,
|
||||
MixNodeDescriptionResponse,
|
||||
MixNodeResponse,
|
||||
MixNodeResponseItem,
|
||||
MixnodeStatus,
|
||||
MixNodeEconomicDynamicsStatsResponse,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
SummaryOverviewResponse,
|
||||
ValidatorsResponse,
|
||||
Environment,
|
||||
GatewayBondAnnotated,
|
||||
GatewayBond,
|
||||
DirectoryServiceProvider,
|
||||
LocatedGateway,
|
||||
} from '../typeDefs/explorer-api';
|
||||
|
||||
function getFromCache(key: string) {
|
||||
const ts = Number(localStorage.getItem('ts'));
|
||||
const hasExpired = Date.now() - ts > 5000;
|
||||
const curr = localStorage.getItem(key);
|
||||
if (curr && !hasExpired) {
|
||||
return JSON.parse(curr);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function storeInCache(key: string, data: any) {
|
||||
localStorage.setItem(key, data);
|
||||
localStorage.setItem('ts', Date.now().toString());
|
||||
}
|
||||
|
||||
export class Api {
|
||||
static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => {
|
||||
const cache = getFromCache('overview-summary');
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
const res = await fetch(`${OVERVIEW_API}/summary`);
|
||||
const json: SummaryOverviewResponse = await res.json();
|
||||
|
||||
if (json.nymnodes?.roles) {
|
||||
json.mixnodes.count += json.nymnodes.roles.mixnode;
|
||||
json.gateways.count += json.nymnodes.roles.entry;
|
||||
json.gateways.count += Math.max(json.nymnodes.roles.exit_ipr, json.nymnodes.roles.exit_nr);
|
||||
}
|
||||
|
||||
storeInCache('overview-summary', JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodes = async (): Promise<MixNodeResponse> => {
|
||||
const cachedMixnodes = getFromCache('mixnodes');
|
||||
if (cachedMixnodes) {
|
||||
return cachedMixnodes;
|
||||
}
|
||||
|
||||
const res = await fetch(LEGACY_MIXNODES_API);
|
||||
const json = await res.json();
|
||||
storeInCache('mixnodes', JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => {
|
||||
const cachedMixnodes = getFromCache(`mixnodes-${status}`);
|
||||
if (cachedMixnodes) {
|
||||
return cachedMixnodes;
|
||||
}
|
||||
const res = await fetch(`${MIXNODES_API}/active-set/${status}`);
|
||||
const json = await res.json();
|
||||
storeInCache(`mixnodes-${status}`, JSON.stringify(json));
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => {
|
||||
const response = await fetch(`${MIXNODE_API}/${id}`);
|
||||
|
||||
// when the mixnode is not found, returned undefined
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
static fetchGateways = async (): Promise<LocatedGateway[]> => {
|
||||
// const res = await fetch(GATEWAYS_API);
|
||||
// const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json();
|
||||
// const res2 = await fetch(GATEWAYS_EXPLORER_API);
|
||||
// const locatedGateways: LocatedGateway[] = await res2.json();
|
||||
// const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner');
|
||||
// return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({
|
||||
// ...gateway_bond,
|
||||
// node_performance,
|
||||
// location: locatedGatewaysByOwner[gateway_bond.owner]?.location,
|
||||
// }));
|
||||
|
||||
const res = await fetch(LEGACY_GATEWAYS_API);
|
||||
const locatedGateways: LocatedGateway[] = await res.json();
|
||||
return locatedGateways;
|
||||
};
|
||||
|
||||
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json();
|
||||
|
||||
static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json();
|
||||
|
||||
static fetchValidators = async (): Promise<ValidatorsResponse> => {
|
||||
const res = await fetch(VALIDATORS_API);
|
||||
const json = await res.json();
|
||||
return json.result;
|
||||
};
|
||||
|
||||
static fetchBlock = async (): Promise<number> => {
|
||||
const res = await fetch(BLOCK_API);
|
||||
const json = await res.json();
|
||||
const { height } = json.result.block.header;
|
||||
return height;
|
||||
};
|
||||
|
||||
static fetchCountryData = async (): Promise<CountryDataResponse> => {
|
||||
const result: CountryDataResponse = {};
|
||||
const res = await fetch(COUNTRY_DATA_API);
|
||||
const json = await res.json();
|
||||
Object.keys(json).forEach((ISO3) => {
|
||||
result[ISO3] = { ISO3, nodes: json[ISO3] };
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/delegations`)).json();
|
||||
|
||||
static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json();
|
||||
|
||||
static fetchStatsById = async (id: string): Promise<StatsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/stats`)).json();
|
||||
|
||||
static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
|
||||
|
||||
static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> =>
|
||||
(await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json();
|
||||
|
||||
static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json();
|
||||
|
||||
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
|
||||
|
||||
static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => {
|
||||
const res = await fetch(SERVICE_PROVIDERS);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchNodes = async () => {
|
||||
const res = await fetch(TEMP_UNSTABLE_NYM_NODES);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
static fetchNodeById = async (id: number) => {
|
||||
const res = await fetch(`${TEMP_UNSTABLE_NYM_NODES}/${id}`);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
static fetchNymNodeUptimeHistoryById = async (id: number | string) => {
|
||||
const res = await fetch(`${NYM_API_NODE_UPTIME}/${id}`)
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
static fetchNymNodePerformanceById = async (id: number | string) => {
|
||||
const res = await fetch(`${NYM_API_NODE_PERFORMANCE}/${id}`)
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
static fetchAccountById = async (id: string) => {
|
||||
const res = await fetch(`${TEMP_UNSTABLE_ACCOUNT}/${id}`);
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
export const getEnvironment = (): Environment => {
|
||||
const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env;
|
||||
return matchEnv('sandbox') || matchEnv('qa') || 'mainnet';
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user