Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35b7f655c6 | |||
| bf12ce2477 | |||
| 9ed80ebf52 | |||
| 172171f1f2 | |||
| 0c83ae2408 | |||
| 0d031875f6 | |||
| e6103e4c43 | |||
| e32c042c8d | |||
| 2209e8ac04 | |||
| 6f2a3d9033 | |||
| 0530967807 | |||
| 9db748e8dd | |||
| 82ed88e26e | |||
| 594174827d | |||
| f6f364c551 | |||
| f648349e82 | |||
| 4fb78c3737 | |||
| f208855bc8 | |||
| 60426b8c45 | |||
| 9792a8829b | |||
| 5b23429415 | |||
| 89de989ad1 | |||
| 97068b2aac | |||
| 0e3e5c27f3 | |||
| 01e3c8206b | |||
| ef20b8c7d1 | |||
| 61af16784b | |||
| caf21076c9 | |||
| 1672135308 | |||
| c07ef0253d | |||
| cc799b69d3 | |||
| dd4bbc0708 | |||
| 7b77091fb1 | |||
| 6581ebf235 | |||
| 82ace6d27b | |||
| e362207583 | |||
| 68caecff35 | |||
| 2fae4414d2 | |||
| 6eca09b904 | |||
| 7ab821cb11 | |||
| 9904f6b17c | |||
| 5e0eeeddd6 | |||
| b6df383584 | |||
| 3f00e2c317 | |||
| 3cdda8fdfd | |||
| 33f47ef36e | |||
| 180802feb8 | |||
| 4077717d3a | |||
| bc3df31518 | |||
| 61d6acace8 | |||
| abb4e3f988 | |||
| c5488337da | |||
| f06eefe184 | |||
| e86fa8fc7f |
@@ -15,10 +15,8 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git python3 && sudo apt-get update --fix-missing
|
||||
- name: Install pip3
|
||||
run: sudo apt install -y python3-pip
|
||||
- name: Install Python3 modules
|
||||
run: sudo pip3 install pandas tabulate
|
||||
run: sudo apt install -y python3-pandas python3-tabulate
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
@@ -41,6 +39,8 @@ jobs:
|
||||
|
||||
- name: Install project dependencies
|
||||
run: pnpm i
|
||||
- name: Generate llms-full.txt
|
||||
run: pnpm run generate:llms
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
- name: Generate sitemap
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api -- -D warnings
|
||||
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api --exclude nym-node-status-agent --exclude nym-node-status-client -- -D warnings
|
||||
|
||||
- name: Clippy (non-macos)
|
||||
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
|
||||
@@ -104,14 +104,15 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --workspace --exclude nym-gateway-probe --exclude nym-node-status-api --exclude nym-node-status-agent --exclude nym-node-status-client
|
||||
|
||||
# only build on linux because of wg FFI bindings of its dependency (network probe)
|
||||
- name: Build nym-node-status-api (linux only)
|
||||
# Build Go FFI-dependent crates separately (requires Go, only available on Linux CI)
|
||||
- name: Build nym-node-status-api and nym-node-status-agent (linux only)
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: -p nym-node-status-api
|
||||
args: -p nym-node-status-api -p nym-node-status-agent
|
||||
|
||||
- name: Build all examples
|
||||
if: contains(matrix.os, 'linux')
|
||||
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
branches-ignore: [master]
|
||||
paths:
|
||||
- "documentation/docs/**"
|
||||
- "sdk/typescript/packages/sdk/src/**"
|
||||
- "sdk/typescript/packages/mix-fetch/src/**"
|
||||
- ".github/workflows/ci-docs.yml"
|
||||
|
||||
jobs:
|
||||
@@ -20,10 +22,8 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git python3 && sudo apt-get update --fix-missing
|
||||
- name: Install pip3
|
||||
run: sudo apt install -y python3-pip
|
||||
- name: Install Python3 modules
|
||||
run: sudo pip3 install pandas tabulate
|
||||
run: sudo apt install -y python3-pandas python3-tabulate
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install -y rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
@@ -44,8 +44,27 @@ jobs:
|
||||
command: build
|
||||
args: --workspace --release
|
||||
|
||||
- name: Check if TypeScript SDK source changed
|
||||
id: check-ts-sdk
|
||||
run: |
|
||||
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-fetch)/src/'; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
working-directory: ${{ github.workspace }}
|
||||
|
||||
- name: Regenerate TypeDoc API reference
|
||||
if: steps.check-ts-sdk.outputs.changed == 'true'
|
||||
run: |
|
||||
npm install -g typedoc@0.25.13 typedoc-plugin-markdown@4.0.3
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
||||
|
||||
- name: Install project dependencies
|
||||
run: pnpm i
|
||||
- name: Generate llms-full.txt
|
||||
run: pnpm run generate:llms
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
- name: Generate sitemap
|
||||
|
||||
@@ -4,6 +4,40 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.8-urda] (2026-04-20)
|
||||
|
||||
- Include all gateways in the returned list ([#6649])
|
||||
- Optimize GW probe in NS agent ([#6636])
|
||||
- Max/sdk docrs ([#6566])
|
||||
- Max/sdk stream wrapper ([#6320])
|
||||
|
||||
[#6649]: https://github.com/nymtech/nym/pull/6649
|
||||
[#6636]: https://github.com/nymtech/nym/pull/6636
|
||||
[#6566]: https://github.com/nymtech/nym/pull/6566
|
||||
[#6320]: https://github.com/nymtech/nym/pull/6320
|
||||
|
||||
## [2026.7-tola] (2026-04-07)
|
||||
|
||||
- Simon/ecash contract serde fix ([#6634])
|
||||
- Update Fallback IP for Nym API ([#6622])
|
||||
- Nym Node spam logging ([#6621])
|
||||
- feat: multiple deposit prices ([#6608])
|
||||
- move format_debug_bytes in common crate ([#6580])
|
||||
- bugfix: make sure client keys are generated before requesting credentials ([#6579])
|
||||
- Fix socks5 GW probe regression ([#6576])
|
||||
- Max/lp stream framing ([#6573])
|
||||
- HTTP domain rotation conditions ([#6570])
|
||||
|
||||
[#6634]: https://github.com/nymtech/nym/pull/6634
|
||||
[#6622]: https://github.com/nymtech/nym/pull/6622
|
||||
[#6621]: https://github.com/nymtech/nym/pull/6621
|
||||
[#6608]: https://github.com/nymtech/nym/pull/6608
|
||||
[#6580]: https://github.com/nymtech/nym/pull/6580
|
||||
[#6579]: https://github.com/nymtech/nym/pull/6579
|
||||
[#6576]: https://github.com/nymtech/nym/pull/6576
|
||||
[#6573]: https://github.com/nymtech/nym/pull/6573
|
||||
[#6570]: https://github.com/nymtech/nym/pull/6570
|
||||
|
||||
## [2026.6-stilton] (2026-03-25)
|
||||
|
||||
- lp fixes ([#6601])
|
||||
|
||||
Generated
+44
-33
@@ -2534,7 +2534,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3589,7 +3589,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"thiserror 2.0.12",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
@@ -3614,7 +3614,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"rand 0.9.2",
|
||||
"resolv-conf",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"smallvec",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
@@ -3871,7 +3871,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"rustls-native-certs 0.8.3",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -5448,7 +5448,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5547,7 +5547,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.76"
|
||||
version = "1.1.78"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5792,7 +5792,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.73"
|
||||
version = "1.1.75"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5875,7 +5875,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.73"
|
||||
version = "1.1.75"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -6687,6 +6687,7 @@ version = "1.20.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"clap",
|
||||
"futures",
|
||||
@@ -6711,7 +6712,6 @@ dependencies = [
|
||||
"nym-lp",
|
||||
"nym-network-defaults",
|
||||
"nym-node-requests",
|
||||
"nym-node-status-client",
|
||||
"nym-registration-client",
|
||||
"nym-registration-common",
|
||||
"nym-sdk",
|
||||
@@ -6849,6 +6849,7 @@ dependencies = [
|
||||
"nym-network-defaults",
|
||||
"once_cell",
|
||||
"reqwest 0.13.1",
|
||||
"rustls 0.23.37",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_plain",
|
||||
@@ -6956,12 +6957,15 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-service-provider-requests-common",
|
||||
"nym-sphinx",
|
||||
"pnet_packet",
|
||||
"rand 0.8.5",
|
||||
"semver 1.0.27",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6984,7 +6988,7 @@ dependencies = [
|
||||
"nym-exit-policy",
|
||||
"nym-id",
|
||||
"nym-ip-packet-requests",
|
||||
"nym-kcp",
|
||||
"nym-lp",
|
||||
"nym-network-defaults",
|
||||
"nym-network-requester",
|
||||
"nym-sdk",
|
||||
@@ -7086,6 +7090,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"libcrux-psq",
|
||||
"num_enum",
|
||||
"nym-common",
|
||||
"nym-crypto",
|
||||
"nym-kkt",
|
||||
"nym-kkt-ciphersuite",
|
||||
@@ -7279,7 +7284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.74"
|
||||
version = "1.1.76"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -7329,7 +7334,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.28.0"
|
||||
version = "1.30.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -7466,14 +7471,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-agent"
|
||||
version = "1.1.3"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"futures",
|
||||
"nym-bin-common",
|
||||
"nym-crypto",
|
||||
"nym-gateway-probe",
|
||||
"nym-node-status-client",
|
||||
"nym-sdk",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"serde_json",
|
||||
@@ -7485,7 +7492,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.3.0"
|
||||
version = "4.5.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -7550,9 +7557,9 @@ version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"bs58",
|
||||
"nym-credentials",
|
||||
"nym-crypto",
|
||||
"nym-gateway-probe",
|
||||
"reqwest 0.13.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -7790,6 +7797,8 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-gateway-requests",
|
||||
"nym-http-api-client",
|
||||
"nym-ip-packet-requests",
|
||||
"nym-lp",
|
||||
"nym-network-defaults",
|
||||
"nym-ordered-buffer",
|
||||
"nym-service-providers-common",
|
||||
@@ -7802,8 +7811,10 @@ dependencies = [
|
||||
"nym-topology",
|
||||
"nym-validator-client",
|
||||
"parking_lot",
|
||||
"pnet_packet",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.13.1",
|
||||
"semver 1.0.27",
|
||||
"serde",
|
||||
"tap",
|
||||
"tempfile",
|
||||
@@ -7875,7 +7886,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.73"
|
||||
version = "1.1.75"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -8673,7 +8684,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.38"
|
||||
version = "0.1.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -9652,7 +9663,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
@@ -9673,7 +9684,7 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.12",
|
||||
@@ -9940,7 +9951,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"rustls-native-certs 0.8.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@@ -9981,7 +9992,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
@@ -10241,16 +10252,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.29"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.4",
|
||||
"rustls-webpki 0.103.9",
|
||||
"subtle 2.6.1",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -10331,14 +10342,14 @@ dependencies = [
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"rustls-native-certs 0.8.3",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.103.4",
|
||||
"rustls-webpki 0.103.9",
|
||||
"security-framework 3.6.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10370,9 +10381,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.4"
|
||||
version = "0.103.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
@@ -11203,7 +11214,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -12025,7 +12036,7 @@ version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
|
||||
dependencies = [
|
||||
"rustls 0.23.29",
|
||||
"rustls 0.23.37",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -12843,7 +12854,7 @@ dependencies = [
|
||||
"serde",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
"toml 0.8.23",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"uniffi_internal_macros 0.31.0",
|
||||
"uniffi_meta 0.31.0",
|
||||
"uniffi_pipeline 0.31.0",
|
||||
@@ -12952,7 +12963,7 @@ dependencies = [
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.106",
|
||||
"toml 0.8.23",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"uniffi_meta 0.31.0",
|
||||
]
|
||||
|
||||
|
||||
+1
-1
@@ -184,7 +184,6 @@ default-members = [
|
||||
"nym-api",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-node",
|
||||
"nym-node-status-api/nym-node-status-agent",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
@@ -334,6 +333,7 @@ rayon = "1.5.1"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
rs_merkle = "1.5.0"
|
||||
rustls = { version = "0.23.37", default-features = false }
|
||||
schemars = "0.8.22"
|
||||
semver = "1.0.26"
|
||||
serde = "1.0.219"
|
||||
|
||||
@@ -60,7 +60,8 @@ packages:
|
||||
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
|
||||
###############################################################################
|
||||
|
||||
# nym_version: "v2025.21-mozzarella"
|
||||
# To use particular version instead of Latest, provide in such form:
|
||||
# nym_version: "nym-binaries-v2026.7-tola"
|
||||
|
||||
## NOTE:
|
||||
## if you want to pin Nym to a specific version instead of using the
|
||||
@@ -117,4 +118,4 @@ packages:
|
||||
|
||||
# enable_writeback_tuning: true
|
||||
# writeback_dirty_writeback_centisecs: 1500
|
||||
# writeback_dirty_expire_centisecs: 6000
|
||||
# writeback_dirty_expire_centisecs: 6000
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
---
|
||||
- name: Configure tunnel manager
|
||||
- name: Ensure nym binaries directory exists
|
||||
file:
|
||||
path: /root/nym-binaries
|
||||
state: directory
|
||||
mode: "0755"
|
||||
tags:
|
||||
- tunnel
|
||||
- network_tunnel_manager
|
||||
become: true
|
||||
command:
|
||||
cmd: "/root/nym-binaries/network-tunnel-manager.sh {{ item }}"
|
||||
- ntm
|
||||
|
||||
- name: Download network tunnel manager
|
||||
get_url:
|
||||
url: "{{ tunnel_manager_url }}"
|
||||
dest: /root/nym-binaries/network-tunnel-manager.sh
|
||||
mode: "0755"
|
||||
force: yes
|
||||
tags:
|
||||
- tunnel
|
||||
- network_tunnel_manager
|
||||
- ntm
|
||||
|
||||
- name: Run network tunnel manager
|
||||
command: "/root/nym-binaries/network-tunnel-manager.sh {{ item }}"
|
||||
loop:
|
||||
- complete_networking_configuration
|
||||
register: tunnel_mgr
|
||||
failed_when: false
|
||||
tags:
|
||||
- tunnel
|
||||
- network_tunnel_manager
|
||||
- ntm
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.73"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
version = "1.1.75"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.85"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.73"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
version = "1.1.75"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.85"
|
||||
license.workspace = true
|
||||
|
||||
@@ -34,7 +34,7 @@ where
|
||||
let signing_key = ed25519::PrivateKey::new(&mut rng);
|
||||
let expiration = expiration.unwrap_or_else(ecash_default_expiration_date);
|
||||
|
||||
let deposit_amount = client.get_required_deposit_amount().await?;
|
||||
let deposit_amount = client.get_default_deposit_amount().await?;
|
||||
info!("we'll need to deposit {deposit_amount} to obtain the ticketbook");
|
||||
let result = client
|
||||
.make_ticketbook_deposit(
|
||||
|
||||
@@ -342,7 +342,7 @@ impl SendWithoutResponse for Client {
|
||||
sending_res.map_err(|err| {
|
||||
match err {
|
||||
TrySendError::Full(_) => {
|
||||
warn!(
|
||||
trace!(
|
||||
event = "mixclient.try_send",
|
||||
peer = %address,
|
||||
result = "full_dropped",
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmwasm_std::Coin;
|
||||
use nym_ecash_contract_common::deposit::LatestDepositResponse;
|
||||
use nym_ecash_contract_common::deposit_statistics::DepositsStatistics;
|
||||
use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg;
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -17,6 +18,9 @@ pub use nym_ecash_contract_common::blacklist::{
|
||||
pub use nym_ecash_contract_common::deposit::{
|
||||
Deposit, DepositData, DepositId, DepositResponse, PagedDepositsResponse,
|
||||
};
|
||||
pub use nym_ecash_contract_common::reduced_deposit::{
|
||||
WhitelistedAccount, WhitelistedAccountsResponse,
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
@@ -42,8 +46,18 @@ pub trait EcashQueryClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_required_deposit_amount(&self) -> Result<Coin, NyxdError> {
|
||||
self.query_ecash_contract(EcashQueryMsg::GetRequiredDepositAmount {})
|
||||
async fn get_default_deposit_amount(&self) -> Result<Coin, NyxdError> {
|
||||
self.query_ecash_contract(EcashQueryMsg::GetDefaultDepositAmount {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_reduced_deposit_amount(&self, address: String) -> Result<Option<Coin>, NyxdError> {
|
||||
self.query_ecash_contract(EcashQueryMsg::GetReducedDepositAmount { address })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_whitelisted_accounts(&self) -> Result<WhitelistedAccountsResponse, NyxdError> {
|
||||
self.query_ecash_contract(EcashQueryMsg::GetAllWhitelistedAccounts {})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -65,6 +79,11 @@ pub trait EcashQueryClient {
|
||||
self.query_ecash_contract(EcashQueryMsg::GetDepositsPaged { start_after, limit })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_deposits_statistics(&self) -> Result<DepositsStatistics, NyxdError> {
|
||||
self.query_ecash_contract(EcashQueryMsg::GetDepositsStatistics {})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -122,10 +141,17 @@ mod tests {
|
||||
EcashQueryMsg::GetDepositsPaged { limit, start_after } => {
|
||||
client.get_deposits_paged(start_after, limit).ignore()
|
||||
}
|
||||
EcashQueryMsg::GetRequiredDepositAmount {} => {
|
||||
client.get_required_deposit_amount().ignore()
|
||||
EcashQueryMsg::GetDefaultDepositAmount {} => {
|
||||
client.get_default_deposit_amount().ignore()
|
||||
}
|
||||
EcashQueryMsg::GetReducedDepositAmount { address } => {
|
||||
client.get_reduced_deposit_amount(address).ignore()
|
||||
}
|
||||
EcashQueryMsg::GetAllWhitelistedAccounts {} => {
|
||||
client.get_all_whitelisted_accounts().ignore()
|
||||
}
|
||||
EcashQueryMsg::GetLatestDeposit {} => client.get_latest_deposit().ignore(),
|
||||
EcashQueryMsg::GetDepositsStatistics {} => client.get_deposits_statistics().ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+42
-2
@@ -62,13 +62,47 @@ pub trait EcashSigningClient {
|
||||
new_deposit: Coin,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = EcashExecuteMsg::UpdateDepositValue {
|
||||
let req = EcashExecuteMsg::UpdateDefaultDepositValue {
|
||||
new_deposit: new_deposit.into(),
|
||||
};
|
||||
self.execute_ecash_contract(fee, req, "Ecash::UpdateDepositValue".to_string(), vec![])
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_reduced_deposit_price(
|
||||
&self,
|
||||
address: String,
|
||||
deposit: Coin,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = EcashExecuteMsg::SetReducedDepositPrice {
|
||||
address,
|
||||
deposit: deposit.into(),
|
||||
};
|
||||
self.execute_ecash_contract(
|
||||
fee,
|
||||
req,
|
||||
"Ecash::SetReducedDepositPrice".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_reduced_deposit_price(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = EcashExecuteMsg::RemoveReducedDepositPrice { address };
|
||||
self.execute_ecash_contract(
|
||||
fee,
|
||||
req,
|
||||
"Ecash::RemoveReducedDepositPrice".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn propose_for_blacklist(
|
||||
&self,
|
||||
public_key: String,
|
||||
@@ -141,9 +175,15 @@ mod tests {
|
||||
.ignore(),
|
||||
ExecuteMsg::RedeemTickets { .. } => unimplemented!(), // no redeem tickets method for the client
|
||||
ExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(),
|
||||
ExecuteMsg::UpdateDepositValue { new_deposit } => client
|
||||
ExecuteMsg::UpdateDefaultDepositValue { new_deposit } => client
|
||||
.update_deposit_value(new_deposit.into(), None)
|
||||
.ignore(),
|
||||
ExecuteMsg::SetReducedDepositPrice { address, deposit } => client
|
||||
.set_reduced_deposit_price(address, deposit.into(), None)
|
||||
.ignore(),
|
||||
ExecuteMsg::RemoveReducedDepositPrice { address } => {
|
||||
client.remove_reduced_deposit_price(address, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,14 @@ use cosmwasm_std::Coin;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct PoolCounters {
|
||||
/// Represents the total amount of funds deposited into the contract.
|
||||
pub total_deposited: Coin,
|
||||
|
||||
/// Represents the total amount of funds redeemed from the contract that got transferred into the holding account.
|
||||
pub total_redeemed: Coin,
|
||||
|
||||
/// Represents the total amount of tickets requested to be redeemed from the contract and get moved into the holding account,
|
||||
/// after that functionality got disabled.
|
||||
#[serde(default)]
|
||||
pub tickets_requested_and_not_redeemed: u64,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Coin;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Aggregate statistics about all deposits made through the ecash contract.
|
||||
#[cw_serde]
|
||||
pub struct DepositsStatistics {
|
||||
/// Total number of deposits ever made (at any price tier),
|
||||
/// derived from the deposit id counter.
|
||||
pub total_deposits_made: u32,
|
||||
|
||||
/// Total value of all deposits ever made (at any price tier),
|
||||
/// sourced from `PoolCounters::total_deposited`.
|
||||
pub total_deposited: Coin,
|
||||
|
||||
/// Number of deposits made at the default (non-reduced) price.
|
||||
pub total_deposits_made_with_default_price: u32,
|
||||
|
||||
/// Total value deposited at the default price.
|
||||
pub total_deposited_with_default_price: Coin,
|
||||
|
||||
/// Number of deposits made at any custom (reduced) price, summed across all whitelisted accounts.
|
||||
pub total_deposits_made_with_custom_price: u32,
|
||||
|
||||
/// Total value deposited at custom prices, summed across all whitelisted accounts.
|
||||
pub total_deposited_with_custom_price: Coin,
|
||||
|
||||
/// Per-account breakdown of deposit counts for whitelisted addresses.
|
||||
// note: we use String for addressing due to serialisation incompatibility
|
||||
pub deposits_made_with_custom_price: HashMap<String, u32>,
|
||||
|
||||
/// Per-account breakdown of deposited amounts for whitelisted addresses.
|
||||
// note: we use String for addressing due to serialisation incompatibility
|
||||
pub deposited_with_custom_price: HashMap<String, Coin>,
|
||||
}
|
||||
@@ -65,4 +65,26 @@ pub enum EcashContractError {
|
||||
|
||||
#[error("the account blacklisting hasn't been fully implemented yet")]
|
||||
UnimplementedBlacklisting,
|
||||
|
||||
#[error("reduced deposit must use the same denom as the default deposit (expected '{expected}', got '{got}')")]
|
||||
InvalidReducedDepositDenom { expected: String, got: String },
|
||||
|
||||
#[error(
|
||||
"reduced deposit amount ({reduced}) must be strictly less than the default ({default})"
|
||||
)]
|
||||
ReducedDepositNotReduced {
|
||||
reduced: cosmwasm_std::Uint128,
|
||||
default: cosmwasm_std::Uint128,
|
||||
},
|
||||
|
||||
#[error("address '{address}' does not have a custom reduced deposit price set")]
|
||||
NoReducedDepositPrice { address: String },
|
||||
|
||||
#[error(
|
||||
"deposit amount ({amount}) must be at least the ticket book size ({ticket_book_size})"
|
||||
)]
|
||||
DepositBelowTicketBookSize {
|
||||
amount: cosmwasm_std::Uint128,
|
||||
ticket_book_size: u64,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
pub mod blacklist;
|
||||
pub mod counters;
|
||||
pub mod deposit;
|
||||
pub mod deposit_statistics;
|
||||
pub mod error;
|
||||
pub mod event_attributes;
|
||||
pub mod events;
|
||||
pub mod msg;
|
||||
pub mod redeem_credential;
|
||||
pub mod reduced_deposit;
|
||||
|
||||
pub use error::EcashContractError;
|
||||
|
||||
@@ -9,6 +9,10 @@ use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountRespon
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::deposit::{DepositResponse, LatestDepositResponse, PagedDepositsResponse};
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::deposit_statistics::DepositsStatistics;
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::reduced_deposit::WhitelistedAccountsResponse;
|
||||
#[cfg(feature = "schema")]
|
||||
use cosmwasm_schema::QueryResponses;
|
||||
|
||||
#[cw_serde]
|
||||
@@ -42,10 +46,25 @@ pub enum ExecuteMsg {
|
||||
admin: String,
|
||||
},
|
||||
|
||||
UpdateDepositValue {
|
||||
#[serde(alias = "update_deposit_value")]
|
||||
UpdateDefaultDepositValue {
|
||||
new_deposit: Coin,
|
||||
},
|
||||
|
||||
/// Set (or overwrite) a reduced deposit price for a specific address.
|
||||
/// Only callable by the contract admin.
|
||||
SetReducedDepositPrice {
|
||||
address: String,
|
||||
deposit: Coin,
|
||||
},
|
||||
|
||||
/// Remove the reduced deposit price for a specific address, reverting them to
|
||||
/// the default price. Returns an error if the address has no custom price set.
|
||||
/// Only callable by the contract admin.
|
||||
RemoveReducedDepositPrice {
|
||||
address: String,
|
||||
},
|
||||
|
||||
// TODO: properly implement
|
||||
ProposeToBlacklist {
|
||||
public_key: String,
|
||||
@@ -68,7 +87,15 @@ pub enum QueryMsg {
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(Coin))]
|
||||
GetRequiredDepositAmount {},
|
||||
#[serde(alias = "get_required_deposit_amount")]
|
||||
#[serde(alias = "GetRequiredDepositAmount")]
|
||||
GetDefaultDepositAmount {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(Option<Coin>))]
|
||||
GetReducedDepositAmount { address: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(WhitelistedAccountsResponse))]
|
||||
GetAllWhitelistedAccounts {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(DepositResponse))]
|
||||
GetDeposit { deposit_id: u32 },
|
||||
@@ -81,7 +108,22 @@ pub enum QueryMsg {
|
||||
limit: Option<u32>,
|
||||
start_after: Option<u32>,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(DepositsStatistics))]
|
||||
GetDepositsStatistics {},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {}
|
||||
pub struct MigrateMsg {
|
||||
/// Initial set of whitelisted accounts with their reduced deposit prices.
|
||||
/// Each entry is validated and stored during migration.
|
||||
pub initial_whitelist: Vec<WhitelistedDeposit>,
|
||||
}
|
||||
|
||||
/// An address and its reduced deposit price, used when seeding the whitelist
|
||||
/// via migration.
|
||||
#[cw_serde]
|
||||
pub struct WhitelistedDeposit {
|
||||
pub address: String,
|
||||
pub deposit: Coin,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
|
||||
#[cw_serde]
|
||||
pub struct WhitelistedAccount {
|
||||
pub address: Addr,
|
||||
pub deposit: Coin,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct WhitelistedAccountsResponse {
|
||||
pub whitelisted_accounts: Vec<WhitelistedAccount>,
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::helpers::LockTimer;
|
||||
use nym_ecash_contract_common::msg::ExecuteMsg;
|
||||
use nym_validator_client::nyxd::contract_traits::NymContractsProvider;
|
||||
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use nym_validator_client::nyxd::{Coin, Config, CosmWasmClient, NyxdClient};
|
||||
use nym_validator_client::nyxd::{AccountId, Coin, Config, CosmWasmClient, NyxdClient};
|
||||
use nym_validator_client::{DirectSigningHttpRpcNyxdClient, nyxd};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
@@ -50,6 +50,10 @@ impl ChainClient {
|
||||
Ok(ChainClient(Arc::new(RwLock::new(client))))
|
||||
}
|
||||
|
||||
pub async fn address(&self) -> AccountId {
|
||||
self.0.read().await.address()
|
||||
}
|
||||
|
||||
pub async fn query_chain(&self) -> ChainReadPermit<'_> {
|
||||
let _acquire_timer = LockTimer::new("acquire chain query permit");
|
||||
self.0.read().await
|
||||
|
||||
@@ -8,6 +8,7 @@ use nym_validator_client::nyxd::contract_traits::EcashQueryClient;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub struct CachedDeposit {
|
||||
valid_until: OffsetDateTime,
|
||||
@@ -56,13 +57,29 @@ impl RequiredDepositCache {
|
||||
|
||||
// update cache
|
||||
drop(read_guard);
|
||||
|
||||
let address = chain_client.address().await;
|
||||
info!("checking deposit required by {address}");
|
||||
let mut write_guard = self.inner.write().await;
|
||||
let deposit_amount = chain_client
|
||||
.query_chain()
|
||||
.await
|
||||
.get_required_deposit_amount()
|
||||
|
||||
let read_permit = chain_client.query_chain().await;
|
||||
let reduced = read_permit
|
||||
.get_reduced_deposit_amount(address.to_string())
|
||||
.await?;
|
||||
|
||||
let deposit_amount = match reduced {
|
||||
Some(reduced) => {
|
||||
info!("we're permitted to use reduced price");
|
||||
reduced
|
||||
}
|
||||
None => {
|
||||
warn!(
|
||||
"using default deposit value {address} is not whitelisted for price reduction"
|
||||
);
|
||||
read_permit.get_default_deposit_amount().await?
|
||||
}
|
||||
};
|
||||
|
||||
let nym_coin: Coin = deposit_amount.into();
|
||||
|
||||
write_guard.update(nym_coin.clone());
|
||||
|
||||
@@ -3,25 +3,19 @@
|
||||
|
||||
use crate::Error;
|
||||
use crate::ecash::error::EcashTicketError;
|
||||
use crate::ecash::helpers::for_each_api_concurrent;
|
||||
use crate::ecash::state::SharedState;
|
||||
use cosmwasm_std::Fraction;
|
||||
use cw_utils::ThresholdResponse;
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
use futures::{Stream, StreamExt};
|
||||
use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY;
|
||||
use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketBody};
|
||||
use nym_api_requests::ecash::models::VerifyEcashTicketBody;
|
||||
use nym_credentials_interface::Bandwidth;
|
||||
use nym_credentials_interface::{ClientTicket, TicketType};
|
||||
use nym_validator_client::EcashApiClient;
|
||||
use nym_validator_client::coconut::EcashApiError;
|
||||
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
|
||||
use nym_validator_client::nym_api::NymApiClientExt;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use nym_validator_client::nyxd::contract_traits::{
|
||||
EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient,
|
||||
};
|
||||
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
|
||||
use nym_validator_client::nyxd::cw3::Status;
|
||||
use nym_validator_client::nyxd::contract_traits::MultisigQueryClient;
|
||||
use si_scale::helpers::bibytes2;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
@@ -31,22 +25,6 @@ use tokio::sync::{Mutex, RwLockReadGuard};
|
||||
use tokio::time::{Duration, Instant, interval_at};
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
|
||||
enum ProposalResult {
|
||||
Executed,
|
||||
Rejected,
|
||||
Pending,
|
||||
}
|
||||
|
||||
impl ProposalResult {
|
||||
fn is_pending(&self) -> bool {
|
||||
matches!(self, ProposalResult::Pending)
|
||||
}
|
||||
|
||||
fn is_rejected(&self) -> bool {
|
||||
matches!(self, ProposalResult::Rejected)
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingVerification {
|
||||
ticket: ClientTicket,
|
||||
|
||||
@@ -68,43 +46,6 @@ impl PendingVerification {
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingRedemptionVote {
|
||||
proposal_id: u64,
|
||||
digest: Vec<u8>,
|
||||
included_serial_numbers: Vec<Vec<u8>>,
|
||||
epoch_id: EpochId,
|
||||
|
||||
// vec of node ids of apis that haven't sent a valid response
|
||||
pending: Vec<u64>,
|
||||
}
|
||||
|
||||
impl PendingRedemptionVote {
|
||||
fn new(
|
||||
proposal_id: u64,
|
||||
digest: Vec<u8>,
|
||||
included_serial_numbers: Vec<Vec<u8>>,
|
||||
epoch_id: EpochId,
|
||||
pending: Vec<u64>,
|
||||
) -> Self {
|
||||
PendingRedemptionVote {
|
||||
proposal_id,
|
||||
digest,
|
||||
included_serial_numbers,
|
||||
epoch_id,
|
||||
pending,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> BatchRedeemTicketsBody {
|
||||
BatchRedeemTicketsBody::new(
|
||||
self.digest.clone(),
|
||||
self.proposal_id,
|
||||
self.included_serial_numbers.clone(),
|
||||
gateway_cosmos_addr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CredentialHandlerConfig {
|
||||
/// Specifies the multiplier for revoking a malformed/double-spent ticket
|
||||
/// (if it has to go all the way to the nym-api for verification)
|
||||
@@ -132,7 +73,6 @@ pub struct CredentialHandler {
|
||||
ticket_receiver: UnboundedReceiver<ClientTicket>,
|
||||
shared_state: SharedState,
|
||||
pending_tickets: Vec<PendingVerification>,
|
||||
pending_redemptions: Vec<PendingRedemptionVote>,
|
||||
}
|
||||
|
||||
impl CredentialHandler {
|
||||
@@ -184,75 +124,6 @@ impl CredentialHandler {
|
||||
Ok(pending)
|
||||
}
|
||||
|
||||
async fn rebuild_pending_votes(
|
||||
shared_state: &SharedState,
|
||||
) -> Result<Vec<PendingRedemptionVote>, EcashTicketError> {
|
||||
// 1. get all tickets that were not fully verified
|
||||
let unverified = shared_state.storage.get_all_unresolved_proposals().await?;
|
||||
let mut pending = Vec::with_capacity(unverified.len());
|
||||
|
||||
let epoch_id = shared_state.current_epoch_id().await?;
|
||||
let apis = shared_state
|
||||
.api_clients(epoch_id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|s| (s.cosmos_address.to_string(), s.node_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for proposal_id in unverified {
|
||||
// get all of the votes
|
||||
let votes = shared_state
|
||||
.start_query()
|
||||
.await
|
||||
.get_all_votes(proposal_id as u64)
|
||||
.await
|
||||
.map_err(EcashTicketError::chain_query_failure)?
|
||||
.into_iter()
|
||||
.map(|v| v.voter)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut missing_votes = Vec::new();
|
||||
|
||||
// see who hasn't voted
|
||||
for (api_address, api_id) in &apis {
|
||||
// for each signer, check if they have actually voted; if not, that's the missing guy
|
||||
if !votes.contains(api_address) {
|
||||
missing_votes.push(*api_id)
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to rebuild SN and digest from the proposal info + storage data
|
||||
let proposal_info = shared_state
|
||||
.start_query()
|
||||
.await
|
||||
.query_proposal(proposal_id as u64)
|
||||
.await
|
||||
.map_err(EcashTicketError::chain_query_failure)?;
|
||||
|
||||
let tickets = shared_state
|
||||
.storage
|
||||
.get_all_proposed_tickets_with_sn(proposal_id as u32)
|
||||
.await?;
|
||||
let digest =
|
||||
BatchRedeemTicketsBody::make_digest(tickets.iter().map(|t| &t.serial_number));
|
||||
let encoded_digest = bs58::encode(&digest).into_string();
|
||||
if encoded_digest != proposal_info.description {
|
||||
error!("the lost proposal {proposal_id} does not have a matching digest!");
|
||||
continue;
|
||||
}
|
||||
|
||||
pending.push(PendingRedemptionVote {
|
||||
proposal_id: proposal_id as u64,
|
||||
digest,
|
||||
included_serial_numbers: tickets.into_iter().map(|t| t.serial_number).collect(),
|
||||
epoch_id,
|
||||
pending: missing_votes,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(pending)
|
||||
}
|
||||
|
||||
pub(crate) async fn new(
|
||||
config: CredentialHandlerConfig,
|
||||
ticket_receiver: UnboundedReceiver<ClientTicket>,
|
||||
@@ -276,51 +147,15 @@ impl CredentialHandler {
|
||||
// on startup read pending credentials and api responses from the storage
|
||||
let pending_tickets = Self::rebuild_pending_tickets(&shared_state).await?;
|
||||
|
||||
// on startup read pending proposals from the storage
|
||||
// then reconstruct the votes by querying the multisig contract for votes on those proposals
|
||||
// digest from the description and count from the message
|
||||
let pending_redemptions = Self::rebuild_pending_votes(&shared_state).await?;
|
||||
|
||||
Ok(CredentialHandler {
|
||||
config,
|
||||
multisig_threshold,
|
||||
ticket_receiver,
|
||||
shared_state,
|
||||
pending_tickets,
|
||||
pending_redemptions,
|
||||
})
|
||||
}
|
||||
|
||||
// the argument is temporary as we'll be reading from the storage
|
||||
async fn create_redemption_proposal(
|
||||
&self,
|
||||
commitment: &[u8],
|
||||
number_of_tickets: u16,
|
||||
) -> Result<u64, EcashTicketError> {
|
||||
let res = self
|
||||
.shared_state
|
||||
.start_tx()
|
||||
.await
|
||||
.request_ticket_redemption(
|
||||
bs58::encode(commitment).into_string(),
|
||||
number_of_tickets,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| EcashTicketError::RedemptionProposalCreationFailure { source })?;
|
||||
|
||||
// that one is quite tricky because proposal exists on chain, but we didn't get the id...
|
||||
// but it should be quite impossible to ever reach this unless we make breaking changes
|
||||
let proposal_id = res
|
||||
.parse_singleton_u64_contract_data()
|
||||
.inspect_err(|err| error!("reached seemingly impossible error! could not recover the redemption proposal id: {err}"))
|
||||
.map_err(|source| EcashTicketError::ProposalIdParsingFailure { source })?;
|
||||
|
||||
info!("created redemption proposal {proposal_id} to redeem {number_of_tickets} tickets");
|
||||
|
||||
Ok(proposal_id)
|
||||
}
|
||||
|
||||
/// Attempt to send ticket verification request to the provided ecash verifier.
|
||||
async fn verify_ticket(
|
||||
&self,
|
||||
@@ -522,42 +357,7 @@ impl CredentialHandler {
|
||||
async fn resolve_pending(&mut self) -> Result<(), EcashTicketError> {
|
||||
let mut still_failing = Vec::new();
|
||||
|
||||
// 1. attempt to resolve all pending proposals
|
||||
while let Some(mut pending) = self.pending_redemptions.pop() {
|
||||
match self.try_resolve_pending_proposal(&mut pending, None).await {
|
||||
Ok(resolution) => {
|
||||
if resolution.is_pending() {
|
||||
warn!(
|
||||
"still failed to reach quorum for proposal {}. apis: {:?} haven't responded. we'll retry later",
|
||||
pending.proposal_id, pending.pending
|
||||
);
|
||||
still_failing.push(pending);
|
||||
} else {
|
||||
self.shared_state
|
||||
.storage
|
||||
.clear_post_proposal_data(
|
||||
pending.proposal_id as u32,
|
||||
OffsetDateTime::now_utc(),
|
||||
resolution.is_rejected(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"experienced internal error when attempting to resolve pending proposal: {err}"
|
||||
);
|
||||
// make sure to update internal state to not lose any data
|
||||
self.pending_redemptions.push(pending);
|
||||
self.pending_redemptions.append(&mut still_failing);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut still_failing = Vec::new();
|
||||
|
||||
// 2. attempt to verify the remaining tickets
|
||||
// 1. attempt to verify the remaining tickets
|
||||
while let Some(mut pending) = self.pending_tickets.pop() {
|
||||
// possible optimisation: if there's a lot of pending tickets, pre-emptively grab locks for api_clients
|
||||
match self
|
||||
@@ -595,362 +395,14 @@ impl CredentialHandler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to send batch redemption request to the provided ecash verifier.
|
||||
async fn redeem_tickets(
|
||||
&self,
|
||||
proposal_id: u64,
|
||||
request: &BatchRedeemTicketsBody,
|
||||
client: &EcashApiClient,
|
||||
) -> Result<bool, EcashTicketError> {
|
||||
match client.api_client.batch_redeem_ecash_tickets(request).await {
|
||||
Ok(res) => {
|
||||
let accepted = if res.proposal_accepted {
|
||||
trace!("{client} has accepted proposal {proposal_id}");
|
||||
true
|
||||
} else {
|
||||
warn!("{client} has rejected proposal {proposal_id}");
|
||||
false
|
||||
};
|
||||
|
||||
Ok(accepted)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"failed to send proposal {proposal_id} for redemption vote to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_execute_proposal(&self, proposal_id: u64) -> Result<(), EcashTicketError> {
|
||||
self.shared_state
|
||||
.start_tx()
|
||||
.await
|
||||
.execute_proposal(proposal_id, None)
|
||||
.await
|
||||
.map_err(
|
||||
|source| EcashTicketError::RedemptionProposalExecutionFailure {
|
||||
proposal_id,
|
||||
source,
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_proposal_status(&self, proposal_id: u64) -> Result<Status, EcashTicketError> {
|
||||
Ok(self
|
||||
.shared_state
|
||||
.start_query()
|
||||
.await
|
||||
.query_proposal(proposal_id)
|
||||
.await
|
||||
.map_err(EcashTicketError::chain_query_failure)?
|
||||
.status)
|
||||
}
|
||||
|
||||
async fn try_finalize_proposal(
|
||||
&self,
|
||||
proposal_id: u64,
|
||||
) -> Result<ProposalResult, EcashTicketError> {
|
||||
match self.get_proposal_status(proposal_id).await? {
|
||||
Status::Pending => {
|
||||
// the voting hasn't even begun!
|
||||
error!("impossible case! the proposal {proposal_id} is still pending");
|
||||
Ok(ProposalResult::Pending)
|
||||
}
|
||||
Status::Open => {
|
||||
debug!("proposal {proposal_id} is still open and needs more votes");
|
||||
Ok(ProposalResult::Pending)
|
||||
}
|
||||
Status::Rejected => {
|
||||
warn!("proposal {proposal_id} has been rejected");
|
||||
Ok(ProposalResult::Rejected)
|
||||
}
|
||||
Status::Passed => {
|
||||
info!(
|
||||
"proposal {proposal_id} has already been passed - we just need to execute it"
|
||||
);
|
||||
self.try_execute_proposal(proposal_id).await?;
|
||||
info!("executed proposal {proposal_id}");
|
||||
Ok(ProposalResult::Executed)
|
||||
}
|
||||
Status::Executed => {
|
||||
info!("proposal {proposal_id} has already been executed - nothing to do!");
|
||||
Ok(ProposalResult::Executed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_resolve_pending_proposal(
|
||||
&self,
|
||||
pending: &mut PendingRedemptionVote,
|
||||
api_clients: Option<RwLockReadGuard<'_, Vec<EcashApiClient>>>,
|
||||
) -> Result<ProposalResult, EcashTicketError> {
|
||||
let proposal_id = pending.proposal_id;
|
||||
|
||||
info!(
|
||||
"attempting to resolve pending redemption proposal {proposal_id} to redeem {} tickets",
|
||||
pending.included_serial_numbers.len()
|
||||
);
|
||||
|
||||
// check if the proposal still needs more votes from the apis
|
||||
let result = self.try_finalize_proposal(proposal_id).await?;
|
||||
if !result.is_pending() {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
let api_clients = match api_clients {
|
||||
Some(clients) => clients,
|
||||
None => self.shared_state.api_clients(pending.epoch_id).await?,
|
||||
};
|
||||
|
||||
let redemption_request = pending.to_request_body(self.shared_state.address.clone());
|
||||
|
||||
// TODO: optimisation: tell other apis they can purge our tickets even if they haven't voted
|
||||
|
||||
let total = api_clients.len();
|
||||
let api_failures = Mutex::new(Vec::new());
|
||||
let rejected = AtomicUsize::new(0);
|
||||
|
||||
for_each_api_concurrent(&api_clients, &pending.pending, |ecash_client| async {
|
||||
// errors are only returned on hard, storage, failures
|
||||
match self
|
||||
.redeem_tickets(pending.proposal_id, &redemption_request, ecash_client)
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
error!("internal failure. could not proceed with ticket redemption: {err}");
|
||||
api_failures.lock().await.push(ecash_client.node_id);
|
||||
}
|
||||
Ok(false) => {
|
||||
rejected.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let api_failures = api_failures.into_inner();
|
||||
let num_failures = api_failures.len();
|
||||
pending.pending = api_failures;
|
||||
|
||||
let rejected = rejected.into_inner();
|
||||
let rejected_ratio = rejected as f32 / total as f32;
|
||||
let rejected_perc = rejected_ratio * 100.;
|
||||
if rejected_ratio >= (1. - self.multisig_threshold) {
|
||||
error!(
|
||||
"{rejected_perc:.2}% of signers rejected proposal {proposal_id}. we won't be able to execute it"
|
||||
);
|
||||
// no need to query the chain as with so many rejections it's impossible it has passed.
|
||||
return Ok(ProposalResult::Rejected);
|
||||
}
|
||||
|
||||
let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32;
|
||||
let accepted_perc = accepted_ratio * 100.;
|
||||
match accepted_ratio {
|
||||
n if n < self.multisig_threshold => {
|
||||
error!(
|
||||
"less than 2/3 of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}. we're not yet be able to execute it to get funds out"
|
||||
);
|
||||
return Ok(ProposalResult::Pending);
|
||||
}
|
||||
n if n < self.config.minimum_api_quorum => {
|
||||
warn!(
|
||||
"the system seems to be a bit unstable: less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}"
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
trace!("{accepted_perc:.2}% of signers accepted proposal {proposal_id}");
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to execute the proposal if it reached the required threshold
|
||||
self.try_finalize_proposal(proposal_id).await
|
||||
}
|
||||
|
||||
async fn maybe_redeem_tickets(&mut self) -> Result<(), EcashTicketError> {
|
||||
if !self.pending_tickets.is_empty() {
|
||||
return Err(EcashTicketError::PendingTickets);
|
||||
}
|
||||
|
||||
let latest_stored = self.shared_state.storage.latest_proposal().await?;
|
||||
|
||||
// check if we have already created the proposal but crashed before persisting it in the db
|
||||
//
|
||||
// if we have some persisted proposals in storage, try to see if there's anything more recent on chain
|
||||
// (i.e. the missing proposal)
|
||||
// if not (i.e. this would have been our first) check the latest page of proposals.
|
||||
// while this is not ideal, realistically speaking we probably crashed few minutes ago
|
||||
// and worst case scenario we'll just recreate the proposal instead
|
||||
//
|
||||
// LIMITATION: if MULTIPLE proposals got created in between, well. though luck.
|
||||
let latest_on_chain = if let Some(latest_stored) = &latest_stored {
|
||||
// those are sorted in ASCENDING way
|
||||
self.shared_state
|
||||
.proposals_since(latest_stored.proposal_id as u64)
|
||||
.await?
|
||||
.pop()
|
||||
} else {
|
||||
// but those are DESCENDING
|
||||
self.shared_state
|
||||
.last_proposal_page()
|
||||
.await?
|
||||
.first()
|
||||
.cloned()
|
||||
};
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
let prior_proposal = match (&latest_stored, latest_on_chain) {
|
||||
(None, None) => {
|
||||
// we haven't created any proposals before
|
||||
trace!("this could be our first redemption proposal");
|
||||
None
|
||||
}
|
||||
(Some(stored), None) => {
|
||||
if stored.created_at + MIN_BATCH_REDEMPTION_DELAY > now {
|
||||
trace!("too soon to create new redemption proposal");
|
||||
return Ok(());
|
||||
}
|
||||
None
|
||||
}
|
||||
(_, Some(on_chain)) => {
|
||||
warn!(
|
||||
"we seem to have crashed after creating proposal, but before persisting it onto disk!"
|
||||
);
|
||||
|
||||
Some(on_chain)
|
||||
}
|
||||
};
|
||||
|
||||
// technically we could have been just caching all of those serial numbers as we verify tickets,
|
||||
// but given how infrequently we call this, there's no point in wasting this memory
|
||||
let verified_tickets = self
|
||||
.shared_state
|
||||
.storage
|
||||
.get_all_verified_tickets_with_sn()
|
||||
.await?;
|
||||
|
||||
// TODO: somehow simplify that nasty nested if
|
||||
if verified_tickets.len() < self.config.minimum_redemption_tickets {
|
||||
// bypass the number of tickets check if we're about to lose our rewards due to expiration
|
||||
if let Some(latest_stored) = latest_stored {
|
||||
if latest_stored.created_at + self.config.maximum_time_between_redemption < now {
|
||||
{}
|
||||
} else {
|
||||
debug!(
|
||||
"we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))",
|
||||
verified_tickets.len(),
|
||||
self.config.minimum_redemption_tickets
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
// first proposal
|
||||
debug!(
|
||||
"we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))",
|
||||
verified_tickets.len(),
|
||||
self.config.minimum_redemption_tickets
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// this should have been ensured when querying
|
||||
assert!(verified_tickets.len() <= u16::MAX as usize);
|
||||
|
||||
let digest =
|
||||
BatchRedeemTicketsBody::make_digest(verified_tickets.iter().map(|t| &t.serial_number));
|
||||
let encoded_digest = bs58::encode(&digest).into_string();
|
||||
|
||||
let prior_proposal_id = if let Some(prior_proposal) = prior_proposal {
|
||||
if prior_proposal.description == encoded_digest {
|
||||
info!("we have already created proposal for those tickets");
|
||||
Some(prior_proposal.id)
|
||||
} else {
|
||||
warn!(
|
||||
"our missed proposal seem to have been for different tickets - abandoning it"
|
||||
);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// if the proposal has already existed on chain, do use it. otherwise create a new one
|
||||
let proposal_id = if let Some(prior) = prior_proposal_id {
|
||||
prior
|
||||
} else {
|
||||
self.create_redemption_proposal(&digest, verified_tickets.len() as u16)
|
||||
.await?
|
||||
};
|
||||
|
||||
if proposal_id > u32::MAX as u64 {
|
||||
// realistically will we ever reach it? no.
|
||||
panic!(
|
||||
"we have created more than {} proposals. we can't handle that.",
|
||||
u32::MAX
|
||||
)
|
||||
}
|
||||
|
||||
self.shared_state
|
||||
.storage
|
||||
.insert_redemption_proposal(
|
||||
&verified_tickets,
|
||||
proposal_id as u32,
|
||||
OffsetDateTime::now_utc(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let current_epoch = self.shared_state.current_epoch_id().await?;
|
||||
let api_clients = self.shared_state.api_clients(current_epoch).await?;
|
||||
let ids = api_clients.iter().map(|c| c.node_id).collect();
|
||||
let mut pending = PendingRedemptionVote::new(
|
||||
proposal_id,
|
||||
digest,
|
||||
verified_tickets
|
||||
.into_iter()
|
||||
.map(|t| t.serial_number)
|
||||
.collect(),
|
||||
current_epoch,
|
||||
ids,
|
||||
);
|
||||
|
||||
let resolution = self
|
||||
.try_resolve_pending_proposal(&mut pending, Some(api_clients))
|
||||
.await?;
|
||||
if resolution.is_pending() {
|
||||
warn!(
|
||||
"failed to reach quorum for proposal {proposal_id}. apis: {:?} haven't responded. we'll retry later",
|
||||
pending.pending
|
||||
);
|
||||
self.pending_redemptions.push(pending);
|
||||
} else {
|
||||
self.shared_state
|
||||
.storage
|
||||
.clear_post_proposal_data(
|
||||
proposal_id as u32,
|
||||
OffsetDateTime::now_utc(),
|
||||
resolution.is_rejected(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn periodic_operations(&mut self) -> Result<(), EcashTicketError> {
|
||||
trace!(
|
||||
"attempting to resolve all pending operations -> tickets that are waiting for verification and possibly redemption"
|
||||
"attempting to resolve all pending operations -> tickets that are waiting for verification"
|
||||
);
|
||||
|
||||
// 1. retry all operations that have failed in the past: verification requests and pending redemption
|
||||
// retry the pending verification requests that have failed before
|
||||
self.resolve_pending().await?;
|
||||
|
||||
// 2. if applicable, attempt to redeem all newly verified tickets
|
||||
self.maybe_redeem_tickets().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use tokio::sync::RwLockReadGuard;
|
||||
|
||||
pub(crate) fn apis_stream<'a>(
|
||||
pub fn apis_stream<'a>(
|
||||
// if needed we could make this argument more generic to accept either locks or iterators, etc.
|
||||
all_clients: &'a RwLockReadGuard<'a, Vec<EcashApiClient>>,
|
||||
filter_by_id: &'a [u64],
|
||||
@@ -22,7 +22,7 @@ pub(crate) fn apis_stream<'a>(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn for_each_api_concurrent<'a, F, Fut>(
|
||||
pub async fn for_each_api_concurrent<'a, F, Fut>(
|
||||
all_clients: &'a RwLockReadGuard<'a, Vec<EcashApiClient>>,
|
||||
filter_by_id: &'a [u64],
|
||||
f: F,
|
||||
|
||||
@@ -20,7 +20,7 @@ use tracing::error;
|
||||
|
||||
pub mod credential_sender;
|
||||
pub mod error;
|
||||
mod helpers;
|
||||
pub mod helpers;
|
||||
mod state;
|
||||
pub mod traits;
|
||||
|
||||
|
||||
@@ -3,17 +3,12 @@
|
||||
|
||||
use crate::Error;
|
||||
use crate::ecash::error::EcashTicketError;
|
||||
use cosmwasm_std::{CosmosMsg, WasmMsg, from_json};
|
||||
use nym_credentials_interface::VerificationKeyAuth;
|
||||
use nym_ecash_contract_common::msg::ExecuteMsg;
|
||||
use nym_gateway_storage::traits::BandwidthGatewayStorage;
|
||||
use nym_validator_client::coconut::all_ecash_api_clients;
|
||||
use nym_validator_client::nym_api::EpochId;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use nym_validator_client::nyxd::contract_traits::{
|
||||
DkgQueryClient, MultisigQueryClient, NymContractsProvider,
|
||||
};
|
||||
use nym_validator_client::nyxd::cw3::ProposalResponse;
|
||||
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider};
|
||||
use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient};
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
@@ -77,53 +72,6 @@ impl SharedState {
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
fn created_redemption_proposal(&self, proposal: &ProposalResponse) -> bool {
|
||||
let Some(msg) = proposal.msgs.first() else {
|
||||
return false;
|
||||
};
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = msg else {
|
||||
return false;
|
||||
};
|
||||
let Ok(ExecuteMsg::RedeemTickets { gw, .. }) = from_json(msg) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
gw == self.address.as_ref()
|
||||
}
|
||||
|
||||
/// retrieve all redemption proposals made by this gateway since, but excluding, the provided id
|
||||
pub(crate) async fn proposals_since(
|
||||
&self,
|
||||
proposal_id: u64,
|
||||
) -> Result<Vec<ProposalResponse>, EcashTicketError> {
|
||||
Ok(self
|
||||
.start_query()
|
||||
.await
|
||||
.list_proposals(Some(proposal_id), None)
|
||||
.await
|
||||
.map_err(EcashTicketError::chain_query_failure)?
|
||||
.proposals
|
||||
.into_iter()
|
||||
.filter(|p| self.created_redemption_proposal(p))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// retrieve all redemption proposals made by this gateway that are available on the last page of the query
|
||||
pub(crate) async fn last_proposal_page(
|
||||
&self,
|
||||
) -> Result<Vec<ProposalResponse>, EcashTicketError> {
|
||||
Ok(self
|
||||
.start_query()
|
||||
.await
|
||||
.reverse_proposals(None, None)
|
||||
.await
|
||||
.map_err(EcashTicketError::chain_query_failure)?
|
||||
.proposals
|
||||
.into_iter()
|
||||
.filter(|p| self.created_redemption_proposal(p))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn set_epoch_data(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
@@ -240,24 +188,6 @@ impl SharedState {
|
||||
data.get(&epoch_id).map(|d| &d.master_key).unwrap()
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<'_, DirectSigningHttpRpcNyxdClient> {
|
||||
self.nyxd_client.write().await
|
||||
}
|
||||
|
||||
pub(crate) async fn start_query(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
|
||||
self.nyxd_client.read().await
|
||||
}
|
||||
|
||||
pub(crate) async fn current_epoch_id(&self) -> Result<EpochId, EcashTicketError> {
|
||||
Ok(self
|
||||
.start_query()
|
||||
.await
|
||||
.get_current_epoch()
|
||||
.await
|
||||
.map_err(EcashTicketError::chain_query_failure)?
|
||||
.epoch_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct EpochState {
|
||||
|
||||
@@ -34,6 +34,7 @@ tracing = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "time"] }
|
||||
rustls = { workspace=true }
|
||||
# used for decoding text responses (they were already implicitly included)
|
||||
bytes = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub const NYM_API_DOMAIN: &str = "validator.nymtech.net";
|
||||
pub const NYM_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(212, 71, 233, 232))];
|
||||
pub const NYM_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(92, 39, 63, 14))];
|
||||
|
||||
pub const NYM_VPN_API_DOMAIN: &str = "nymvpn.com";
|
||||
pub const NYM_VPN_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 21))];
|
||||
|
||||
@@ -161,6 +161,8 @@ use reqwest::{RequestBuilder, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::io::ErrorKind;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
@@ -1167,14 +1169,10 @@ impl ApiClientCore for Client {
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(err) => {
|
||||
// only if there was a network issue should we consider updating the host info
|
||||
//
|
||||
// note: for now this includes DNS resolution failure, I am not sure how I would go about
|
||||
// segregating that based on the interface provided by request for errors.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let is_network_err = err.is_timeout();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let is_network_err = err.is_timeout() || err.is_connect();
|
||||
let is_network_err = might_be_network_interference(&err);
|
||||
|
||||
if is_network_err {
|
||||
// if we have multiple urls, update to the next
|
||||
@@ -1222,6 +1220,68 @@ impl ApiClientCore for Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
|
||||
|
||||
/// This functions attempts to check the error returned by reqwest to see if
|
||||
/// rotating host informtion (for clients with mutliple hosts defined) could be
|
||||
/// helpful. This looks for situations where the error could plausibly be caused
|
||||
/// by a network adversary, or where rotating to an equival hostname might help.
|
||||
///
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains,
|
||||
/// but ConnectionReset might be caused by a network adversary blocking by SNI
|
||||
/// which could possibly benefit from rotating domains.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
||||
if err.is_timeout() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !(err.is_connect() || err.is_request()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The io::Error source is several layers deep, for clarity this is done as a loop
|
||||
// * reqwest::Error -> hyper_util::Error
|
||||
// * hyper_util::Error -> hyper_util::ClientError
|
||||
// * hyper_util::ClientError -> io::Error
|
||||
let mut inner = err.source();
|
||||
for _ in 0..MAX_ERR_SOURCE_ITERATIONS {
|
||||
if let Some(e) = inner {
|
||||
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
|
||||
// try downcast to io::Error from <dyn std::error:Error>
|
||||
match io_err.kind() {
|
||||
// device not connected to the internet
|
||||
ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => return false,
|
||||
// connection errors can indicate connection interference
|
||||
ErrorKind::ConnectionReset
|
||||
| ErrorKind::HostUnreachable
|
||||
| ErrorKind::ConnectionRefused => return true,
|
||||
// TLS errors get wrapped in custom io::Errors
|
||||
ErrorKind::Other | ErrorKind::InvalidData => {
|
||||
// io::Error get_ref works while source doesn't here -_-
|
||||
// if you don't like it take it up with the rust devs https://users.rust-lang.org/t/question-about-implementation-of-std-source/121117
|
||||
inner = io_err.get_ref().map(|e| e as &dyn std::error::Error);
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
} else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
|
||||
// try downcast to TLS error
|
||||
return true;
|
||||
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::ResolveError>() {
|
||||
// try downcast to DNS error
|
||||
return resolve_err.is_nx_domain();
|
||||
} else {
|
||||
inner = e.source();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Common usage functionality for the http client.
|
||||
///
|
||||
/// These functions allow for cleaner downstream usage free of type parameters and unneeded imports.
|
||||
|
||||
@@ -10,7 +10,11 @@ license.workspace = true
|
||||
description = "Codec, signing functionality, and different version definitions for IP packet request and responses"
|
||||
|
||||
|
||||
[features]
|
||||
test-utils = ["pnet_packet"]
|
||||
|
||||
[dependencies]
|
||||
pnet_packet = { workspace = true, optional = true }
|
||||
bincode = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
nym-bin-common = { workspace = true }
|
||||
@@ -18,8 +22,10 @@ nym-crypto = { workspace = true }
|
||||
nym-service-provider-requests-common = { workspace = true }
|
||||
nym-sphinx = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Extracted from sdk/rust/nym-sdk/examples/ipr_tunnel.rs
|
||||
|
||||
//! ICMP/ICMPv6 packet construction and reply detection helpers for testing
|
||||
//! IPR connectivity. Gated behind the `test-utils` feature.
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use pnet_packet::Packet;
|
||||
use pnet_packet::icmp::echo_reply::EchoReplyPacket;
|
||||
use pnet_packet::icmp::echo_request::MutableEchoRequestPacket;
|
||||
use pnet_packet::icmp::{IcmpPacket, IcmpTypes};
|
||||
use pnet_packet::icmpv6::Icmpv6Types;
|
||||
use pnet_packet::ipv4::{Ipv4Flags, MutableIpv4Packet};
|
||||
use pnet_packet::ipv6::MutableIpv6Packet;
|
||||
|
||||
/// Build a complete IPv4 ICMP echo request packet.
|
||||
pub fn build_icmp_ping(src: Ipv4Addr, dst: Ipv4Addr, seq: u16) -> Option<Vec<u8>> {
|
||||
let mut echo = MutableEchoRequestPacket::owned(vec![0u8; 64])?;
|
||||
echo.set_icmp_type(IcmpTypes::EchoRequest);
|
||||
echo.set_icmp_code(pnet_packet::icmp::IcmpCode::new(0));
|
||||
echo.set_sequence_number(seq);
|
||||
let cksum = pnet_packet::icmp::checksum(&IcmpPacket::new(echo.packet())?);
|
||||
echo.set_checksum(cksum);
|
||||
|
||||
let total_len = 20 + echo.packet().len();
|
||||
let mut ip = MutableIpv4Packet::owned(vec![0u8; total_len])?;
|
||||
ip.set_version(4);
|
||||
ip.set_header_length(5);
|
||||
ip.set_total_length(total_len as u16);
|
||||
ip.set_ttl(64);
|
||||
ip.set_next_level_protocol(pnet_packet::ip::IpNextHeaderProtocols::Icmp);
|
||||
ip.set_source(src);
|
||||
ip.set_destination(dst);
|
||||
ip.set_flags(Ipv4Flags::DontFragment);
|
||||
ip.set_payload(echo.packet());
|
||||
|
||||
let mut buf = ip.consume_to_immutable().packet().to_vec();
|
||||
let cksum = ipv4_checksum(&buf);
|
||||
buf[10] = (cksum >> 8) as u8;
|
||||
buf[11] = cksum as u8;
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
/// Build a complete IPv6 ICMPv6 echo request packet.
|
||||
pub fn build_icmpv6_ping(src: Ipv6Addr, dst: Ipv6Addr, seq: u16) -> Option<Vec<u8>> {
|
||||
let mut echo =
|
||||
pnet_packet::icmpv6::echo_request::MutableEchoRequestPacket::owned(vec![0u8; 64])?;
|
||||
echo.set_icmpv6_type(Icmpv6Types::EchoRequest);
|
||||
echo.set_icmpv6_code(pnet_packet::icmpv6::Icmpv6Code::new(0));
|
||||
echo.set_sequence_number(seq);
|
||||
let cksum = pnet_packet::icmpv6::checksum(
|
||||
&pnet_packet::icmpv6::Icmpv6Packet::new(echo.packet())?,
|
||||
&src,
|
||||
&dst,
|
||||
);
|
||||
echo.set_checksum(cksum);
|
||||
|
||||
let payload_len = echo.packet().len();
|
||||
let mut ip = MutableIpv6Packet::owned(vec![0u8; 40 + payload_len])?;
|
||||
ip.set_version(6);
|
||||
ip.set_payload_length(payload_len as u16);
|
||||
ip.set_next_header(pnet_packet::ip::IpNextHeaderProtocols::Icmpv6);
|
||||
ip.set_hop_limit(64);
|
||||
ip.set_source(src);
|
||||
ip.set_destination(dst);
|
||||
ip.set_payload(echo.packet());
|
||||
|
||||
Some(ip.consume_to_immutable().packet().to_vec())
|
||||
}
|
||||
|
||||
/// Check if a raw packet is an IPv4 ICMP echo reply destined to `expected_dst`.
|
||||
pub fn is_echo_reply_v4(data: &[u8], expected_dst: Ipv4Addr) -> bool {
|
||||
let Some(ip) = pnet_packet::ipv4::Ipv4Packet::new(data) else {
|
||||
return false;
|
||||
};
|
||||
if ip.get_destination() != expected_dst {
|
||||
return false;
|
||||
}
|
||||
if ip.get_next_level_protocol() != pnet_packet::ip::IpNextHeaderProtocols::Icmp {
|
||||
return false;
|
||||
}
|
||||
let Some(reply) = EchoReplyPacket::new(ip.payload()) else {
|
||||
return false;
|
||||
};
|
||||
reply.get_icmp_type() == IcmpTypes::EchoReply
|
||||
}
|
||||
|
||||
/// Check if a raw packet is an IPv6 ICMPv6 echo reply destined to `expected_dst`.
|
||||
pub fn is_echo_reply_v6(data: &[u8], expected_dst: Ipv6Addr) -> bool {
|
||||
let Some(ip) = pnet_packet::ipv6::Ipv6Packet::new(data) else {
|
||||
return false;
|
||||
};
|
||||
if ip.get_destination() != expected_dst {
|
||||
return false;
|
||||
}
|
||||
if ip.get_next_header() != pnet_packet::ip::IpNextHeaderProtocols::Icmpv6 {
|
||||
return false;
|
||||
}
|
||||
let Some(reply) = pnet_packet::icmpv6::echo_reply::EchoReplyPacket::new(ip.payload()) else {
|
||||
return false;
|
||||
};
|
||||
reply.get_icmpv6_type() == Icmpv6Types::EchoReply
|
||||
}
|
||||
|
||||
fn ipv4_checksum(header: &[u8]) -> u16 {
|
||||
let mut sum = 0u32;
|
||||
for i in (0..20).step_by(2) {
|
||||
sum += ((header[i] as u32) << 8) | header[i + 1] as u32;
|
||||
}
|
||||
while (sum >> 16) > 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!sum as u16
|
||||
}
|
||||
@@ -3,10 +3,14 @@ use std::fmt::{Display, Formatter};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub mod codec;
|
||||
#[cfg(feature = "test-utils")]
|
||||
pub mod icmp_utils;
|
||||
pub mod response_helpers;
|
||||
pub mod sign;
|
||||
pub mod v6;
|
||||
pub mod v7;
|
||||
pub mod v8;
|
||||
pub mod v9;
|
||||
|
||||
// version 3: initial version
|
||||
// version 4: IPv6 support
|
||||
@@ -14,6 +18,8 @@ pub mod v8;
|
||||
// version 6: Increase the available IPs
|
||||
// version 7: Add signature support (for the future)
|
||||
// version 8: Anonymous sends
|
||||
// version 9: LP-framed transport (SphinxStream)
|
||||
// response_helpers: shared IPR response parsing (nym-ip-packet-client + nym-sdk)
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct IpPair {
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use tokio_util::codec::Decoder;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::{
|
||||
IpPair,
|
||||
codec::MultiIpPacketCodec,
|
||||
v8::response::{
|
||||
ConnectResponseReply, ControlResponse, InfoLevel, IpPacketResponse, IpPacketResponseData,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IprResponseError {
|
||||
#[error("no version byte in message")]
|
||||
NoVersionByte,
|
||||
|
||||
#[error("version mismatch: received v{received}, expected v{expected}")]
|
||||
VersionMismatch { expected: u8, received: u8 },
|
||||
|
||||
#[error("expected control response, got {0:?}")]
|
||||
UnexpectedResponse(IpPacketResponseData),
|
||||
|
||||
#[error("connect denied: {0:?}")]
|
||||
ConnectDenied(crate::v8::response::ConnectFailureReason),
|
||||
}
|
||||
|
||||
pub enum MixnetMessageOutcome {
|
||||
IpPackets(Vec<Bytes>),
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
// Extracted from:
|
||||
// nym-ip-packet-client/src/helpers.rs — check_ipr_message_version()
|
||||
// sdk/rust/nym-sdk/src/ip_packet_client/listener.rs — check_ipr_message_version()
|
||||
/// Check that the first byte of an IPR message matches the expected protocol version.
|
||||
///
|
||||
/// v9 currently reuses the v8 bincode layout (`nym_ip_packet_requests::v9` re-exports v8 types);
|
||||
/// the version byte signals LP/SphinxStream framing, not a wire-format change. Until exit gateways
|
||||
/// have rolled past `crate::v9::MIN_RELEASE_VERSION`, a v9 client may still receive v8 replies and
|
||||
/// must accept them. Revisit this compat branch if a future bump diverges the wire layout.
|
||||
///
|
||||
/// TODO(IPR-v9-rollout): remove the v9-accepts-v8 branch once the exit gateway fleet is on
|
||||
/// `crate::v9::MIN_RELEASE_VERSION` or newer.
|
||||
pub fn check_ipr_message_version(data: &[u8], expected: u8) -> Result<(), IprResponseError> {
|
||||
let version = *data.first().ok_or(IprResponseError::NoVersionByte)?;
|
||||
if version == expected {
|
||||
return Ok(());
|
||||
}
|
||||
if expected == crate::v9::VERSION && version == crate::v8::VERSION {
|
||||
debug!(
|
||||
"accepting v{} IPR reply under v{} client compat",
|
||||
crate::v8::VERSION,
|
||||
crate::v9::VERSION
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(IprResponseError::VersionMismatch {
|
||||
expected,
|
||||
received: version,
|
||||
})
|
||||
}
|
||||
|
||||
// Extracted from:
|
||||
// nym-ip-packet-client/src/connect.rs — handle_connect_response() + handle_ip_packet_router_response()
|
||||
// sdk/rust/nym-sdk/src/ip_packet_client/discovery.rs — parse_connect_response()
|
||||
/// Parse an IPR connect response, returning allocated IPs on success.
|
||||
pub fn parse_connect_response(response: IpPacketResponse) -> Result<IpPair, IprResponseError> {
|
||||
let control_response = match response.data {
|
||||
IpPacketResponseData::Control(c) => c,
|
||||
other => return Err(IprResponseError::UnexpectedResponse(other)),
|
||||
};
|
||||
|
||||
match *control_response {
|
||||
ControlResponse::Connect(connect_resp) => match connect_resp.reply {
|
||||
ConnectResponseReply::Success(success) => Ok(success.ips),
|
||||
ConnectResponseReply::Failure(reason) => Err(IprResponseError::ConnectDenied(reason)),
|
||||
},
|
||||
_ => Err(IprResponseError::UnexpectedResponse(
|
||||
IpPacketResponseData::Control(control_response),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// Extracted from:
|
||||
// nym-ip-packet-client/src/listener.rs — IprListener::handle_reconstructed_message()
|
||||
// sdk/rust/nym-sdk/src/ip_packet_client/listener.rs — handle_ipr_response()
|
||||
/// Parse raw IPR response bytes into an outcome.
|
||||
///
|
||||
/// Logs non-fatal conditions (unknown control messages, deserialization
|
||||
/// failures) and returns `None` for them.
|
||||
pub fn handle_ipr_response(data: &[u8]) -> Option<MixnetMessageOutcome> {
|
||||
match IpPacketResponse::from_bytes(data) {
|
||||
Ok(response) => match response.data {
|
||||
IpPacketResponseData::Data(data_response) => {
|
||||
let mut codec = MultiIpPacketCodec::new();
|
||||
let mut buf = BytesMut::from(data_response.ip_packet.as_ref());
|
||||
let mut packets = Vec::new();
|
||||
loop {
|
||||
match codec.decode(&mut buf) {
|
||||
Ok(Some(packet)) => packets.push(packet.into_bytes()),
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
warn!("Failed to decode bundled IP packet: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(MixnetMessageOutcome::IpPackets(packets))
|
||||
}
|
||||
IpPacketResponseData::Control(control_response) => match *control_response {
|
||||
ControlResponse::Connect(_) => {
|
||||
info!("Received connect response when already connected - ignoring");
|
||||
None
|
||||
}
|
||||
ControlResponse::Disconnect(_) | ControlResponse::UnrequestedDisconnect(_) => {
|
||||
info!("Received disconnect from IPR");
|
||||
Some(MixnetMessageOutcome::Disconnect)
|
||||
}
|
||||
ControlResponse::Pong(_) => {
|
||||
info!("Received pong response");
|
||||
None
|
||||
}
|
||||
ControlResponse::Health(_) => {
|
||||
info!("Received health response");
|
||||
None
|
||||
}
|
||||
ControlResponse::Info(info_resp) => {
|
||||
let msg = format!(
|
||||
"Received info response from the mixnet: {}",
|
||||
info_resp.reply
|
||||
);
|
||||
match info_resp.level {
|
||||
InfoLevel::Info => info!("{msg}"),
|
||||
InfoLevel::Warn => warn!("{msg}"),
|
||||
InfoLevel::Error => error!("{msg}"),
|
||||
}
|
||||
None
|
||||
}
|
||||
},
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Failed to deserialize IPR response: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn check_version_matches() {
|
||||
assert!(check_ipr_message_version(&[crate::v9::VERSION], crate::v9::VERSION).is_ok());
|
||||
assert!(check_ipr_message_version(&[crate::v8::VERSION], crate::v8::VERSION).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v9_client_accepts_v8_reply_compat() {
|
||||
assert!(check_ipr_message_version(&[crate::v8::VERSION], crate::v9::VERSION).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v8_client_rejects_v9_reply() {
|
||||
let err = check_ipr_message_version(&[crate::v9::VERSION], crate::v8::VERSION)
|
||||
.expect_err("v8 client must not silently accept v9");
|
||||
assert!(matches!(
|
||||
err,
|
||||
IprResponseError::VersionMismatch {
|
||||
expected: 8,
|
||||
received: 9
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unrelated_version_mismatch() {
|
||||
let err = check_ipr_message_version(&[7], crate::v9::VERSION)
|
||||
.expect_err("v9 client must reject v7");
|
||||
assert!(matches!(
|
||||
err,
|
||||
IprResponseError::VersionMismatch {
|
||||
expected: 9,
|
||||
received: 7
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_payload_returns_no_version_byte() {
|
||||
let err = check_ipr_message_version(&[], crate::v9::VERSION)
|
||||
.expect_err("empty payload must error");
|
||||
assert!(matches!(err, IprResponseError::NoVersionByte));
|
||||
}
|
||||
}
|
||||
@@ -179,11 +179,15 @@ impl IpPacketResponse {
|
||||
make_bincode_serializer().serialize(self)
|
||||
}
|
||||
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().deserialize(data)
|
||||
}
|
||||
|
||||
pub fn from_reconstructed_message(
|
||||
message: &nym_sphinx::receiver::ReconstructedMessage,
|
||||
) -> Result<Self, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().deserialize(&message.message)
|
||||
Self::from_bytes(&message.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
pub const VERSION: u8 = 9;
|
||||
|
||||
/// Minimum nym-node release version that supports v9 (LP Stream framing).
|
||||
/// Nodes running older versions will not understand LP-wrapped packets.
|
||||
pub const MIN_RELEASE_VERSION: semver::Version = semver::Version::new(1, 30, 0);
|
||||
|
||||
// v9 uses the same wire format as v8. The version bump indicates
|
||||
// the message was sent with LP framing (SphinxStream).
|
||||
//
|
||||
// Types are re-exported for deserialization/matching. Use the wrapper
|
||||
// constructors below to create correctly-versioned packets — never
|
||||
// manually set `protocol.version` or `response.version`.
|
||||
pub use super::v8::{request, response};
|
||||
|
||||
/// Create a v9 connect request (version byte set to 9).
|
||||
pub fn new_connect_request(buffer_timeout: Option<u64>) -> (request::IpPacketRequest, u64) {
|
||||
let (mut req, id) = request::IpPacketRequest::new_connect_request(buffer_timeout);
|
||||
req.protocol.version = VERSION;
|
||||
(req, id)
|
||||
}
|
||||
|
||||
/// Create a v9 data request (version byte set to 9).
|
||||
pub fn new_data_request(data: bytes::Bytes) -> request::IpPacketRequest {
|
||||
let mut req = request::IpPacketRequest::new_data_request(data);
|
||||
req.protocol.version = VERSION;
|
||||
req
|
||||
}
|
||||
|
||||
/// Create a v9 IP packet response (version byte set to 9).
|
||||
pub fn new_ip_packet_response(ip_packet: bytes::Bytes) -> response::IpPacketResponse {
|
||||
let mut resp = response::IpPacketResponse::new_ip_packet(ip_packet);
|
||||
resp.version = VERSION;
|
||||
resp
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright 2026 Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
pub fn format_debug_bytes(bytes: &[u8]) -> Result<String, fmt::Error> {
|
||||
@@ -2,6 +2,7 @@
|
||||
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod debug;
|
||||
mod error;
|
||||
pub mod flood;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ tls_codec = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net", "io-util"] }
|
||||
|
||||
nym-crypto = { workspace = true, features = ["hashing"] }
|
||||
nym-common.workspace = true
|
||||
nym-kkt = { workspace = true }
|
||||
nym-kkt-ciphersuite = { workspace = true }
|
||||
|
||||
@@ -45,4 +46,4 @@ mock = ["nym-test-utils"]
|
||||
|
||||
[[bench]]
|
||||
name = "replay_protection"
|
||||
harness = false
|
||||
harness = false
|
||||
|
||||
@@ -9,13 +9,13 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LpFrameHeader {
|
||||
pub kind: LpFrameKind,
|
||||
pub frame_attributes: [u8; 14],
|
||||
pub frame_attributes: LpFrameAttributes,
|
||||
}
|
||||
|
||||
impl LpFrameHeader {
|
||||
pub const SIZE: usize = 16; // message_kind(2) + message_attributes(14)
|
||||
|
||||
pub fn new(kind: LpFrameKind, frame_attributes: [u8; 14]) -> Self {
|
||||
pub fn new(kind: LpFrameKind, frame_attributes: LpFrameAttributes) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
frame_attributes,
|
||||
@@ -103,6 +103,13 @@ impl LpFrame {
|
||||
Self::new(LpFrameKind::Forward, data)
|
||||
}
|
||||
|
||||
pub fn new_stream(attrs: SphinxStreamFrameAttributes, content: impl Into<Bytes>) -> Self {
|
||||
Self {
|
||||
header: LpFrameHeader::new(LpFrameKind::SphinxStream, attrs.encode()),
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
LpFrameHeader::SIZE + self.content.len()
|
||||
}
|
||||
@@ -115,6 +122,66 @@ pub enum LpFrameKind {
|
||||
Opaque = 0,
|
||||
Registration = 1,
|
||||
Forward = 2,
|
||||
SphinxStream = 3,
|
||||
}
|
||||
|
||||
/// Message type within a `LpFrameKind::SphinxStream` frame.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum SphinxStreamMsgType {
|
||||
/// Open a new stream. Content is optional initial data.
|
||||
Open = 0,
|
||||
/// Data on an existing stream.
|
||||
Data = 1,
|
||||
}
|
||||
|
||||
/// Parsed form of the 14-byte `frame_attributes` for `LpFrameKind::SphinxStream`.
|
||||
///
|
||||
/// Wire layout (big-endian):
|
||||
/// ```text
|
||||
/// [0..8 ) stream_id : u64
|
||||
/// [8 ) msg_type : u8 (0 = Open, 1 = Data)
|
||||
/// [9..13) sequence_num : u32
|
||||
/// [13 ) reserved : u8
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SphinxStreamFrameAttributes {
|
||||
pub stream_id: u64,
|
||||
pub msg_type: SphinxStreamMsgType,
|
||||
pub sequence_num: u32,
|
||||
}
|
||||
|
||||
/// Raw 14-byte frame attributes field in every [`LpFrameHeader`].
|
||||
/// Interpretation depends on the [`LpFrameKind`].
|
||||
pub type LpFrameAttributes = [u8; 14];
|
||||
|
||||
impl SphinxStreamFrameAttributes {
|
||||
pub fn encode(&self) -> LpFrameAttributes {
|
||||
let mut buf = [0u8; 14];
|
||||
buf[0..8].copy_from_slice(&self.stream_id.to_be_bytes());
|
||||
buf[8] = self.msg_type as u8;
|
||||
buf[9..13].copy_from_slice(&self.sequence_num.to_be_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn parse(attrs: &LpFrameAttributes) -> Result<Self, MalformedLpPacketError> {
|
||||
let stream_id = u64::from_be_bytes(attrs[0..8].try_into().unwrap());
|
||||
let msg_type = match attrs[8] {
|
||||
0 => SphinxStreamMsgType::Open,
|
||||
1 => SphinxStreamMsgType::Data,
|
||||
other => {
|
||||
return Err(MalformedLpPacketError::DeserialisationFailure(format!(
|
||||
"invalid stream msg_type: {other}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let sequence_num = u32::from_be_bytes(attrs[9..13].try_into().unwrap());
|
||||
Ok(Self {
|
||||
stream_id,
|
||||
msg_type,
|
||||
sequence_num,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::utils::format_debug_bytes;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
use nym_common::debug::format_debug_bytes;
|
||||
|
||||
pub use error::MalformedLpPacketError;
|
||||
pub use frame::{ForwardPacketData, LpFrame};
|
||||
pub use header::{InnerHeader, LpHeader, OuterHeader};
|
||||
@@ -13,7 +14,6 @@ pub mod error;
|
||||
pub mod frame;
|
||||
pub mod header;
|
||||
pub mod replay;
|
||||
pub mod utils;
|
||||
|
||||
pub mod version {
|
||||
/// The current version of the Lewes Protocol that is put into each new constructed header.
|
||||
|
||||
@@ -31,9 +31,7 @@ where
|
||||
// instead we could maybe use the `from_bytes` variant and adding some suffix
|
||||
// when computing the digest until we produce a valid scalar.
|
||||
let mut bytes = [0u8; 64];
|
||||
let pad_size = 64usize
|
||||
.checked_sub(D::OutputSize::to_usize())
|
||||
.unwrap_or_default();
|
||||
let pad_size = 64usize.saturating_sub(D::OutputSize::to_usize());
|
||||
|
||||
bytes[pad_size..].copy_from_slice(&digest);
|
||||
|
||||
|
||||
Generated
+20
-7
@@ -709,7 +709,7 @@ dependencies = [
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1258,7 +1258,7 @@ dependencies = [
|
||||
"cw2",
|
||||
"cw4",
|
||||
"nym-contracts-common",
|
||||
"nym-multisig-contract-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1329,11 +1329,11 @@ dependencies = [
|
||||
"cw3",
|
||||
"cw4",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-crypto",
|
||||
"nym-ecash-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nym-network-defaults",
|
||||
"rand_chacha",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -1344,8 +1344,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nym-ecash-contract-common"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d022e85291bf51877fbbf4688bc3762c605fdaee3b98c6407414f7c358bc5610"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1353,7 +1351,7 @@ dependencies = [
|
||||
"cw-controllers",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"nym-multisig-contract-common",
|
||||
"nym-multisig-contract-common 1.20.4",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
@@ -1416,6 +1414,21 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw3",
|
||||
"cw4",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.20.4"
|
||||
|
||||
@@ -96,3 +96,6 @@ unreachable = "deny"
|
||||
# For local development, import via path instead of crates.io, e.g.
|
||||
# [patch.crates-io]
|
||||
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
|
||||
[patch.crates-io]
|
||||
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
|
||||
@@ -39,8 +39,12 @@ nym-network-defaults = { workspace = true, default-features = false }
|
||||
anyhow = { workspace = true }
|
||||
sylvia = { workspace = true, features = ["mt"] }
|
||||
nym-crypto = { workspace = true, features = ["rand", "asymmetric"] }
|
||||
rand_chacha = "0.3"
|
||||
cw-multi-test = { workspace = true }
|
||||
nym-contracts-common-testing = { workspace = true }
|
||||
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-ecash-contract-common/schema"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -156,10 +156,10 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_deposit_value"
|
||||
"update_default_deposit_value"
|
||||
],
|
||||
"properties": {
|
||||
"update_deposit_value": {
|
||||
"update_default_deposit_value": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_deposit"
|
||||
@@ -174,6 +174,54 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Set (or overwrite) a reduced deposit price for a specific address. Only callable by the contract admin.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"set_reduced_deposit_price"
|
||||
],
|
||||
"properties": {
|
||||
"set_reduced_deposit_price": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"deposit"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"deposit": {
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Remove the reduced deposit price for a specific address, reverting them to the default price. Returns an error if the address has no custom price set. Only callable by the contract admin.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"remove_reduced_deposit_price"
|
||||
],
|
||||
"properties": {
|
||||
"remove_reduced_deposit_price": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -297,10 +345,44 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_required_deposit_amount"
|
||||
"get_default_deposit_amount"
|
||||
],
|
||||
"properties": {
|
||||
"get_required_deposit_amount": {
|
||||
"get_default_deposit_amount": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_reduced_deposit_amount"
|
||||
],
|
||||
"properties": {
|
||||
"get_reduced_deposit_amount": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_all_whitelisted_accounts"
|
||||
],
|
||||
"properties": {
|
||||
"get_all_whitelisted_accounts": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -373,6 +455,19 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_deposits_statistics"
|
||||
],
|
||||
"properties": {
|
||||
"get_deposits_statistics": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -380,10 +475,120 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
"required": [
|
||||
"initial_whitelist"
|
||||
],
|
||||
"properties": {
|
||||
"initial_whitelist": {
|
||||
"description": "Initial set of whitelisted accounts with their reduced deposit prices. Each entry is validated and stored during migration.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/WhitelistedDeposit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
},
|
||||
"WhitelistedDeposit": {
|
||||
"description": "An address and its reduced deposit price, used when seeding the whitelist via migration.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"deposit"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"deposit": {
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"sudo": null,
|
||||
"responses": {
|
||||
"get_all_whitelisted_accounts": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WhitelistedAccountsResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"whitelisted_accounts"
|
||||
],
|
||||
"properties": {
|
||||
"whitelisted_accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/WhitelistedAccount"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
},
|
||||
"WhitelistedAccount": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"deposit"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"deposit": {
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_blacklist_paged": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedBlacklistedAccountResponse",
|
||||
@@ -496,6 +701,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_default_deposit_amount": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Coin",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_deposit": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DepositResponse",
|
||||
@@ -594,6 +823,99 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_deposits_statistics": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DepositsStatistics",
|
||||
"description": "Aggregate statistics about all deposits made through the ecash contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"deposited_with_custom_price",
|
||||
"deposits_made_with_custom_price",
|
||||
"total_deposited",
|
||||
"total_deposited_with_custom_price",
|
||||
"total_deposited_with_default_price",
|
||||
"total_deposits_made",
|
||||
"total_deposits_made_with_custom_price",
|
||||
"total_deposits_made_with_default_price"
|
||||
],
|
||||
"properties": {
|
||||
"deposited_with_custom_price": {
|
||||
"description": "Per-account breakdown of deposited amounts for whitelisted addresses.",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"deposits_made_with_custom_price": {
|
||||
"description": "Per-account breakdown of deposit counts for whitelisted addresses.",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"total_deposited": {
|
||||
"description": "Total value of all deposits ever made (at any price tier), sourced from `PoolCounters::total_deposited`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"total_deposited_with_custom_price": {
|
||||
"description": "Total value deposited at custom prices, summed across all whitelisted accounts.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"total_deposited_with_default_price": {
|
||||
"description": "Total value deposited at the default price.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"total_deposits_made": {
|
||||
"description": "Total number of deposits ever made (at any price tier), derived from the deposit id counter.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"total_deposits_made_with_custom_price": {
|
||||
"description": "Number of deposits made at any custom (reduced) price, summed across all whitelisted accounts.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"total_deposits_made_with_default_price": {
|
||||
"description": "Number of deposits made at the default (non-reduced) price.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_latest_deposit": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "LatestDepositResponse",
|
||||
@@ -644,24 +966,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_required_deposit_amount": {
|
||||
"get_reduced_deposit_amount": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Coin",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
"title": "Nullable_Coin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
],
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
|
||||
@@ -104,10 +104,10 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_deposit_value"
|
||||
"update_default_deposit_value"
|
||||
],
|
||||
"properties": {
|
||||
"update_deposit_value": {
|
||||
"update_default_deposit_value": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_deposit"
|
||||
@@ -122,6 +122,54 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Set (or overwrite) a reduced deposit price for a specific address. Only callable by the contract admin.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"set_reduced_deposit_price"
|
||||
],
|
||||
"properties": {
|
||||
"set_reduced_deposit_price": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"deposit"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"deposit": {
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Remove the reduced deposit price for a specific address, reverting them to the default price. Returns an error if the address has no custom price set. Only callable by the contract admin.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"remove_reduced_deposit_price"
|
||||
],
|
||||
"properties": {
|
||||
"remove_reduced_deposit_price": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -2,5 +2,56 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
"required": [
|
||||
"initial_whitelist"
|
||||
],
|
||||
"properties": {
|
||||
"initial_whitelist": {
|
||||
"description": "Initial set of whitelisted accounts with their reduced deposit prices. Each entry is validated and stored during migration.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/WhitelistedDeposit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
},
|
||||
"WhitelistedDeposit": {
|
||||
"description": "An address and its reduced deposit price, used when seeding the whitelist via migration.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"deposit"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"deposit": {
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,44 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_required_deposit_amount"
|
||||
"get_default_deposit_amount"
|
||||
],
|
||||
"properties": {
|
||||
"get_required_deposit_amount": {
|
||||
"get_default_deposit_amount": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_reduced_deposit_amount"
|
||||
],
|
||||
"properties": {
|
||||
"get_reduced_deposit_amount": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_all_whitelisted_accounts"
|
||||
],
|
||||
"properties": {
|
||||
"get_all_whitelisted_accounts": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -131,6 +165,19 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_deposits_statistics"
|
||||
],
|
||||
"properties": {
|
||||
"get_deposits_statistics": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WhitelistedAccountsResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"whitelisted_accounts"
|
||||
],
|
||||
"properties": {
|
||||
"whitelisted_accounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/WhitelistedAccount"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
},
|
||||
"WhitelistedAccount": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"deposit"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"deposit": {
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Coin",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DepositsStatistics",
|
||||
"description": "Aggregate statistics about all deposits made through the ecash contract.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"deposited_with_custom_price",
|
||||
"deposits_made_with_custom_price",
|
||||
"total_deposited",
|
||||
"total_deposited_with_custom_price",
|
||||
"total_deposited_with_default_price",
|
||||
"total_deposits_made",
|
||||
"total_deposits_made_with_custom_price",
|
||||
"total_deposits_made_with_default_price"
|
||||
],
|
||||
"properties": {
|
||||
"deposited_with_custom_price": {
|
||||
"description": "Per-account breakdown of deposited amounts for whitelisted addresses.",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"deposits_made_with_custom_price": {
|
||||
"description": "Per-account breakdown of deposit counts for whitelisted addresses.",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"total_deposited": {
|
||||
"description": "Total value of all deposits ever made (at any price tier), sourced from `PoolCounters::total_deposited`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"total_deposited_with_custom_price": {
|
||||
"description": "Total value deposited at custom prices, summed across all whitelisted accounts.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"total_deposited_with_default_price": {
|
||||
"description": "Total value deposited at the default price.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"total_deposits_made": {
|
||||
"description": "Total number of deposits ever made (at any price tier), derived from the deposit id counter.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"total_deposits_made_with_custom_price": {
|
||||
"description": "Number of deposits made at any custom (reduced) price, summed across all whitelisted accounts.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"total_deposits_made_with_default_price": {
|
||||
"description": "Number of deposits made at the default (non-reduced) price.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Nullable_Coin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Coin"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Uint128": {
|
||||
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::NymEcashContract;
|
||||
use crate::helpers::{
|
||||
create_batch_redemption_proposal, create_blacklist_proposal, Config, ProposalId,
|
||||
};
|
||||
use crate::helpers::{create_batch_redemption_proposal, create_blacklist_proposal, ProposalId};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Deps, Storage, SubMsg, Uint128};
|
||||
use cosmwasm_std::{to_json_binary, Addr, Deps, Storage, SubMsg};
|
||||
use cw3::ProposalResponse;
|
||||
use nym_ecash_contract_common::EcashContractError;
|
||||
use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg;
|
||||
@@ -33,28 +31,6 @@ impl NymEcashContract {
|
||||
Ok(TICKETBOOK_SIZE)
|
||||
}
|
||||
|
||||
pub(crate) fn tickets_redemption_amount(
|
||||
&self,
|
||||
storage: &dyn Storage,
|
||||
config: &Config,
|
||||
number_of_tickets: u16,
|
||||
) -> Result<Coin, EcashContractError> {
|
||||
let deposit_amount = config.deposit_amount.amount;
|
||||
let ticketbook_size = Uint128::new(self.get_ticketbook_size(storage)? as u128);
|
||||
let tickets = Uint128::new(number_of_tickets as u128);
|
||||
|
||||
// how many tickets from a ticketbook you redeemed
|
||||
let book_ratio = Decimal::from_ratio(tickets, ticketbook_size);
|
||||
|
||||
// return = ticketbook_price * (tickets / ticketbook_size)
|
||||
let return_amount = deposit_amount.mul_floor(book_ratio);
|
||||
|
||||
Ok(Coin {
|
||||
denom: config.deposit_amount.denom.clone(),
|
||||
amount: return_amount,
|
||||
})
|
||||
}
|
||||
|
||||
fn must_get_multisig_addr(&self, deps: Deps) -> Result<Addr, EcashContractError> {
|
||||
// SAFETY: multisig admin MUST always be set on initialisation,
|
||||
// if the call fails, we're in some weird UB land
|
||||
@@ -117,172 +93,3 @@ impl NymEcashContract {
|
||||
Ok(proposal_response)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::TestSetupSimple;
|
||||
|
||||
#[test]
|
||||
fn ticket_redemption_amount() -> anyhow::Result<()> {
|
||||
// make sure the ticketbook size hasn't changed so that our tests are still valid
|
||||
assert_eq!(TICKETBOOK_SIZE, 50);
|
||||
|
||||
// ticketbook price of 100nym
|
||||
let test = TestSetupSimple::new().with_deposit_amount(100_000_000);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
|
||||
assert_eq!(res.amount.u128(), 2_000_000);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
|
||||
assert_eq!(res.amount.u128(), 4_000_000);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
|
||||
assert_eq!(res.amount.u128(), 10_000_000);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
|
||||
assert_eq!(res.amount.u128(), 20_000_000);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
|
||||
assert_eq!(res.amount.u128(), 60_000_000);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
|
||||
assert_eq!(res.amount.u128(), 100_000_000);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
|
||||
assert_eq!(res.amount.u128(), 246_000_000);
|
||||
|
||||
// ticketbook price of 1.5unym per ticket
|
||||
let test = TestSetupSimple::new().with_deposit_amount(75);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
|
||||
assert_eq!(res.amount.u128(), 1);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
|
||||
assert_eq!(res.amount.u128(), 3);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
|
||||
assert_eq!(res.amount.u128(), 7);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
|
||||
assert_eq!(res.amount.u128(), 15);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
|
||||
assert_eq!(res.amount.u128(), 45);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
|
||||
assert_eq!(res.amount.u128(), 75);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
|
||||
assert_eq!(res.amount.u128(), 184);
|
||||
|
||||
// ticketbook price of 1unym per ticket
|
||||
let test = TestSetupSimple::new().with_deposit_amount(50);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
|
||||
assert_eq!(res.amount.u128(), 1);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
|
||||
assert_eq!(res.amount.u128(), 2);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
|
||||
assert_eq!(res.amount.u128(), 5);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
|
||||
assert_eq!(res.amount.u128(), 10);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
|
||||
assert_eq!(res.amount.u128(), 30);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
|
||||
assert_eq!(res.amount.u128(), 50);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
|
||||
assert_eq!(res.amount.u128(), 123);
|
||||
|
||||
// ticketbook price of 1unym in total
|
||||
let test = TestSetupSimple::new().with_deposit_amount(1);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
|
||||
assert_eq!(res.amount.u128(), 1);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
|
||||
assert_eq!(res.amount.u128(), 2);
|
||||
|
||||
// ticketbook price of 0unym
|
||||
let test = TestSetupSimple::new().with_deposit_amount(0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 1)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 2)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 5)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 10)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 30)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 50)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
let res =
|
||||
test.contract()
|
||||
.tickets_redemption_amount(test.deps().storage, &test.config(), 123)?;
|
||||
assert_eq!(res.amount.u128(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
use crate::constants::{BLACKLIST_PROPOSAL_REPLY_ID, REDEMPTION_PROPOSAL_REPLY_ID};
|
||||
use crate::contract::helpers::Invariants;
|
||||
use crate::deposit::DepositStorage;
|
||||
use crate::deposit_stats::DepositStatsStorage;
|
||||
use crate::helpers::{
|
||||
BlacklistKey, Config, MultisigReply, BLACKLIST_PAGE_DEFAULT_LIMIT, BLACKLIST_PAGE_MAX_LIMIT,
|
||||
CONTRACT_NAME, CONTRACT_VERSION, DEPOSITS_PAGE_DEFAULT_LIMIT, DEPOSITS_PAGE_MAX_LIMIT,
|
||||
};
|
||||
use cosmwasm_std::{coin, BankMsg, Coin, Event, Order, Reply, Response, StdResult};
|
||||
use cosmwasm_std::{coin, Addr, Coin, DepsMut, Event, Order, Reply, Response, StdResult};
|
||||
use cw4::Cw4Contract;
|
||||
use cw_controllers::Admin;
|
||||
use cw_storage_plus::{Bound, Item, Map};
|
||||
@@ -20,9 +21,12 @@ use nym_ecash_contract_common::counters::PoolCounters;
|
||||
use nym_ecash_contract_common::deposit::{
|
||||
DepositData, DepositResponse, LatestDepositResponse, PagedDepositsResponse,
|
||||
};
|
||||
use nym_ecash_contract_common::deposit_statistics::DepositsStatistics;
|
||||
use nym_ecash_contract_common::events::{
|
||||
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ID, PROPOSAL_ID_ATTRIBUTE_NAME,
|
||||
};
|
||||
use nym_ecash_contract_common::msg::WhitelistedDeposit;
|
||||
use nym_ecash_contract_common::reduced_deposit::{WhitelistedAccount, WhitelistedAccountsResponse};
|
||||
use nym_ecash_contract_common::EcashContractError;
|
||||
use nym_network_defaults::TICKETBOOK_SIZE;
|
||||
use sylvia::ctx::{ExecCtx, InstantiateCtx, MigrateCtx, QueryCtx};
|
||||
@@ -41,6 +45,13 @@ pub struct NymEcashContract {
|
||||
pub(crate) pool_counters: Item<PoolCounters>,
|
||||
pub(crate) expected_invariants: Item<Invariants>,
|
||||
|
||||
/// Information about the performed deposits
|
||||
pub(crate) deposit_stats: DepositStatsStorage,
|
||||
|
||||
/// Map of approved addresses that are allowed to perform deposits using a reduced amount
|
||||
/// as specified by the saved value.
|
||||
pub(crate) reduced_deposits: Map<Addr, Coin>,
|
||||
|
||||
pub(crate) blacklist: Map<BlacklistKey, Blacklisting>,
|
||||
|
||||
pub(crate) deposits: DepositStorage,
|
||||
@@ -58,6 +69,8 @@ impl NymEcashContract {
|
||||
config: Item::new("config"),
|
||||
pool_counters: Item::new("pool_counters"),
|
||||
expected_invariants: Item::new("expected_invariants"),
|
||||
deposit_stats: DepositStatsStorage::new(),
|
||||
reduced_deposits: Map::new("reduced_deposits"),
|
||||
blacklist: Map::new("blacklist"),
|
||||
deposits: DepositStorage::new(),
|
||||
}
|
||||
@@ -72,6 +85,9 @@ impl NymEcashContract {
|
||||
group_addr: String,
|
||||
deposit_amount: Coin,
|
||||
) -> Result<Response, EcashContractError> {
|
||||
// all counters, deposits, etc. always use and always will use the same denom
|
||||
let zero_coin = coin(0, &deposit_amount.denom);
|
||||
|
||||
let multisig_addr = ctx.deps.api.addr_validate(&multisig_addr)?;
|
||||
let holding_account = ctx.deps.api.addr_validate(&holding_account)?;
|
||||
let group_addr = Cw4Contract(ctx.deps.api.addr_validate(&group_addr).map_err(|_| {
|
||||
@@ -96,8 +112,9 @@ impl NymEcashContract {
|
||||
self.pool_counters.save(
|
||||
ctx.deps.storage,
|
||||
&PoolCounters {
|
||||
total_deposited: coin(0, &deposit_amount.denom),
|
||||
total_redeemed: coin(0, &deposit_amount.denom),
|
||||
total_deposited: zero_coin.clone(),
|
||||
total_redeemed: zero_coin.clone(),
|
||||
tickets_requested_and_not_redeemed: 0,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -110,6 +127,14 @@ impl NymEcashContract {
|
||||
},
|
||||
)?;
|
||||
|
||||
self.deposit_stats
|
||||
.deposits_with_default_price
|
||||
.save(ctx.deps.storage, &0)?;
|
||||
|
||||
self.deposit_stats
|
||||
.deposits_with_default_price_amounts
|
||||
.save(ctx.deps.storage, &zero_coin)?;
|
||||
|
||||
cw2::set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
set_build_information!(ctx.deps.storage)?;
|
||||
|
||||
@@ -161,12 +186,49 @@ impl NymEcashContract {
|
||||
}
|
||||
|
||||
#[sv::msg(query)]
|
||||
pub fn get_required_deposit_amount(&self, ctx: QueryCtx) -> StdResult<Coin> {
|
||||
pub fn get_default_deposit_amount(&self, ctx: QueryCtx) -> StdResult<Coin> {
|
||||
let deposit_amount = self.config.load(ctx.deps.storage)?.deposit_amount;
|
||||
|
||||
Ok(deposit_amount)
|
||||
}
|
||||
|
||||
// Poor man's alias for backwards compatibility as sv::attr didn't seem to work
|
||||
#[sv::msg(query)]
|
||||
pub fn get_required_deposit_amount(&self, ctx: QueryCtx) -> StdResult<Coin> {
|
||||
self.get_default_deposit_amount(ctx)
|
||||
}
|
||||
|
||||
#[sv::msg(query)]
|
||||
pub fn get_reduced_deposit_amount(
|
||||
&self,
|
||||
ctx: QueryCtx,
|
||||
address: String,
|
||||
) -> StdResult<Option<Coin>> {
|
||||
let address = ctx.deps.api.addr_validate(&address)?;
|
||||
let deposit_amount = self.reduced_deposits.may_load(ctx.deps.storage, address)?;
|
||||
|
||||
Ok(deposit_amount)
|
||||
}
|
||||
|
||||
#[sv::msg(query)]
|
||||
pub fn get_all_whitelisted_accounts(
|
||||
&self,
|
||||
ctx: QueryCtx,
|
||||
) -> StdResult<WhitelistedAccountsResponse> {
|
||||
let whitelisted_accounts = self
|
||||
.reduced_deposits
|
||||
.range(ctx.deps.storage, None, None, Order::Ascending)
|
||||
.map(|item| {
|
||||
let (address, deposit) = item?;
|
||||
Ok(WhitelistedAccount { address, deposit })
|
||||
})
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
Ok(WhitelistedAccountsResponse {
|
||||
whitelisted_accounts,
|
||||
})
|
||||
}
|
||||
|
||||
#[sv::msg(query)]
|
||||
pub fn get_deposit(
|
||||
&self,
|
||||
@@ -226,6 +288,40 @@ impl NymEcashContract {
|
||||
})
|
||||
}
|
||||
|
||||
#[sv::msg(query)]
|
||||
pub fn get_deposits_statistics(
|
||||
&self,
|
||||
ctx: QueryCtx,
|
||||
) -> Result<DepositsStatistics, EcashContractError> {
|
||||
let storage = ctx.deps.storage;
|
||||
let denom = &self.config.load(storage)?.deposit_amount.denom;
|
||||
|
||||
let total_deposits_made = self.deposits.total_deposits_made(storage)?;
|
||||
let total_deposited = self.pool_counters.load(storage)?.total_deposited;
|
||||
|
||||
let total_deposits_made_with_default_price = self
|
||||
.deposit_stats
|
||||
.get_total_deposits_made_with_default_price(storage)?;
|
||||
let total_deposited_with_default_price = self
|
||||
.deposit_stats
|
||||
.get_total_deposited_with_default_price(storage, denom)?;
|
||||
|
||||
let custom = self
|
||||
.deposit_stats
|
||||
.get_custom_price_deposits(storage, denom)?;
|
||||
|
||||
Ok(DepositsStatistics {
|
||||
total_deposits_made,
|
||||
total_deposited,
|
||||
total_deposits_made_with_default_price,
|
||||
total_deposited_with_default_price,
|
||||
total_deposits_made_with_custom_price: custom.total_count,
|
||||
total_deposited_with_custom_price: custom.total_amount,
|
||||
deposits_made_with_custom_price: custom.per_account_count,
|
||||
deposited_with_custom_price: custom.per_account_amount,
|
||||
})
|
||||
}
|
||||
|
||||
/*=====================
|
||||
======EXECUTIONS=======
|
||||
=====================*/
|
||||
@@ -236,20 +332,48 @@ impl NymEcashContract {
|
||||
ctx: ExecCtx,
|
||||
identity_key: String,
|
||||
) -> Result<Response, EcashContractError> {
|
||||
let required_deposit = self.config.load(ctx.deps.storage)?.deposit_amount;
|
||||
let default_deposit = self.config.load(ctx.deps.storage)?.deposit_amount;
|
||||
let reduced_deposit = self
|
||||
.reduced_deposits
|
||||
.may_load(ctx.deps.storage, ctx.info.sender.clone())?;
|
||||
|
||||
let submitted = cw_utils::must_pay(&ctx.info, &required_deposit.denom)?;
|
||||
let submitted = cw_utils::must_pay(&ctx.info, &default_deposit.denom)?;
|
||||
let mut funds = ctx.info.funds;
|
||||
|
||||
if submitted != required_deposit.amount {
|
||||
let mut funds = ctx.info.funds;
|
||||
// Whitelisted accounts may deposit at either their reduced price or the
|
||||
// default price. If the default price is sent, the deposit is treated as
|
||||
// a regular (non-reduced) deposit for statistics purposes.
|
||||
if submitted == default_deposit.amount {
|
||||
self.deposit_stats
|
||||
.new_default_deposit(ctx.deps.storage, &default_deposit)?;
|
||||
} else if let Some(reduced_deposit) = reduced_deposit.as_ref() {
|
||||
// can't do if let chaining due to outdated rustc used for building contracts
|
||||
if reduced_deposit.amount == submitted {
|
||||
self.deposit_stats.new_reduced_deposit(
|
||||
ctx.deps.storage,
|
||||
&ctx.info.sender,
|
||||
reduced_deposit,
|
||||
)?;
|
||||
} else {
|
||||
// we are allowed to send reduced amounts, but we sent the wrong amount
|
||||
return Err(EcashContractError::WrongAmount {
|
||||
// SAFETY: the call to `must_pay` ensured a single coin has been sent
|
||||
#[allow(clippy::unwrap_used)]
|
||||
received: funds.pop().unwrap(),
|
||||
amount: reduced_deposit.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// we sent wrong amount of tokens
|
||||
return Err(EcashContractError::WrongAmount {
|
||||
// SAFETY: the call to `must_pay` ensured a single coin has been sent
|
||||
#[allow(clippy::unwrap_used)]
|
||||
received: funds.pop().unwrap(),
|
||||
amount: required_deposit,
|
||||
amount: default_deposit,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// global total needed when migrating to the nym pool contract
|
||||
self.pool_counters
|
||||
.update(ctx.deps.storage, |mut counters| -> StdResult<_> {
|
||||
counters.total_deposited.amount += submitted;
|
||||
@@ -285,6 +409,8 @@ impl NymEcashContract {
|
||||
Ok(Response::new().add_submessage(msg))
|
||||
}
|
||||
|
||||
/// Old legacy method for requesting ticket redemption by moving them into the holding accounts
|
||||
/// currently only executed by legacy gateways
|
||||
#[sv::msg(exec)]
|
||||
pub fn redeem_tickets(
|
||||
&self,
|
||||
@@ -301,22 +427,15 @@ impl NymEcashContract {
|
||||
self.multisig
|
||||
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
|
||||
|
||||
let config = self.config.load(ctx.deps.storage)?;
|
||||
let to_return = self.tickets_redemption_amount(ctx.deps.storage, &config, n)?;
|
||||
if to_return.amount.is_zero() {
|
||||
return Ok(Response::new());
|
||||
}
|
||||
|
||||
self.pool_counters
|
||||
.update(ctx.deps.storage, |mut counters| -> StdResult<_> {
|
||||
counters.total_redeemed.amount += to_return.amount;
|
||||
counters.tickets_requested_and_not_redeemed += n as u64;
|
||||
Ok(counters)
|
||||
})?;
|
||||
|
||||
Ok(Response::new().add_message(BankMsg::Send {
|
||||
to_address: config.holding_account.to_string(),
|
||||
amount: vec![to_return],
|
||||
}))
|
||||
Ok(Response::new().add_event(
|
||||
Event::new("ticket_redemption").add_attribute("moved_to_holding_account", "false"),
|
||||
))
|
||||
}
|
||||
|
||||
#[sv::msg(exec)]
|
||||
@@ -334,7 +453,7 @@ impl NymEcashContract {
|
||||
}
|
||||
|
||||
#[sv::msg(exec)]
|
||||
pub fn update_deposit_value(
|
||||
pub fn update_default_deposit_value(
|
||||
&self,
|
||||
ctx: ExecCtx,
|
||||
new_deposit: Coin,
|
||||
@@ -343,6 +462,14 @@ impl NymEcashContract {
|
||||
self.contract_admin
|
||||
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
|
||||
|
||||
let ticket_book_size = self.get_ticketbook_size(ctx.deps.storage)?;
|
||||
if new_deposit.amount < cosmwasm_std::Uint128::from(ticket_book_size) {
|
||||
return Err(EcashContractError::DepositBelowTicketBookSize {
|
||||
amount: new_deposit.amount,
|
||||
ticket_book_size,
|
||||
});
|
||||
}
|
||||
|
||||
let deposit_str = new_deposit.to_string();
|
||||
self.config
|
||||
.update(ctx.deps.storage, |mut cfg| -> StdResult<_> {
|
||||
@@ -352,6 +479,85 @@ impl NymEcashContract {
|
||||
Ok(Response::new().add_attribute("updated_deposit", deposit_str))
|
||||
}
|
||||
|
||||
pub(crate) fn add_reduced_deposit_address(
|
||||
&self,
|
||||
deps: DepsMut,
|
||||
address: Addr,
|
||||
deposit: &Coin,
|
||||
) -> Result<(), EcashContractError> {
|
||||
// the reduced price must be strictly less than the default to avoid
|
||||
// accidentally misconfiguring an address to pay more than everyone else
|
||||
let default = self.config.load(deps.storage)?.deposit_amount;
|
||||
if deposit.denom != default.denom {
|
||||
return Err(EcashContractError::InvalidReducedDepositDenom {
|
||||
expected: default.denom,
|
||||
got: deposit.denom.clone(),
|
||||
});
|
||||
}
|
||||
if deposit.amount >= default.amount {
|
||||
return Err(EcashContractError::ReducedDepositNotReduced {
|
||||
reduced: deposit.amount,
|
||||
default: default.amount,
|
||||
});
|
||||
}
|
||||
|
||||
let ticket_book_size = self.get_ticketbook_size(deps.storage)?;
|
||||
if deposit.amount < cosmwasm_std::Uint128::from(ticket_book_size) {
|
||||
return Err(EcashContractError::DepositBelowTicketBookSize {
|
||||
amount: deposit.amount,
|
||||
ticket_book_size,
|
||||
});
|
||||
}
|
||||
|
||||
self.reduced_deposits.save(deps.storage, address, deposit)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sv::msg(exec)]
|
||||
pub fn set_reduced_deposit_price(
|
||||
&self,
|
||||
ctx: ExecCtx,
|
||||
address: String,
|
||||
deposit: Coin,
|
||||
) -> Result<Response, EcashContractError> {
|
||||
self.contract_admin
|
||||
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
|
||||
|
||||
let addr = ctx.deps.api.addr_validate(&address)?;
|
||||
self.add_reduced_deposit_address(ctx.deps, addr.clone(), &deposit)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "set_reduced_deposit_price")
|
||||
.add_attribute("address", address)
|
||||
.add_attribute("deposit", deposit.to_string()))
|
||||
}
|
||||
|
||||
/// Removes the reduced deposit price for a given address, reverting them to
|
||||
/// the default deposit amount. This is safe to call even if the address has
|
||||
/// already deposited at the reduced price — their next deposit will simply
|
||||
/// use the default price. Historical deposit statistics are not affected.
|
||||
#[sv::msg(exec)]
|
||||
pub fn remove_reduced_deposit_price(
|
||||
&self,
|
||||
ctx: ExecCtx,
|
||||
address: String,
|
||||
) -> Result<Response, EcashContractError> {
|
||||
self.contract_admin
|
||||
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
|
||||
|
||||
let addr = ctx.deps.api.addr_validate(&address)?;
|
||||
|
||||
if !self.reduced_deposits.has(ctx.deps.storage, addr.clone()) {
|
||||
return Err(EcashContractError::NoReducedDepositPrice { address });
|
||||
}
|
||||
|
||||
self.reduced_deposits.remove(ctx.deps.storage, addr);
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "remove_reduced_deposit_price")
|
||||
.add_attribute("address", address))
|
||||
}
|
||||
|
||||
#[sv::msg(exec)]
|
||||
pub fn propose_to_blacklist(
|
||||
&self,
|
||||
@@ -459,11 +665,15 @@ impl NymEcashContract {
|
||||
=======MIGRATION=======
|
||||
=====================*/
|
||||
#[sv::msg(migrate)]
|
||||
pub fn migrate(&self, ctx: MigrateCtx) -> Result<Response, EcashContractError> {
|
||||
pub fn migrate(
|
||||
&self,
|
||||
ctx: MigrateCtx,
|
||||
initial_whitelist: Vec<WhitelistedDeposit>,
|
||||
) -> Result<Response, EcashContractError> {
|
||||
set_build_information!(ctx.deps.storage)?;
|
||||
cw2::ensure_from_older_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
queued_migrations::remove_redemption_gateway_share(ctx.deps)?;
|
||||
queued_migrations::add_tiered_pricing(ctx.deps, initial_whitelist)?;
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
@@ -1,42 +1,303 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::NymEcashContract;
|
||||
use crate::helpers::Config;
|
||||
use cosmwasm_std::{Addr, Coin, Decimal, DepsMut};
|
||||
use cw4::Cw4Contract;
|
||||
use cw_storage_plus::Item;
|
||||
use cosmwasm_std::DepsMut;
|
||||
use nym_ecash_contract_common::msg::WhitelistedDeposit;
|
||||
use nym_ecash_contract_common::EcashContractError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn remove_redemption_gateway_share(deps: DepsMut) -> Result<(), EcashContractError> {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct OldConfig {
|
||||
group_addr: Cw4Contract,
|
||||
holding_account: Addr,
|
||||
pub fn add_tiered_pricing(
|
||||
mut deps: DepsMut,
|
||||
initial_whitelist: Vec<WhitelistedDeposit>,
|
||||
) -> Result<(), EcashContractError> {
|
||||
let contract = NymEcashContract::new();
|
||||
|
||||
redemption_gateway_share: Decimal,
|
||||
deposit_amount: Coin,
|
||||
// All the deposits made so far were performed with the default price.
|
||||
let deposits_performed = contract.deposits.total_deposits_made(deps.storage)?;
|
||||
let deposits_amounts = contract.pool_counters.load(deps.storage)?.total_deposited;
|
||||
|
||||
contract
|
||||
.deposit_stats
|
||||
.deposits_with_default_price
|
||||
.save(deps.storage, &deposits_performed)?;
|
||||
|
||||
contract
|
||||
.deposit_stats
|
||||
.deposits_with_default_price_amounts
|
||||
.save(deps.storage, &deposits_amounts)?;
|
||||
|
||||
// Seed the whitelist with the initial set of reduced deposit prices.
|
||||
for whitelisted in initial_whitelist {
|
||||
let addr = deps.api.addr_validate(&whitelisted.address)?;
|
||||
|
||||
contract.add_reduced_deposit_address(deps.branch(), addr, &whitelisted.deposit)?;
|
||||
}
|
||||
|
||||
impl From<OldConfig> for Config {
|
||||
fn from(config: OldConfig) -> Self {
|
||||
Config {
|
||||
group_addr: config.group_addr,
|
||||
holding_account: config.holding_account,
|
||||
deposit_amount: config.deposit_amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const OLD_CONFIG: Item<OldConfig> = Item::new("config");
|
||||
|
||||
let old_config = OLD_CONFIG.load(deps.storage)?;
|
||||
let new_config = old_config.into();
|
||||
|
||||
NymEcashContract::new()
|
||||
.config
|
||||
.save(deps.storage, &new_config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::contract::helpers::Invariants;
|
||||
use crate::deposit::DepositStorage;
|
||||
use crate::deposit_stats::DepositStatsStorage;
|
||||
use crate::helpers::Config;
|
||||
use cosmwasm_std::testing::{mock_dependencies, MockApi, MockQuerier};
|
||||
use cosmwasm_std::{coin, Empty, MemoryStorage, OwnedDeps, Uint128};
|
||||
use cw4::Cw4Contract;
|
||||
use cw_storage_plus::Item;
|
||||
use nym_ecash_contract_common::counters::PoolCounters;
|
||||
|
||||
const DENOM: &str = "unym";
|
||||
const DEFAULT_DEPOSIT: u128 = 75_000_000;
|
||||
const TICKET_BOOK_SIZE: u64 = 50;
|
||||
|
||||
/// Initialise the contract config and invariants so that whitelist
|
||||
/// validation during migration has the values it needs.
|
||||
fn save_config_and_invariants(
|
||||
deps: &mut OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>>,
|
||||
) {
|
||||
let contract = NymEcashContract::new();
|
||||
let group_addr = deps.api.addr_make("group");
|
||||
let holding_account = deps.api.addr_make("holding");
|
||||
contract
|
||||
.config
|
||||
.save(
|
||||
deps.as_mut().storage,
|
||||
&Config {
|
||||
group_addr: Cw4Contract(group_addr),
|
||||
holding_account,
|
||||
deposit_amount: coin(DEFAULT_DEPOSIT, DENOM),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
contract
|
||||
.expected_invariants
|
||||
.save(
|
||||
deps.as_mut().storage,
|
||||
&Invariants {
|
||||
ticket_book_size: TICKET_BOOK_SIZE,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn save_pool_counters(storage: &mut dyn cosmwasm_std::Storage, total_deposited: u128) {
|
||||
let pool_counters: Item<PoolCounters> = Item::new("pool_counters");
|
||||
pool_counters
|
||||
.save(
|
||||
storage,
|
||||
&PoolCounters {
|
||||
total_deposited: coin(total_deposited, DENOM),
|
||||
total_redeemed: coin(0, DENOM),
|
||||
tickets_requested_and_not_redeemed: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_with_no_prior_deposits_initialises_stats_to_zero() {
|
||||
let mut deps = mock_dependencies();
|
||||
|
||||
// No deposit_id_counter saved — contract never had a deposit.
|
||||
save_pool_counters(deps.as_mut().storage, 0);
|
||||
|
||||
add_tiered_pricing(deps.as_mut(), vec![]).unwrap();
|
||||
|
||||
let stats = DepositStatsStorage::new();
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price_amounts
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
coin(0, DENOM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_with_prior_deposits_backfills_correct_count() {
|
||||
let mut deps = mock_dependencies();
|
||||
let n_deposits: u32 = 3;
|
||||
let total: u128 = n_deposits as u128 * 75_000_000;
|
||||
|
||||
// Simulate n_deposits having been made: counter stores the next available id,
|
||||
// which equals the number of deposits already performed.
|
||||
let deposits = DepositStorage::new();
|
||||
deposits
|
||||
.deposit_id_counter
|
||||
.save(deps.as_mut().storage, &n_deposits)
|
||||
.unwrap();
|
||||
|
||||
save_pool_counters(deps.as_mut().storage, total);
|
||||
|
||||
add_tiered_pricing(deps.as_mut(), vec![]).unwrap();
|
||||
|
||||
let stats = DepositStatsStorage::new();
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
n_deposits
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price_amounts
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
coin(total, DENOM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_with_single_deposit_backfills_count_of_one() {
|
||||
let mut deps = mock_dependencies();
|
||||
|
||||
// After one deposit, next_id returns 0 and saves counter=1.
|
||||
let deposits = DepositStorage::new();
|
||||
deposits
|
||||
.deposit_id_counter
|
||||
.save(deps.as_mut().storage, &1u32)
|
||||
.unwrap();
|
||||
|
||||
save_pool_counters(deps.as_mut().storage, 75_000_000);
|
||||
|
||||
add_tiered_pricing(deps.as_mut(), vec![]).unwrap();
|
||||
|
||||
let stats = DepositStatsStorage::new();
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_stores_valid_whitelist_entries() {
|
||||
let mut deps = mock_dependencies();
|
||||
save_pool_counters(deps.as_mut().storage, 0);
|
||||
save_config_and_invariants(&mut deps);
|
||||
|
||||
let addr1 = deps.api.addr_make("alice");
|
||||
let addr2 = deps.api.addr_make("bob");
|
||||
|
||||
let whitelist = vec![
|
||||
WhitelistedDeposit {
|
||||
address: addr1.to_string(),
|
||||
deposit: coin(10_000_000, DENOM),
|
||||
},
|
||||
WhitelistedDeposit {
|
||||
address: addr2.to_string(),
|
||||
deposit: coin(50_000_000, DENOM),
|
||||
},
|
||||
];
|
||||
|
||||
add_tiered_pricing(deps.as_mut(), whitelist).unwrap();
|
||||
|
||||
let contract = NymEcashContract::new();
|
||||
assert_eq!(
|
||||
contract
|
||||
.reduced_deposits
|
||||
.load(deps.as_ref().storage, addr1)
|
||||
.unwrap(),
|
||||
coin(10_000_000, DENOM)
|
||||
);
|
||||
assert_eq!(
|
||||
contract
|
||||
.reduced_deposits
|
||||
.load(deps.as_ref().storage, addr2)
|
||||
.unwrap(),
|
||||
coin(50_000_000, DENOM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_rejects_wrong_denom() {
|
||||
let mut deps = mock_dependencies();
|
||||
save_pool_counters(deps.as_mut().storage, 0);
|
||||
save_config_and_invariants(&mut deps);
|
||||
|
||||
let whitelist = vec![WhitelistedDeposit {
|
||||
address: deps.api.addr_make("alice").to_string(),
|
||||
deposit: coin(10_000_000, "uatom"),
|
||||
}];
|
||||
|
||||
let err = add_tiered_pricing(deps.as_mut(), whitelist).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
EcashContractError::InvalidReducedDepositDenom {
|
||||
expected: DENOM.to_string(),
|
||||
got: "uatom".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_rejects_amount_not_less_than_default() {
|
||||
let mut deps = mock_dependencies();
|
||||
save_pool_counters(deps.as_mut().storage, 0);
|
||||
save_config_and_invariants(&mut deps);
|
||||
|
||||
// Equal to default — should fail
|
||||
let whitelist = vec![WhitelistedDeposit {
|
||||
address: deps.api.addr_make("alice").to_string(),
|
||||
deposit: coin(DEFAULT_DEPOSIT, DENOM),
|
||||
}];
|
||||
|
||||
let err = add_tiered_pricing(deps.as_mut(), whitelist).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
EcashContractError::ReducedDepositNotReduced {
|
||||
reduced: Uint128::new(DEFAULT_DEPOSIT),
|
||||
default: Uint128::new(DEFAULT_DEPOSIT),
|
||||
}
|
||||
);
|
||||
|
||||
// Greater than default — should also fail
|
||||
let whitelist = vec![WhitelistedDeposit {
|
||||
address: deps.api.addr_make("alice").to_string(),
|
||||
deposit: coin(DEFAULT_DEPOSIT + 1, DENOM),
|
||||
}];
|
||||
|
||||
let err = add_tiered_pricing(deps.as_mut(), whitelist).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
EcashContractError::ReducedDepositNotReduced {
|
||||
reduced: Uint128::new(DEFAULT_DEPOSIT + 1),
|
||||
default: Uint128::new(DEFAULT_DEPOSIT),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_rejects_amount_below_ticket_book_size() {
|
||||
let mut deps = mock_dependencies();
|
||||
save_pool_counters(deps.as_mut().storage, 0);
|
||||
save_config_and_invariants(&mut deps);
|
||||
|
||||
let whitelist = vec![WhitelistedDeposit {
|
||||
address: deps.api.addr_make("alice").to_string(),
|
||||
deposit: coin(TICKET_BOOK_SIZE as u128 - 1, DENOM),
|
||||
}];
|
||||
|
||||
let err = add_tiered_pricing(deps.as_mut(), whitelist).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
EcashContractError::DepositBelowTicketBookSize {
|
||||
amount: Uint128::new(TICKET_BOOK_SIZE as u128 - 1),
|
||||
ticket_book_size: TICKET_BOOK_SIZE,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
use crate::contract::NymEcashContract;
|
||||
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env, MockApi, MockQuerier};
|
||||
use cosmwasm_std::{coin, Addr, Empty, Env, MemoryStorage, OwnedDeps};
|
||||
use sylvia::ctx::{InstantiateCtx, QueryCtx};
|
||||
use cosmwasm_std::{coin, Addr, Empty, Env, MemoryStorage, MessageInfo, OwnedDeps};
|
||||
use sylvia::ctx::{ExecCtx, InstantiateCtx, QueryCtx};
|
||||
|
||||
pub const TEST_DENOM: &str = "unym";
|
||||
|
||||
@@ -55,30 +55,48 @@ impl TestSetup {
|
||||
pub fn query_ctx(&self) -> QueryCtx<'_> {
|
||||
QueryCtx::from((self.deps.as_ref(), self.env.clone()))
|
||||
}
|
||||
|
||||
pub fn exec_ctx(&mut self, sender: MessageInfo) -> ExecCtx<'_> {
|
||||
ExecCtx::from((self.deps.as_mut(), self.env.clone(), sender))
|
||||
}
|
||||
|
||||
pub fn admin_info(&self) -> MessageInfo {
|
||||
let admin = self
|
||||
.contract
|
||||
.contract_admin
|
||||
.get(self.deps.as_ref())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
message_info(&admin, &[])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmwasm_std::coin;
|
||||
use nym_ecash_contract_common::deposit::Deposit;
|
||||
use nym_ecash_contract_common::reduced_deposit::WhitelistedAccount;
|
||||
use nym_ecash_contract_common::EcashContractError;
|
||||
use sylvia::anyhow;
|
||||
|
||||
const CONTRACT: NymEcashContract = NymEcashContract::new();
|
||||
|
||||
#[test]
|
||||
fn deposit_queries() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
|
||||
// no deposit
|
||||
let res = test.contract.get_deposit(test.query_ctx(), 42)?;
|
||||
let res = CONTRACT.get_deposit(test.query_ctx(), 42)?;
|
||||
assert!(res.deposit.is_none());
|
||||
|
||||
let deps = test.deps.as_mut();
|
||||
let deposit_id = test.contract.deposits.save_deposit(
|
||||
deps.storage,
|
||||
let deposit_id = CONTRACT.deposits.save_deposit(
|
||||
test.deps.as_mut().storage,
|
||||
"GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(),
|
||||
)?;
|
||||
|
||||
// deposit exists
|
||||
let res = test.contract.get_deposit(test.query_ctx(), deposit_id)?;
|
||||
let res = CONTRACT.get_deposit(test.query_ctx(), deposit_id)?;
|
||||
let expected = Deposit {
|
||||
bs58_encoded_ed25519_pubkey: "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(),
|
||||
};
|
||||
@@ -87,4 +105,392 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_default_deposit_amount_returns_configured_value() -> anyhow::Result<()> {
|
||||
let test = TestSetup::init();
|
||||
|
||||
let amount = CONTRACT.get_default_deposit_amount(test.query_ctx())?;
|
||||
|
||||
assert_eq!(amount, coin(75_000_000, TEST_DENOM));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_reduced_deposit_amount_returns_none_for_unlisted_address() -> anyhow::Result<()> {
|
||||
let test = TestSetup::init();
|
||||
let unknown = test.deps.api.addr_make("unknown");
|
||||
|
||||
let amount = CONTRACT.get_reduced_deposit_amount(test.query_ctx(), unknown.to_string())?;
|
||||
|
||||
assert!(amount.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_reduced_deposit_amount_returns_amount_for_whitelisted_address() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("whitelisted");
|
||||
let reduced = coin(10_000_000, TEST_DENOM);
|
||||
|
||||
CONTRACT
|
||||
.reduced_deposits
|
||||
.save(test.deps.as_mut().storage, addr.clone(), &reduced)?;
|
||||
|
||||
let amount = CONTRACT.get_reduced_deposit_amount(test.query_ctx(), addr.to_string())?;
|
||||
|
||||
assert_eq!(amount, Some(reduced));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- get_all_whitelisted_accounts ---
|
||||
|
||||
#[test]
|
||||
fn get_all_whitelisted_accounts_returns_empty_by_default() -> anyhow::Result<()> {
|
||||
let test = TestSetup::init();
|
||||
|
||||
let res = CONTRACT.get_all_whitelisted_accounts(test.query_ctx())?;
|
||||
assert!(res.whitelisted_accounts.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_all_whitelisted_accounts_returns_all_entries() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let alice = test.deps.api.addr_make("alice");
|
||||
let bob = test.deps.api.addr_make("bob");
|
||||
|
||||
let admin = test.admin_info();
|
||||
CONTRACT.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
alice.to_string(),
|
||||
coin(10_000_000, TEST_DENOM),
|
||||
)?;
|
||||
|
||||
let admin = test.admin_info();
|
||||
CONTRACT.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
bob.to_string(),
|
||||
coin(5_000_000, TEST_DENOM),
|
||||
)?;
|
||||
|
||||
let res = CONTRACT.get_all_whitelisted_accounts(test.query_ctx())?;
|
||||
assert_eq!(res.whitelisted_accounts.len(), 2);
|
||||
|
||||
assert!(res.whitelisted_accounts.contains(&WhitelistedAccount {
|
||||
address: alice,
|
||||
deposit: coin(10_000_000, TEST_DENOM),
|
||||
}));
|
||||
assert!(res.whitelisted_accounts.contains(&WhitelistedAccount {
|
||||
address: bob,
|
||||
deposit: coin(5_000_000, TEST_DENOM),
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- set_reduced_deposit_price ---
|
||||
|
||||
#[test]
|
||||
fn set_reduced_deposit_price_requires_admin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let non_admin = test.deps.api.addr_make("non_admin");
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
|
||||
let err = CONTRACT
|
||||
.set_reduced_deposit_price(
|
||||
test.exec_ctx(message_info(&non_admin, &[])),
|
||||
addr.to_string(),
|
||||
coin(10_000_000, TEST_DENOM),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, EcashContractError::Admin(_)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_reduced_deposit_price_rejects_wrong_denom() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
let admin = test.admin_info();
|
||||
|
||||
let err = CONTRACT
|
||||
.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
addr.to_string(),
|
||||
coin(10_000_000, "uatom"),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
EcashContractError::InvalidReducedDepositDenom {
|
||||
expected: TEST_DENOM.to_string(),
|
||||
got: "uatom".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_reduced_deposit_price_rejects_amount_equal_to_default() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
let admin = test.admin_info();
|
||||
|
||||
let err = CONTRACT
|
||||
.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
addr.to_string(),
|
||||
coin(75_000_000, TEST_DENOM), // same as default
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
EcashContractError::ReducedDepositNotReduced { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_reduced_deposit_price_rejects_amount_above_default() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
let admin = test.admin_info();
|
||||
|
||||
let err = CONTRACT
|
||||
.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
addr.to_string(),
|
||||
coin(100_000_000, TEST_DENOM),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
EcashContractError::ReducedDepositNotReduced { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_reduced_deposit_price_rejects_amount_below_ticket_book_size() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
let admin = test.admin_info();
|
||||
|
||||
let err = CONTRACT
|
||||
.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
addr.to_string(),
|
||||
coin(10, TEST_DENOM), // below ticket_book_size (50)
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
EcashContractError::DepositBelowTicketBookSize { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_reduced_deposit_price_stores_price() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
let reduced = coin(10_000_000, TEST_DENOM);
|
||||
let admin = test.admin_info();
|
||||
|
||||
CONTRACT.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
addr.to_string(),
|
||||
reduced.clone(),
|
||||
)?;
|
||||
|
||||
let stored = CONTRACT.get_reduced_deposit_amount(test.query_ctx(), addr.to_string())?;
|
||||
|
||||
assert_eq!(stored, Some(reduced));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_reduced_deposit_price_overwrites_existing_price() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
|
||||
let admin = test.admin_info();
|
||||
CONTRACT.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
addr.to_string(),
|
||||
coin(10_000_000, TEST_DENOM),
|
||||
)?;
|
||||
|
||||
let admin = test.admin_info();
|
||||
CONTRACT.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
addr.to_string(),
|
||||
coin(5_000_000, TEST_DENOM),
|
||||
)?;
|
||||
|
||||
let stored = CONTRACT.get_reduced_deposit_amount(test.query_ctx(), addr.to_string())?;
|
||||
|
||||
assert_eq!(stored, Some(coin(5_000_000, TEST_DENOM)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- remove_reduced_deposit_price ---
|
||||
|
||||
#[test]
|
||||
fn remove_reduced_deposit_price_requires_admin() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let non_admin = test.deps.api.addr_make("non_admin");
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
|
||||
let err = CONTRACT
|
||||
.remove_reduced_deposit_price(
|
||||
test.exec_ctx(message_info(&non_admin, &[])),
|
||||
addr.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, EcashContractError::Admin(_)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_reduced_deposit_price_clears_stored_price() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
|
||||
let admin = test.admin_info();
|
||||
CONTRACT.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
addr.to_string(),
|
||||
coin(10_000_000, TEST_DENOM),
|
||||
)?;
|
||||
|
||||
let admin = test.admin_info();
|
||||
CONTRACT.remove_reduced_deposit_price(test.exec_ctx(admin), addr.to_string())?;
|
||||
|
||||
let stored = CONTRACT.get_reduced_deposit_amount(test.query_ctx(), addr.to_string())?;
|
||||
|
||||
assert!(stored.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_reduced_deposit_price_errors_for_unlisted_address() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let addr = test.deps.api.addr_make("alice");
|
||||
|
||||
let admin = test.admin_info();
|
||||
let err = CONTRACT
|
||||
.remove_reduced_deposit_price(test.exec_ctx(admin), addr.to_string())
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
EcashContractError::NoReducedDepositPrice {
|
||||
address: addr.to_string()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- get_deposits_statistics ---
|
||||
|
||||
#[test]
|
||||
fn get_deposits_statistics_returns_zeroes_after_init() -> anyhow::Result<()> {
|
||||
let test = TestSetup::init();
|
||||
|
||||
let stats = CONTRACT.get_deposits_statistics(test.query_ctx())?;
|
||||
|
||||
assert_eq!(stats.total_deposits_made, 0);
|
||||
assert_eq!(stats.total_deposited, coin(0, TEST_DENOM));
|
||||
assert_eq!(stats.total_deposits_made_with_default_price, 0);
|
||||
assert_eq!(
|
||||
stats.total_deposited_with_default_price,
|
||||
coin(0, TEST_DENOM)
|
||||
);
|
||||
assert_eq!(stats.total_deposits_made_with_custom_price, 0);
|
||||
assert_eq!(stats.total_deposited_with_custom_price, coin(0, TEST_DENOM));
|
||||
assert!(stats.deposits_made_with_custom_price.is_empty());
|
||||
assert!(stats.deposited_with_custom_price.is_empty());
|
||||
|
||||
CONTRACT
|
||||
.deposit_stats
|
||||
.assert_counts_consistent(test.deps.as_ref().storage, stats.total_deposits_made);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deposit_stats_invariant_holds_after_mixed_deposits() -> anyhow::Result<()> {
|
||||
let mut test = TestSetup::init();
|
||||
let alice = test.deps.api.addr_make("alice");
|
||||
let bob = test.deps.api.addr_make("bob");
|
||||
|
||||
// whitelist alice
|
||||
let admin = test.admin_info();
|
||||
CONTRACT.set_reduced_deposit_price(
|
||||
test.exec_ctx(admin),
|
||||
alice.to_string(),
|
||||
coin(10_000_000, TEST_DENOM),
|
||||
)?;
|
||||
|
||||
// alice deposits at reduced price
|
||||
let alice_info = message_info(&alice, &[coin(10_000_000, TEST_DENOM)]);
|
||||
CONTRACT.deposit_ticket_book_funds(
|
||||
test.exec_ctx(alice_info),
|
||||
"GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(),
|
||||
)?;
|
||||
|
||||
// bob deposits at default price
|
||||
let bob_info = message_info(&bob, &[coin(75_000_000, TEST_DENOM)]);
|
||||
CONTRACT.deposit_ticket_book_funds(
|
||||
test.exec_ctx(bob_info),
|
||||
"GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(),
|
||||
)?;
|
||||
|
||||
// alice deposits again at reduced price
|
||||
let alice_info = message_info(&alice, &[coin(10_000_000, TEST_DENOM)]);
|
||||
CONTRACT.deposit_ticket_book_funds(
|
||||
test.exec_ctx(alice_info),
|
||||
"GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(),
|
||||
)?;
|
||||
|
||||
// alice deposits at the default price — should be treated as a normal deposit
|
||||
let alice_info = message_info(&alice, &[coin(75_000_000, TEST_DENOM)]);
|
||||
CONTRACT.deposit_ticket_book_funds(
|
||||
test.exec_ctx(alice_info),
|
||||
"GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(),
|
||||
)?;
|
||||
|
||||
let total = CONTRACT
|
||||
.deposits
|
||||
.total_deposits_made(test.deps.as_ref().storage)?;
|
||||
assert_eq!(total, 4);
|
||||
|
||||
CONTRACT
|
||||
.deposit_stats
|
||||
.assert_counts_consistent(test.deps.as_ref().storage, total);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ impl DepositStorage {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the total number of deposits ever made.
|
||||
///
|
||||
/// The deposit id counter stores the next available id, which equals the
|
||||
/// total count (first deposit gets id 0, counter becomes 1, and so on).
|
||||
pub fn total_deposits_made(&self, storage: &dyn Storage) -> Result<u32, EcashContractError> {
|
||||
Ok(self.deposit_id_counter.may_load(storage)?.unwrap_or(0))
|
||||
}
|
||||
|
||||
fn next_id(&self, store: &mut dyn Storage) -> Result<DepositId, EcashContractError> {
|
||||
let id: DepositId = self.deposit_id_counter.may_load(store)?.unwrap_or_default();
|
||||
let next_id = id + 1;
|
||||
@@ -115,8 +123,8 @@ impl StoredDeposits {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::test_rng;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use nym_contracts_common_testing::test_rng;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
#[test]
|
||||
@@ -142,6 +150,23 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_deposits_made_tracks_count() -> anyhow::Result<()> {
|
||||
let mut deps = mock_dependencies();
|
||||
let storage = DepositStorage::new();
|
||||
|
||||
assert_eq!(storage.total_deposits_made(deps.as_ref().storage)?, 0);
|
||||
|
||||
let _ = storage.next_id(deps.as_mut().storage)?;
|
||||
assert_eq!(storage.total_deposits_made(deps.as_ref().storage)?, 1);
|
||||
|
||||
let _ = storage.next_id(deps.as_mut().storage)?;
|
||||
let _ = storage.next_id(deps.as_mut().storage)?;
|
||||
assert_eq!(storage.total_deposits_made(deps.as_ref().storage)?, 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterating_over_deposits() {
|
||||
let mut deps = mock_dependencies();
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_std::{coin, Addr, Coin, Order, StdResult, Storage};
|
||||
use cw_storage_plus::{Item, Map};
|
||||
use nym_ecash_contract_common::EcashContractError;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) struct DepositStatsStorage {
|
||||
/// Total deposits performed with the default price
|
||||
pub(crate) deposits_with_default_price: Item<u32>,
|
||||
|
||||
/// Total amounts deposited with the default price
|
||||
pub(crate) deposits_with_default_price_amounts: Item<Coin>,
|
||||
|
||||
/// Total deposits performed with a custom price by account
|
||||
pub(crate) deposits_with_custom_price: Map<Addr, u32>,
|
||||
|
||||
/// Total amounts deposited with a custom price by account
|
||||
pub(crate) deposits_with_custom_price_amounts: Map<Addr, Coin>,
|
||||
}
|
||||
|
||||
impl DepositStatsStorage {
|
||||
pub(crate) const fn new() -> Self {
|
||||
Self {
|
||||
deposits_with_default_price: Item::new("deposits_with_default_price"),
|
||||
deposits_with_default_price_amounts: Item::new("deposits_with_default_price_amounts"),
|
||||
deposits_with_custom_price: Map::new("deposits_with_custom_price"),
|
||||
deposits_with_custom_price_amounts: Map::new("deposits_with_custom_price_amounts"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_default_deposit(
|
||||
&self,
|
||||
store: &mut dyn Storage,
|
||||
deposited: &Coin,
|
||||
) -> Result<(), EcashContractError> {
|
||||
self.deposits_with_default_price
|
||||
.update(store, |count| StdResult::Ok(count + 1))?;
|
||||
self.deposits_with_default_price_amounts
|
||||
.update(store, |amount| {
|
||||
let mut updated = amount;
|
||||
updated.amount += deposited.amount;
|
||||
StdResult::Ok(updated)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn new_reduced_deposit(
|
||||
&self,
|
||||
store: &mut dyn Storage,
|
||||
sender: &Addr,
|
||||
deposited: &Coin,
|
||||
) -> Result<(), EcashContractError> {
|
||||
self.deposits_with_custom_price
|
||||
.update(store, sender.clone(), |count| {
|
||||
StdResult::Ok(count.unwrap_or_default() + 1)
|
||||
})?;
|
||||
|
||||
self.deposits_with_custom_price_amounts
|
||||
.update(store, sender.clone(), |amount| {
|
||||
let updated = match amount {
|
||||
None => deposited.clone(),
|
||||
Some(mut existing) => {
|
||||
existing.amount += deposited.amount;
|
||||
existing
|
||||
}
|
||||
};
|
||||
StdResult::Ok(updated)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_total_deposits_made_with_default_price(
|
||||
&self,
|
||||
store: &dyn Storage,
|
||||
) -> StdResult<u32> {
|
||||
Ok(self
|
||||
.deposits_with_default_price
|
||||
.may_load(store)?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
pub(crate) fn get_total_deposited_with_default_price(
|
||||
&self,
|
||||
store: &dyn Storage,
|
||||
denom: &str,
|
||||
) -> StdResult<Coin> {
|
||||
Ok(self
|
||||
.deposits_with_default_price_amounts
|
||||
.may_load(store)?
|
||||
.unwrap_or_else(|| coin(0, denom)))
|
||||
}
|
||||
|
||||
pub(crate) fn get_custom_price_deposits(
|
||||
&self,
|
||||
store: &dyn Storage,
|
||||
denom: &str,
|
||||
) -> StdResult<CustomPriceDepositStats> {
|
||||
let mut total_count = 0;
|
||||
let mut total_amount = coin(0, denom);
|
||||
let mut per_account_count = HashMap::new();
|
||||
let mut per_account_amount = HashMap::new();
|
||||
|
||||
for item in self
|
||||
.deposits_with_custom_price
|
||||
.range(store, None, None, Order::Ascending)
|
||||
{
|
||||
let (addr, count) = item?;
|
||||
total_count += count;
|
||||
per_account_count.insert(addr.into_string(), count);
|
||||
}
|
||||
|
||||
for item in
|
||||
self.deposits_with_custom_price_amounts
|
||||
.range(store, None, None, Order::Ascending)
|
||||
{
|
||||
let (addr, amount) = item?;
|
||||
total_amount.amount += amount.amount;
|
||||
per_account_amount.insert(addr.into_string(), amount);
|
||||
}
|
||||
|
||||
Ok(CustomPriceDepositStats {
|
||||
total_count,
|
||||
total_amount,
|
||||
per_account_count,
|
||||
per_account_amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DepositStatsStorage {
|
||||
/// Asserts that the per-tier deposit counts sum to the given total.
|
||||
/// Only meaningful when all deposits go through the contract entry point
|
||||
/// (not after raw storage writes that bypass bookkeeping).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn assert_counts_consistent(&self, store: &dyn Storage, total_deposits_made: u32) {
|
||||
let default_count = self
|
||||
.get_total_deposits_made_with_default_price(store)
|
||||
.unwrap();
|
||||
let custom = self.get_custom_price_deposits(store, "unused").unwrap();
|
||||
assert_eq!(
|
||||
default_count + custom.total_count,
|
||||
total_deposits_made,
|
||||
"deposit stats invariant violated: default ({default_count}) + custom ({}) != total ({total_deposits_made})",
|
||||
custom.total_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CustomPriceDepositStats {
|
||||
pub(crate) total_count: u32,
|
||||
pub(crate) total_amount: Coin,
|
||||
pub(crate) per_account_count: HashMap<String, u32>,
|
||||
pub(crate) per_account_amount: HashMap<String, Coin>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmwasm_std::coin;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
|
||||
const DENOM: &str = "unym";
|
||||
const DEFAULT_AMOUNT: u128 = 75_000_000;
|
||||
const REDUCED_AMOUNT: u128 = 10_000_000;
|
||||
|
||||
/// Mirror what `instantiate` does: zero-initialise the default-price counters.
|
||||
/// The custom-price Maps need no initialisation (they start empty).
|
||||
fn init_stats(storage: &mut dyn Storage) -> DepositStatsStorage {
|
||||
let stats = DepositStatsStorage::new();
|
||||
stats.deposits_with_default_price.save(storage, &0).unwrap();
|
||||
stats
|
||||
.deposits_with_default_price_amounts
|
||||
.save(storage, &coin(0, DENOM))
|
||||
.unwrap();
|
||||
stats
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_default_deposit_increments_count_and_amount() {
|
||||
let mut deps = mock_dependencies();
|
||||
let stats = init_stats(deps.as_mut().storage);
|
||||
|
||||
stats
|
||||
.new_default_deposit(deps.as_mut().storage, &coin(DEFAULT_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price_amounts
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
coin(DEFAULT_AMOUNT, DENOM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_default_deposits_accumulate() {
|
||||
let mut deps = mock_dependencies();
|
||||
let stats = init_stats(deps.as_mut().storage);
|
||||
|
||||
for _ in 0..3 {
|
||||
stats
|
||||
.new_default_deposit(deps.as_mut().storage, &coin(DEFAULT_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price_amounts
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
coin(DEFAULT_AMOUNT * 3, DENOM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_reduced_deposit_is_tracked_per_address() {
|
||||
let mut deps = mock_dependencies();
|
||||
let stats = init_stats(deps.as_mut().storage);
|
||||
let alice = deps.api.addr_make("alice");
|
||||
|
||||
stats
|
||||
.new_reduced_deposit(deps.as_mut().storage, &alice, &coin(REDUCED_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price
|
||||
.load(deps.as_ref().storage, alice.clone())
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price_amounts
|
||||
.load(deps.as_ref().storage, alice.clone())
|
||||
.unwrap(),
|
||||
coin(REDUCED_AMOUNT, DENOM)
|
||||
);
|
||||
// default-price stats must be untouched
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_reduced_deposits_same_address_accumulate() {
|
||||
let mut deps = mock_dependencies();
|
||||
let stats = init_stats(deps.as_mut().storage);
|
||||
let alice = deps.api.addr_make("alice");
|
||||
|
||||
for _ in 0..4 {
|
||||
stats
|
||||
.new_reduced_deposit(deps.as_mut().storage, &alice, &coin(REDUCED_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price
|
||||
.load(deps.as_ref().storage, alice.clone())
|
||||
.unwrap(),
|
||||
4
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price_amounts
|
||||
.load(deps.as_ref().storage, alice.clone())
|
||||
.unwrap(),
|
||||
coin(REDUCED_AMOUNT * 4, DENOM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reduced_deposits_for_different_addresses_tracked_independently() {
|
||||
let mut deps = mock_dependencies();
|
||||
let stats = init_stats(deps.as_mut().storage);
|
||||
let alice = deps.api.addr_make("alice");
|
||||
let bob = deps.api.addr_make("bob");
|
||||
|
||||
stats
|
||||
.new_reduced_deposit(deps.as_mut().storage, &alice, &coin(REDUCED_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
stats
|
||||
.new_reduced_deposit(deps.as_mut().storage, &alice, &coin(REDUCED_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
stats
|
||||
.new_reduced_deposit(deps.as_mut().storage, &bob, &coin(5_000_000, DENOM))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price
|
||||
.load(deps.as_ref().storage, alice.clone())
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price
|
||||
.load(deps.as_ref().storage, bob.clone())
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price_amounts
|
||||
.load(deps.as_ref().storage, alice)
|
||||
.unwrap(),
|
||||
coin(REDUCED_AMOUNT * 2, DENOM)
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price_amounts
|
||||
.load(deps.as_ref().storage, bob)
|
||||
.unwrap(),
|
||||
coin(5_000_000, DENOM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_and_reduced_stats_do_not_interfere() {
|
||||
let mut deps = mock_dependencies();
|
||||
let stats = init_stats(deps.as_mut().storage);
|
||||
let alice = deps.api.addr_make("alice");
|
||||
|
||||
stats
|
||||
.new_default_deposit(deps.as_mut().storage, &coin(DEFAULT_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
stats
|
||||
.new_reduced_deposit(deps.as_mut().storage, &alice, &coin(REDUCED_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
stats
|
||||
.new_default_deposit(deps.as_mut().storage, &coin(DEFAULT_AMOUNT, DENOM))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_default_price
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
stats
|
||||
.deposits_with_custom_price
|
||||
.load(deps.as_ref().storage, alice)
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub struct Config {
|
||||
pub group_addr: Cw4Contract,
|
||||
pub holding_account: Addr,
|
||||
|
||||
/// Specifies the expected default deposit amount if the sender is not in the whitelisted set.
|
||||
#[serde(alias = "default_deposit_amount")]
|
||||
pub deposit_amount: Coin,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
#![warn(clippy::todo)]
|
||||
#![warn(clippy::dbg_macro)]
|
||||
|
||||
mod constants;
|
||||
pub mod contract;
|
||||
mod deposit;
|
||||
mod deposit_stats;
|
||||
mod helpers;
|
||||
#[cfg(test)]
|
||||
pub mod multitest;
|
||||
mod support;
|
||||
|
||||
@@ -10,6 +10,9 @@ use sylvia::{cw_multi_test::App as MtApp, multitest::App};
|
||||
|
||||
use crate::contract::sv::mt::{CodeId, NymEcashContractProxy};
|
||||
|
||||
const DENOM: &str = "unym";
|
||||
const DEPOSIT_AMOUNT: u128 = 75_000_000;
|
||||
|
||||
#[test]
|
||||
fn invalid_deposit() {
|
||||
let owner = "owner".into_bech32();
|
||||
@@ -73,3 +76,198 @@ fn invalid_deposit() {
|
||||
EcashContractError::InvalidDeposit(PaymentError::MissingDenom(denom.to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_deposit_amount() {
|
||||
let owner = "owner".into_bech32();
|
||||
|
||||
let mtapp = MtApp::new(|router, _, storage| {
|
||||
router
|
||||
.bank
|
||||
.init_balance(storage, &owner, vec![Coin::new(1_000_000_000u128, DENOM)])
|
||||
.unwrap()
|
||||
});
|
||||
let app = App::new(mtapp);
|
||||
let code_id = CodeId::store_code(&app);
|
||||
let contract = code_id
|
||||
.instantiate(
|
||||
MockApi::default().addr_make("holding_account").to_string(),
|
||||
MockApi::default().addr_make("multisig_addr").to_string(),
|
||||
MockApi::default().addr_make("group_addr").to_string(),
|
||||
coin(DEPOSIT_AMOUNT, DENOM),
|
||||
)
|
||||
.call(&owner)
|
||||
.unwrap();
|
||||
|
||||
let vk = "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK";
|
||||
|
||||
// too little
|
||||
assert_eq!(
|
||||
contract
|
||||
.deposit_ticket_book_funds(vk.to_string())
|
||||
.with_funds(&[coin(1_000_000u128, DENOM)])
|
||||
.call(&owner)
|
||||
.unwrap_err(),
|
||||
EcashContractError::WrongAmount {
|
||||
received: coin(1_000_000u128, DENOM),
|
||||
amount: coin(DEPOSIT_AMOUNT, DENOM),
|
||||
}
|
||||
);
|
||||
|
||||
// too much
|
||||
assert_eq!(
|
||||
contract
|
||||
.deposit_ticket_book_funds(vk.to_string())
|
||||
.with_funds(&[coin(100_000_000u128, DENOM)])
|
||||
.call(&owner)
|
||||
.unwrap_err(),
|
||||
EcashContractError::WrongAmount {
|
||||
received: coin(100_000_000u128, DENOM),
|
||||
amount: coin(DEPOSIT_AMOUNT, DENOM),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correct_default_deposit_succeeds() {
|
||||
let owner = "owner".into_bech32();
|
||||
|
||||
let mtapp = MtApp::new(|router, _, storage| {
|
||||
router
|
||||
.bank
|
||||
.init_balance(storage, &owner, vec![Coin::new(1_000_000_000u128, DENOM)])
|
||||
.unwrap()
|
||||
});
|
||||
let app = App::new(mtapp);
|
||||
let code_id = CodeId::store_code(&app);
|
||||
let contract = code_id
|
||||
.instantiate(
|
||||
MockApi::default().addr_make("holding_account").to_string(),
|
||||
MockApi::default().addr_make("multisig_addr").to_string(),
|
||||
MockApi::default().addr_make("group_addr").to_string(),
|
||||
coin(DEPOSIT_AMOUNT, DENOM),
|
||||
)
|
||||
.call(&owner)
|
||||
.unwrap();
|
||||
|
||||
let vk = "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK";
|
||||
|
||||
contract
|
||||
.deposit_ticket_book_funds(vk.to_string())
|
||||
.with_funds(&[coin(DEPOSIT_AMOUNT, DENOM)])
|
||||
.call(&owner)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reduced_price_deposit_end_to_end() {
|
||||
let owner = "owner".into_bech32();
|
||||
let whitelisted = "whitelisted".into_bech32();
|
||||
let non_whitelisted = "non_whitelisted".into_bech32();
|
||||
let reduced_amount: u128 = 10_000_000;
|
||||
|
||||
let mtapp = MtApp::new(|router, _, storage| {
|
||||
router
|
||||
.bank
|
||||
.init_balance(
|
||||
storage,
|
||||
&whitelisted,
|
||||
vec![Coin::new(1_000_000_000u128, DENOM)],
|
||||
)
|
||||
.unwrap();
|
||||
router
|
||||
.bank
|
||||
.init_balance(
|
||||
storage,
|
||||
&non_whitelisted,
|
||||
vec![Coin::new(1_000_000_000u128, DENOM)],
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
let app = App::new(mtapp);
|
||||
let code_id = CodeId::store_code(&app);
|
||||
let contract = code_id
|
||||
.instantiate(
|
||||
MockApi::default().addr_make("holding_account").to_string(),
|
||||
MockApi::default().addr_make("multisig_addr").to_string(),
|
||||
MockApi::default().addr_make("group_addr").to_string(),
|
||||
coin(DEPOSIT_AMOUNT, DENOM),
|
||||
)
|
||||
.call(&owner)
|
||||
.unwrap();
|
||||
|
||||
let vk = "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK";
|
||||
|
||||
// whitelist an address with a reduced price
|
||||
contract
|
||||
.set_reduced_deposit_price(whitelisted.to_string(), coin(reduced_amount, DENOM))
|
||||
.call(&owner)
|
||||
.unwrap();
|
||||
|
||||
// whitelisted address can deposit at the reduced price
|
||||
contract
|
||||
.deposit_ticket_book_funds(vk.to_string())
|
||||
.with_funds(&[coin(reduced_amount, DENOM)])
|
||||
.call(&whitelisted)
|
||||
.unwrap();
|
||||
|
||||
// whitelisted address can also deposit at the default price —
|
||||
// treated as a normal (non-reduced) deposit for statistics purposes
|
||||
contract
|
||||
.deposit_ticket_book_funds(vk.to_string())
|
||||
.with_funds(&[coin(DEPOSIT_AMOUNT, DENOM)])
|
||||
.call(&whitelisted)
|
||||
.unwrap();
|
||||
|
||||
// whitelisted address is rejected when sending an amount that is
|
||||
// neither the reduced nor the default price
|
||||
assert_eq!(
|
||||
contract
|
||||
.deposit_ticket_book_funds(vk.to_string())
|
||||
.with_funds(&[coin(50_000_000, DENOM)])
|
||||
.call(&whitelisted)
|
||||
.unwrap_err(),
|
||||
EcashContractError::WrongAmount {
|
||||
received: coin(50_000_000, DENOM),
|
||||
amount: coin(reduced_amount, DENOM),
|
||||
}
|
||||
);
|
||||
|
||||
// non-whitelisted address is rejected at the reduced amount
|
||||
assert_eq!(
|
||||
contract
|
||||
.deposit_ticket_book_funds(vk.to_string())
|
||||
.with_funds(&[coin(reduced_amount, DENOM)])
|
||||
.call(&non_whitelisted)
|
||||
.unwrap_err(),
|
||||
EcashContractError::WrongAmount {
|
||||
received: coin(reduced_amount, DENOM),
|
||||
amount: coin(DEPOSIT_AMOUNT, DENOM),
|
||||
}
|
||||
);
|
||||
|
||||
// non-whitelisted address succeeds at the default amount
|
||||
contract
|
||||
.deposit_ticket_book_funds(vk.to_string())
|
||||
.with_funds(&[coin(DEPOSIT_AMOUNT, DENOM)])
|
||||
.call(&non_whitelisted)
|
||||
.unwrap();
|
||||
|
||||
let stats = contract.get_deposits_statistics().unwrap();
|
||||
assert_eq!(stats.total_deposits_made, 3);
|
||||
assert_eq!(
|
||||
stats.total_deposited,
|
||||
coin(reduced_amount + DEPOSIT_AMOUNT * 2, DENOM)
|
||||
);
|
||||
// whitelisted depositing at default price + non-whitelisted = 2 default deposits
|
||||
assert_eq!(stats.total_deposits_made_with_default_price, 2);
|
||||
assert_eq!(
|
||||
stats.total_deposited_with_default_price,
|
||||
coin(DEPOSIT_AMOUNT * 2, DENOM)
|
||||
);
|
||||
assert_eq!(stats.total_deposits_made_with_custom_price, 1);
|
||||
assert_eq!(
|
||||
stats.total_deposited_with_custom_price,
|
||||
coin(reduced_amount, DENOM)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::NymEcashContract;
|
||||
use crate::helpers::Config;
|
||||
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env, MockApi, MockQuerier};
|
||||
use cosmwasm_std::{coin, Addr, Deps, Empty, Env, MemoryStorage, MessageInfo, OwnedDeps};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use sylvia::ctx::{ExecCtx, InstantiateCtx, QueryCtx};
|
||||
|
||||
pub fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
|
||||
const CONTRACT: NymEcashContract = NymEcashContract::new();
|
||||
|
||||
const DENOM: &str = "unym";
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct TestSetupSimple {
|
||||
pub deps: OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>>,
|
||||
pub env: Env,
|
||||
pub rng: ChaCha20Rng,
|
||||
pub owner: Addr,
|
||||
pub holding_account: Addr,
|
||||
pub multisig_contract: Addr,
|
||||
pub group_contract: Addr,
|
||||
}
|
||||
|
||||
impl TestSetupSimple {
|
||||
pub fn new() -> Self {
|
||||
let mut deps = mock_dependencies();
|
||||
let env = mock_env();
|
||||
let owner = deps.api.addr_make("owner");
|
||||
|
||||
let rng = test_rng();
|
||||
let holding_account = deps.api.addr_make("holding_account");
|
||||
let multisig_contract = deps.api.addr_make("multisig_contract");
|
||||
let group_contract = deps.api.addr_make("group_contract");
|
||||
|
||||
let init_ctx =
|
||||
InstantiateCtx::from((deps.as_mut(), env.clone(), message_info(&owner, &[])));
|
||||
|
||||
CONTRACT
|
||||
.instantiate(
|
||||
init_ctx,
|
||||
holding_account.to_string(),
|
||||
multisig_contract.to_string(),
|
||||
group_contract.to_string(),
|
||||
coin(75_000_000, DENOM.to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
TestSetupSimple {
|
||||
deps,
|
||||
env,
|
||||
rng,
|
||||
owner,
|
||||
holding_account,
|
||||
multisig_contract,
|
||||
group_contract,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn admin(&self) -> MessageInfo {
|
||||
let admin = CONTRACT
|
||||
.contract_admin
|
||||
.get(self.deps.as_ref())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
message_info(&admin, &[])
|
||||
}
|
||||
|
||||
pub fn execute_ctx(&mut self, sender: MessageInfo) -> ExecCtx<'_> {
|
||||
let env = self.env.clone();
|
||||
ExecCtx::from((self.deps.as_mut(), env, sender))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn query_ctx(&self) -> QueryCtx<'_> {
|
||||
QueryCtx::from((self.deps.as_ref(), self.env.clone()))
|
||||
}
|
||||
|
||||
pub fn contract(&self) -> NymEcashContract {
|
||||
CONTRACT
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> Deps<'_> {
|
||||
self.deps.as_ref()
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Config {
|
||||
CONTRACT.config.load(self.deps().storage).unwrap()
|
||||
}
|
||||
|
||||
pub fn with_deposit_amount(mut self, amount: u128) -> Self {
|
||||
CONTRACT
|
||||
.update_deposit_value(self.execute_ctx(self.admin()), coin(amount, DENOM))
|
||||
.unwrap();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ Our `prebuild` script relies on the following:
|
||||
- [`tabulate`](https://pypi.org/project/tabulate/)
|
||||
- `jq`
|
||||
|
||||
Otherwise make sure to have `node` installed.
|
||||
Otherwise make sure to have `node` and `rust` installed.
|
||||
|
||||
### Link checking (optional)
|
||||
We use [lychee](https://github.com/lycheeverse/lychee) to check for broken links. Install via your package manager or `cargo install lychee`, then run:
|
||||
@@ -89,6 +89,13 @@ NEXT_PUBLIC_SITE_URL=https://nym.com/docs
|
||||
| HowTo | Step-by-step install/setup guides |
|
||||
| FAQPage | Question-answer pages |
|
||||
|
||||
## LLM-readability
|
||||
Two files are generated in the deployment workflow: `llms.txt` and `llms-full.txt`. These files follow [Cloudflare's approach](https://developers.cloudflare.com/style-guide/how-we-docs/ai-consumability/) to generation and use.
|
||||
|
||||
When running locally can you find these at `http://localhost:3000/docs/llms.txt` and `http://localhost:3000/docs/llms-full.txt`.
|
||||
|
||||
When deployed to production, these can be found at [https://nym.com/docs/llms.txt](https://nym.com/docs/llms.txt) and [https://nym.com/docs/llms-full.txt](https://nym.com/docs/llms-full.txt).
|
||||
|
||||
## Licensing and copyright information
|
||||
This is a monorepo and components that make up Nym as a system are licensed individually, so for accurate information, please check individual files.
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
const COMMIT_SHORT = "97068b2";
|
||||
const COMMIT_FULL = "97068b2aa";
|
||||
const EXAMPLES_URL =
|
||||
"https://github.com/nymtech/nym/tree/develop/sdk/rust/nym-sdk/examples";
|
||||
|
||||
export const CodeVerified = () => (
|
||||
<Callout type="info">
|
||||
Code verified against commit{" "}
|
||||
<a
|
||||
href={`https://github.com/nymtech/nym/commit/${COMMIT_FULL}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<code>{COMMIT_SHORT}</code>
|
||||
</a>
|
||||
. If the API has changed since then, check the{" "}
|
||||
<a href={EXAMPLES_URL} target="_blank" rel="noopener noreferrer">
|
||||
examples in the repo
|
||||
</a>{" "}
|
||||
for the latest usage.
|
||||
</Callout>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
const CRATES_VERSION = "1.20.4";
|
||||
const INSTALL_PATH = "/developers/rust/importing";
|
||||
|
||||
export const CratesPaused = () => (
|
||||
<Callout type="warning">
|
||||
<strong>Crate publication is paused.</strong> The crates.io release (v
|
||||
{CRATES_VERSION}) doesn't include the Stream module or other recent work.
|
||||
Publication resumes with the Lewes Protocol. Import from Git for now — see{" "}
|
||||
<a href={INSTALL_PATH}>Installation</a>.
|
||||
</Callout>
|
||||
);
|
||||
@@ -1,120 +1,507 @@
|
||||
import React from "react";
|
||||
import { Box, Grid, Typography } from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
import Image from "next/image";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import networkDocs from "../public/images/landing/Vector1.png";
|
||||
import developerDocs from "../public/images/landing/Vector2.png";
|
||||
import sdkDocs from "../public/images/landing/Vector3.png";
|
||||
import operatorGuide from "../public/images/landing/Vector4.png";
|
||||
export const LandingPage = () => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up("xl"));
|
||||
const asciiStyle: React.CSSProperties = {
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "0.72rem",
|
||||
lineHeight: 1.4,
|
||||
color: "var(--colorPrimary)",
|
||||
opacity: 0.7,
|
||||
whiteSpace: "pre",
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const squares = [
|
||||
{
|
||||
text: "Network Docs",
|
||||
description: "Architecture, crypto systems, and how the Mixnet works",
|
||||
href: "/network",
|
||||
icon: developerDocs,
|
||||
},
|
||||
{
|
||||
text: "Operator Guides",
|
||||
description:
|
||||
"Guides and maintenance: if you want to run a node, start here",
|
||||
// ── Animation components ──
|
||||
|
||||
href: "/operators/introduction",
|
||||
icon: operatorGuide,
|
||||
},
|
||||
{
|
||||
text: "Developer Portal",
|
||||
description: "Conceptual overview, clients, tools and SDKs",
|
||||
const randomRow = () => Math.floor(Math.random() * 3);
|
||||
const randomPath = () => [randomRow(), randomRow(), randomRow()];
|
||||
|
||||
href: "/developers",
|
||||
icon: sdkDocs,
|
||||
},
|
||||
{
|
||||
text: "APIs",
|
||||
description: "Interactive API specs for Nym infrastructure",
|
||||
const NetworkAnimation = () => {
|
||||
// Packets traverse 5 stages: gw_e(0) → M1(1) → M2(2) → M3(3) → gw_ex(4)
|
||||
// stage -1 = not yet mounted (SSR-safe, renders all ○)
|
||||
const [packets, setPackets] = useState([
|
||||
{ path: randomPath(), stage: -1 },
|
||||
{ path: randomPath(), stage: -1 },
|
||||
]);
|
||||
useEffect(() => {
|
||||
// kick off with staggered positions
|
||||
setPackets([
|
||||
{ path: randomPath(), stage: 0 },
|
||||
{ path: randomPath(), stage: 3 },
|
||||
]);
|
||||
|
||||
href: "/apis/introduction",
|
||||
icon: networkDocs,
|
||||
},
|
||||
];
|
||||
const id = setInterval(() => {
|
||||
setPackets((prev) =>
|
||||
prev.map((p) => {
|
||||
const next = (p.stage + 1) % 5;
|
||||
return { stage: next, path: next === 0 ? randomPath() : p.path };
|
||||
})
|
||||
);
|
||||
}, 300);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const shortenDescription = (description: string) => {
|
||||
return description.slice(0, 18) + "...";
|
||||
const gwNode = (stage: number) => {
|
||||
const active = packets.some((p) => p.stage === stage);
|
||||
return (
|
||||
<span
|
||||
style={
|
||||
active ? { color: "var(--colorPrimary)", opacity: 1 } : undefined
|
||||
}
|
||||
>
|
||||
{active ? "\u25CF" : "\u25CB"}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const mixNode = (col: number, row: number) => {
|
||||
const active = packets.some(
|
||||
(p) => p.stage === col + 1 && p.path[col] === row
|
||||
);
|
||||
const filled = active;
|
||||
return (
|
||||
<span
|
||||
style={
|
||||
active ? { color: "var(--colorPrimary)", opacity: 1 } : undefined
|
||||
}
|
||||
>
|
||||
{filled ? "\u25CF" : "\u25CB"}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box margin={"0 auto"} textAlign="center">
|
||||
{/*<Typography variant="h2" mb={6}>
|
||||
Nym Docs
|
||||
</Typography>
|
||||
|
||||
<Typography mb={10}>
|
||||
Nym is a privacy platform. It provides strong network-level privacy
|
||||
against sophisticated end-to-end attackers, and anonymous access control
|
||||
using blinded, re-randomizable, decentralized credentials. Our goal is
|
||||
to allow developers to build new applications, or upgrade existing apps,
|
||||
with privacy features unavailable in other systems.
|
||||
</Typography>*/}
|
||||
<Grid container border={"1px solid #2E3538"}>
|
||||
{squares.map((square, index) => (
|
||||
<Grid
|
||||
item
|
||||
key={index}
|
||||
xs={12}
|
||||
sm={6}
|
||||
padding={{ xs: 3, xl: 4 }}
|
||||
sx={{
|
||||
borderBottom: {
|
||||
xs: index < 3 ? "1px solid #2E3538" : "none",
|
||||
sm: index === 0 || index === 1 ? "1px solid #2E3538" : "none",
|
||||
},
|
||||
borderRight: {
|
||||
xs: "none",
|
||||
sm: index === 0 || index === 2 ? "1px solid #2E3538" : "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Link href={square.href}>
|
||||
<Box
|
||||
display={"flex"}
|
||||
gap={{ xs: 3, xl: 4 }}
|
||||
height={"100%"}
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
||||
{square.text}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
textAlign="center"
|
||||
sx={{
|
||||
color: "#909195",
|
||||
}}
|
||||
>
|
||||
{square.description}
|
||||
</Typography>
|
||||
|
||||
<Image
|
||||
src={square.icon}
|
||||
alt={square.text}
|
||||
width={isDesktop ? 180 : isTablet ? 140 : 180}
|
||||
height={isDesktop ? 134 : isTablet ? 90 : 134}
|
||||
/>
|
||||
</Box>
|
||||
</Link>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
<pre style={{ ...asciiStyle, marginTop: "1.2rem" }}>
|
||||
{"gw_e M1 M2 M3 gw_ex\n"}
|
||||
{" "}
|
||||
{mixNode(0, 0)}
|
||||
{" \u2500\u2500 "}
|
||||
{mixNode(1, 0)}
|
||||
{" \u2500\u2500 "}
|
||||
{mixNode(2, 0)}
|
||||
{"\n"}
|
||||
{" \\ / \\ /\n"}
|
||||
{" "}
|
||||
{gwNode(0)}
|
||||
{" \u2500\u2500 "}
|
||||
{mixNode(0, 1)}
|
||||
{" \u2500\u2500 "}
|
||||
{mixNode(1, 1)}
|
||||
{" \u2500\u2500 "}
|
||||
{mixNode(2, 1)}
|
||||
{" \u2500\u2500 "}
|
||||
{gwNode(4)}
|
||||
{"\n"}
|
||||
{" / \\ / \\\n"}
|
||||
{" "}
|
||||
{mixNode(0, 2)}
|
||||
{" \u2500\u2500 "}
|
||||
{mixNode(1, 2)}
|
||||
{" \u2500\u2500 "}
|
||||
{mixNode(2, 2)}
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
const TypewriterAnimation = () => {
|
||||
const text =
|
||||
"let client = MixnetClient::connect_new().await?;\n" +
|
||||
"\n" +
|
||||
"client.send(msg).await;";
|
||||
const [charCount, setCharCount] = useState(0);
|
||||
const [showCursor, setShowCursor] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const run = () => {
|
||||
setCharCount(0);
|
||||
let i = 0;
|
||||
const type = () => {
|
||||
if (cancelled) return;
|
||||
if (i <= text.length) {
|
||||
setCharCount(i);
|
||||
i++;
|
||||
setTimeout(type, 40);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (!cancelled) run();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
type();
|
||||
};
|
||||
run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setShowCursor((v) => !v), 530);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<pre style={{ ...asciiStyle, marginTop: "1.2rem" }}>
|
||||
{text.slice(0, charCount)}
|
||||
<span style={{ opacity: 0.6 }}>{showCursor ? "\u258C" : " "}</span>
|
||||
<span style={{ opacity: 0 }}>{text.slice(charCount)}</span>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
const OperatorsAnimation = () => {
|
||||
const totalBars = 10;
|
||||
const [tick, setTick] = useState(0);
|
||||
const mixRef = useRef(0);
|
||||
const [mixCount, setMixCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setTick((t) => t + 1);
|
||||
mixRef.current += Math.floor(Math.random() * 8) + 5;
|
||||
setMixCount(mixRef.current);
|
||||
}, 80);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const mixFilled = Math.min(tick % 12, totalBars);
|
||||
const bar = (f: number) =>
|
||||
"\u25A0".repeat(f) + "\u25A1".repeat(totalBars - f);
|
||||
const fmt = (n: number) => n.toLocaleString("en");
|
||||
|
||||
return (
|
||||
<pre style={{ ...asciiStyle, marginTop: "1.2rem" }}>
|
||||
{"> nym-node run\n\n"}
|
||||
{" mixing: "}
|
||||
{bar(mixFilled)}
|
||||
{" "}
|
||||
{fmt(mixCount)}
|
||||
{" pkts"}
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
const ApiAnimation = () => {
|
||||
const lines = [
|
||||
"GET /v1/mixnodes/active",
|
||||
"",
|
||||
'{ "count": 498,',
|
||||
' "nodes": [ ... ] }',
|
||||
];
|
||||
const [visibleLines, setVisibleLines] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const run = () => {
|
||||
setVisibleLines(0);
|
||||
setTimeout(() => {
|
||||
if (cancelled) return;
|
||||
setVisibleLines(1);
|
||||
setTimeout(() => {
|
||||
if (cancelled) return;
|
||||
let i = 2;
|
||||
const reveal = () => {
|
||||
if (cancelled) return;
|
||||
if (i <= lines.length) {
|
||||
setVisibleLines(i);
|
||||
i++;
|
||||
setTimeout(reveal, 300);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (!cancelled) run();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
reveal();
|
||||
}, 800);
|
||||
}, 100);
|
||||
};
|
||||
run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<pre style={{ ...asciiStyle, marginTop: "1.2rem" }}>
|
||||
{lines.slice(0, visibleLines).map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && "\n"}
|
||||
{line}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span style={{ opacity: 0 }}>
|
||||
{lines.slice(visibleLines).map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{visibleLines > 0 || i > 0 ? "\n" : ""}
|
||||
{line}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</span>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Section data ──
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: "Network",
|
||||
description:
|
||||
"Architecture, cryptographic systems, and how the Mixnet protects your traffic.",
|
||||
href: "/network",
|
||||
animation: "network" as const,
|
||||
},
|
||||
{
|
||||
title: "Developers",
|
||||
description: "SDKs, tutorials, and integration guides for building on Nym.",
|
||||
href: "/developers",
|
||||
animation: "typewriter" as const,
|
||||
},
|
||||
{
|
||||
title: "Operators",
|
||||
description:
|
||||
"Set up and maintain mix nodes, gateways, and network infrastructure.",
|
||||
href: "/operators/introduction",
|
||||
animation: "progress" as const,
|
||||
},
|
||||
{
|
||||
title: "APIs",
|
||||
description: "Interactive specs for querying Nym infrastructure.",
|
||||
href: "/apis/introduction",
|
||||
animation: "api" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const AnimationBlock = ({ type }: { type: string }) => {
|
||||
switch (type) {
|
||||
case "network":
|
||||
return <NetworkAnimation />;
|
||||
case "typewriter":
|
||||
return <TypewriterAnimation />;
|
||||
case "progress":
|
||||
return <OperatorsAnimation />;
|
||||
case "api":
|
||||
return <ApiAnimation />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const sdks = [
|
||||
{
|
||||
name: "Rust",
|
||||
description:
|
||||
"Native SDK with async Mixnet client, streams, and TcpProxy modules.",
|
||||
href: "/developers/rust",
|
||||
},
|
||||
{
|
||||
name: "TypeScript",
|
||||
description:
|
||||
"Browser-based SDK with fetch API replacement and message-based WebSocket transport.",
|
||||
href: "/developers/typescript",
|
||||
},
|
||||
];
|
||||
|
||||
export const LandingPage = () => {
|
||||
return (
|
||||
<div
|
||||
style={{ maxWidth: "64rem", margin: "0 auto", padding: "3rem 1.5rem" }}
|
||||
>
|
||||
{/* ── Section cards ── */}
|
||||
<div
|
||||
className="landing-grid"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
border: "1px solid var(--border)",
|
||||
marginBottom: "3.5rem",
|
||||
}}
|
||||
>
|
||||
{sections.map((s, i) => (
|
||||
<Link
|
||||
key={i}
|
||||
href={s.href}
|
||||
style={{ textDecoration: "none", display: "flex" }}
|
||||
>
|
||||
<div
|
||||
data-index={i}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
borderBottom: i < 2 ? "1px solid var(--border)" : undefined,
|
||||
borderRight:
|
||||
i % 2 === 0 ? "1px solid var(--border)" : undefined,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
transition: "background-color 0.15s",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className="landing-card"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
marginBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
className="landing-heading"
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: 600,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
{s.title}
|
||||
</h2>
|
||||
<span
|
||||
style={{ color: "var(--textMuted)", fontSize: "0.9rem" }}
|
||||
>
|
||||
›
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.88rem",
|
||||
color: "var(--textMuted)",
|
||||
lineHeight: 1.6,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{s.description}
|
||||
</p>
|
||||
</div>
|
||||
<AnimationBlock type={s.animation} />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── SDKs ── */}
|
||||
<div
|
||||
className="landing-sdk-grid"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: "0",
|
||||
marginBottom: "3.5rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ paddingRight: "2rem" }}>
|
||||
<h2
|
||||
className="landing-heading"
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "1.35rem",
|
||||
fontWeight: 600,
|
||||
marginBottom: "0.5rem",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
SDKs
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.88rem",
|
||||
color: "var(--textMuted)",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Integrate Mixnet privacy into your application with our Rust and
|
||||
TypeScript SDKs.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0" }}>
|
||||
{sdks.map((sdk, i) => (
|
||||
<Link key={i} href={sdk.href} style={{ textDecoration: "none" }}>
|
||||
<div
|
||||
className="landing-card"
|
||||
style={{
|
||||
padding: "1rem 1.2rem",
|
||||
border: "1px solid var(--border)",
|
||||
marginTop: i > 0 ? "-1px" : undefined,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
transition: "background-color 0.15s",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
className="landing-heading"
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{sdk.name}
|
||||
</span>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.8rem",
|
||||
color: "var(--textMuted)",
|
||||
margin: "0.25rem 0 0 0",
|
||||
}}
|
||||
>
|
||||
{sdk.description}
|
||||
</p>
|
||||
</div>
|
||||
<span style={{ color: "var(--textMuted)", fontSize: "1rem" }}>
|
||||
›
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Links ── */}
|
||||
<div
|
||||
style={{
|
||||
borderTop: "1px solid var(--border)",
|
||||
paddingTop: "1.5rem",
|
||||
display: "flex",
|
||||
gap: "2rem",
|
||||
fontSize: "0.82rem",
|
||||
fontFamily: "var(--font-mono)",
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="https://github.com/nymtech"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "var(--textMuted)", textDecoration: "none" }}
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://matrix.to/#/#operators:nymtech.chat"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "var(--textMuted)", textDecoration: "none" }}
|
||||
>
|
||||
Matrix
|
||||
</a>
|
||||
<a
|
||||
href="https://nym.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "var(--textMuted)", textDecoration: "none" }}
|
||||
>
|
||||
nym.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Open the needed ports for `nym-node` by running these commands:
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
```sh
|
||||
ufw allow 22/tcp # SSH - you're in control of these ports
|
||||
@@ -7,8 +7,16 @@ ufw allow 443/tcp # HTTPS
|
||||
ufw allow 1789/tcp # Nym specific - Mixnet
|
||||
ufw allow 1790/tcp # Nym specific - Verloc
|
||||
ufw allow 8080/tcp # Nym specific - nym-node-api
|
||||
ufw allow 9000/tcp # Nym Specific - clients port
|
||||
ufw allow 9000/tcp # Nym specific - clients port
|
||||
ufw allow 9001/tcp # Nym specific - wss port
|
||||
ufw allow 51822/udp # WireGuard
|
||||
ufw allow in on nymwg to any port 51830 proto tcp # bandwidth queries/topup - inside the tunnel
|
||||
ufw allow 41264/tcp # Nym specific - Lewes Protocol registration
|
||||
ufw allow 51264/udp # Nym specific - Lewes Protocol data
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Note that these ports were moved to [NTM](/operators/nodes/preliminary-steps/vps-setup#1-download-the-new-ntm) as they are for Gateways only
|
||||
```sh
|
||||
# ufw allow 51822/udp # WireGuard
|
||||
# ufw allow in on nymwg to any port 51830 proto tcp # bandwidth queries/topup - inside the tunnel
|
||||
```
|
||||
</Callout>
|
||||
@@ -0,0 +1,79 @@
|
||||
import { MyTab } from 'components/generic-tabs.tsx';
|
||||
import { Tabs, Steps } from 'nextra/components';
|
||||
import PortsNymNode from 'components/operators/snippets/ports-nym-node.mdx';
|
||||
|
||||
<div>
|
||||
<Tabs
|
||||
items={['Mixnode', 'Gateways and WireGuard']}
|
||||
defaultIndex={0}
|
||||
>
|
||||
<MyTab>
|
||||
For operators considering to eventually run `nym-node` in a Gateway mode or supporting WireGuard routing, use the tab for Gateways & WireGuard.
|
||||
|
||||
For mixnode only open the needed ports for Nym services using Uncomplicated Firewall (`ufw`) by running these commands:
|
||||
|
||||
<Steps>
|
||||
|
||||
###### 1. Ensure `ufw` functionality
|
||||
|
||||
- Check if you have `ufw` installed:
|
||||
```sh
|
||||
ufw version
|
||||
```
|
||||
|
||||
- If it's not installed, install with:
|
||||
```sh
|
||||
apt install ufw -y
|
||||
```
|
||||
|
||||
- Enable ufw
|
||||
```sh
|
||||
ufw enable
|
||||
```
|
||||
|
||||
- Check the status of the firewall
|
||||
```sh
|
||||
ufw status
|
||||
```
|
||||
|
||||
###### 2. Open required ports for Nym services using `ufw`
|
||||
|
||||
- Copy this whole block to open all ports for mixnode
|
||||
|
||||
<PortsNymNode />
|
||||
|
||||
###### 3. Validate opened ports
|
||||
- Re-check the status of the firewall
|
||||
```sh
|
||||
ufw status
|
||||
```
|
||||
|
||||
</Steps>
|
||||
</MyTab>
|
||||
|
||||
<MyTab>
|
||||
Gateways (and WireGuard) nodes use [`network-tunnel-manager.sh` (NTM)](https://github.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh) tool to open ports for Nym related services, setup and configure Nym networking devices, configure Nym exit policy and run tests. For more info, read the chapter [*Routing configuration*](/operators/nodes/nym-node/configuration#routing-configuration).
|
||||
|
||||
Follow these steps to setup complete routing of your server hosting a Nym node.
|
||||
|
||||
<Steps>
|
||||
|
||||
###### 1. Download the new NTM
|
||||
|
||||
- Download the latest NTM and make it executable:
|
||||
```sh
|
||||
curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o ./network-tunnel-manager.sh && \
|
||||
chmod +x network-tunnel-manager.sh
|
||||
```
|
||||
|
||||
###### 2. Update the exit policy
|
||||
|
||||
- To ensure your routing is clean, run:
|
||||
```sh
|
||||
./network-tunnel-manager.sh complete_networking_configuration
|
||||
```
|
||||
|
||||
</Steps>
|
||||
</MyTab>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"mixmining_reserve": {
|
||||
"denom": "unym",
|
||||
"amount": "170550581010206"
|
||||
"amount": "168575020719057"
|
||||
},
|
||||
"vesting_tokens": {
|
||||
"denom": "unym",
|
||||
@@ -13,6 +13,6 @@
|
||||
},
|
||||
"circulating_supply": {
|
||||
"denom": "unym",
|
||||
"amount": "829449418989794"
|
||||
"amount": "831424979280943"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"nodes": 752,
|
||||
"locations": 76,
|
||||
"mixnodes": 273,
|
||||
"exit_gateways": 470
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
829_449_418
|
||||
831_424_979
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4_737
|
||||
4_682
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.90%
|
||||
0.95%
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
31.932
|
||||
30.134
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
253_773
|
||||
254_377
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
60_905_575
|
||||
61_050_638
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
60_905_574
|
||||
61_050_637
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
| **Item** | **Description** | **Amount in NYM** |
|
||||
|:-------------------|:------------------------------------------------------|--------------------:|
|
||||
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 170_550_581 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 168_575_020 |
|
||||
| Vesting Tokens | Tokens locked outside of circulation for future claim | 0 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 829_449_418 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 253_773 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 831_424_979 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 254_377 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"interval": {
|
||||
"reward_pool": "170550581010206.362081368363611499",
|
||||
"staking_supply": "60905574069554.404272283888050139",
|
||||
"reward_pool": "168575020719057.456153922699547282",
|
||||
"staking_supply": "61050637328128.353993717821521057",
|
||||
"staking_supply_scale_factor": "0.07342892",
|
||||
"epoch_reward_budget": "4737516139.172398946704676766",
|
||||
"stake_saturation_point": "253773225289.810017801182866875",
|
||||
"epoch_reward_budget": "4682639464.418262670942297209",
|
||||
"stake_saturation_point": "254377655533.868141640490923004",
|
||||
"sybil_resistance": "0.3",
|
||||
"active_set_work_factor": "10",
|
||||
"interval_pool_emission": "0.02"
|
||||
|
||||
@@ -1 +1 @@
|
||||
Thursday, March 12th 2026, 13:23:46 UTC
|
||||
Thursday, April 9th 2026, 15:33:24 UTC
|
||||
|
||||
@@ -8,12 +8,8 @@ Commands:
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-c, --config-env-file <CONFIG_ENV_FILE>
|
||||
Path pointing to an env file that configures the Nym API [env: NYMAPI_CONFIG_ENV_FILE_ARG=]
|
||||
--no-banner
|
||||
A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops) [env: NYMAPI_NO_BANNER_ARG=]
|
||||
-h, --help
|
||||
Print help
|
||||
-V, --version
|
||||
Print version
|
||||
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the Nym API [env: NYMAPI_CONFIG_ENV_FILE_ARG=]
|
||||
--no-banner A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops) [env: NYMAPI_NO_BANNER_ARG=]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
@@ -12,12 +12,8 @@ Commands:
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-c, --config-env-file <CONFIG_ENV_FILE>
|
||||
Path pointing to an env file that configures the nym-node and overrides any preconfigured values [env: NYMNODE_CONFIG_ENV_FILE_ARG=]
|
||||
--no-banner
|
||||
Flag used for disabling the printed banner in tty [env: NYMNODE_NO_BANNER=]
|
||||
-h, --help
|
||||
Print help
|
||||
-V, --version
|
||||
Print version
|
||||
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the nym-node and overrides any preconfigured values [env: NYMNODE_CONFIG_ENV_FILE_ARG=]
|
||||
--no-banner Flag used for disabling the printed banner in tty [env: NYMNODE_NO_BANNER=]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
@@ -4,98 +4,56 @@ Start this nym-node
|
||||
Usage: nym-node run [OPTIONS]
|
||||
|
||||
Options:
|
||||
--id <ID>
|
||||
Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
|
||||
--config-file <CONFIG_FILE>
|
||||
Path to a configuration file of this node [env: NYMNODE_CONFIG=]
|
||||
--accept-operator-terms-and-conditions
|
||||
Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
|
||||
--deny-init
|
||||
Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
|
||||
--init-only
|
||||
If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
|
||||
--local
|
||||
Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode [<MODE>...]
|
||||
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
--modes <MODES>
|
||||
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes
|
||||
If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT>
|
||||
Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument should be treated only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
|
||||
-o, --output <OUTPUT>
|
||||
Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
|
||||
--public-ips <PUBLIC_IPS>
|
||||
Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical to the address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
|
||||
--hostname <HOSTNAME>
|
||||
Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
|
||||
--location <LOCATION>
|
||||
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH>
|
||||
Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN>
|
||||
An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
|
||||
--expose-system-info <EXPOSE_SYSTEM_INFO>
|
||||
Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
|
||||
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE>
|
||||
Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE>
|
||||
Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS>
|
||||
Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS>
|
||||
Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--nyxd-urls <NYXD_URLS>
|
||||
Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING>
|
||||
Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED>
|
||||
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
|
||||
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
|
||||
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE>
|
||||
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT>
|
||||
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT>
|
||||
If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
|
||||
--enforce-zk-nyms <ENFORCE_ZK_NYMS>
|
||||
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
|
||||
--mnemonic <MNEMONIC>
|
||||
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
|
||||
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL>
|
||||
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
|
||||
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
|
||||
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY>
|
||||
Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false]
|
||||
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS>
|
||||
Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
|
||||
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT>
|
||||
Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
|
||||
--lp-data-bind-address <LP_DATA_BIND_ADDRESS>
|
||||
Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
|
||||
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT>
|
||||
Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH>
|
||||
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
|
||||
-h, --help
|
||||
Print help
|
||||
--id <ID> Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
|
||||
--config-file <CONFIG_FILE> Path to a configuration file of this node [env: NYMNODE_CONFIG=]
|
||||
--accept-operator-terms-and-conditions Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
|
||||
--deny-init Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
|
||||
--init-only If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
|
||||
--local Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode [<MODE>...] Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
--modes <MODES> Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT> Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument should be treated only as a preview of
|
||||
future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
|
||||
-o, --output <OUTPUT> Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
|
||||
--public-ips <PUBLIC_IPS> Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical to the address you're going to use for
|
||||
bonding [env: NYMNODE_PUBLIC_IPS=]
|
||||
--hostname <HOSTNAME> Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
|
||||
--location <LOCATION> Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be
|
||||
provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS> Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH> Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN> An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
|
||||
--expose-system-info <EXPOSE_SYSTEM_INFO> Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
|
||||
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE> Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE> Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS> Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS> Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--nyxd-urls <NYXD_URLS> Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING> Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED> Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS> Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT> Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX> The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE> Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true,
|
||||
false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS> Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS> Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT> Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT> If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
|
||||
--enforce-zk-nyms <ENFORCE_ZK_NYMS> Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
|
||||
--mnemonic <MNEMONIC> Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
|
||||
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL> Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY> Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL> Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY> Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false]
|
||||
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS> Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
|
||||
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT> Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
|
||||
--lp-data-bind-address <LP_DATA_BIND_ADDRESS> Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
|
||||
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT> Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH> Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without blockchain verification [env:
|
||||
NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
@@ -11,10 +11,7 @@ Commands:
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-c, --config-env-file <CONFIG_ENV_FILE>
|
||||
Path pointing to an env file that configures the nymvisor and overrides any preconfigured values
|
||||
-h, --help
|
||||
Print help
|
||||
-V, --version
|
||||
Print version
|
||||
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the nymvisor and overrides any preconfigured values
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
@@ -64,19 +64,19 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/docs/architecture/nym-vs-others.html",
|
||||
destination: "/docs/network/architecture/nym-vs-others",
|
||||
destination: "/docs/network/overview/comparisons",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/architecture/traffic-flow.html",
|
||||
destination: "/docs/network/traffic", // testing difference
|
||||
destination: "/docs/network/mixnet-mode/traffic-flow",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/architecture/addressing-system.html",
|
||||
destination: "/docs/network/traffic/addressing-system",
|
||||
destination: "/docs/network/reference/addressing",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -100,7 +100,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/docs/nodes/overview.html ",
|
||||
destination: "/docs/network/architecture/mixnet#nym-nodes",
|
||||
destination: "/docs/network/infrastructure/nym-nodes",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -132,19 +132,19 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/docs/nyx/smart-contracts.html",
|
||||
destination: "/docs/network/architecture/nyx#smart-contracts",
|
||||
destination: "/docs/network/infrastructure/nyx#smart-contracts",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/nyx/mixnet-contract.html",
|
||||
destination: "/docs/network/architecture/nyx#mixnet-contract",
|
||||
destination: "/docs/network/infrastructure/nyx#mixnet-contract",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/nyx/vesting-contract.html",
|
||||
destination: "/docs/network/architecture/nyx#vesting-contract",
|
||||
destination: "/docs/network/infrastructure/nyx#vesting-contract",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -616,7 +616,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/docs/architecture/network-overview.html",
|
||||
destination: "/docs/network/architecture",
|
||||
destination: "/docs/network/overview",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -634,7 +634,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/docs/network/architecture/nyx/smart-contracts/ecash",
|
||||
destination: "/docs/network/architecture/nyx#zk-nym-contract",
|
||||
destination: "/docs/network/infrastructure/nyx#zk-nym-contract",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -1073,6 +1073,236 @@ const config = {
|
||||
// destination: "https://www.<TODO_EDIT_DESTINATION_BASE>/developers/typescript/FAQ",
|
||||
// permanent: true,
|
||||
// },
|
||||
|
||||
// ==========================================
|
||||
// Docs rework redirects (2026)
|
||||
// ==========================================
|
||||
|
||||
// --- Network overview: deleted pages ---
|
||||
{
|
||||
source: "/docs/network/overview/two-modes",
|
||||
destination: "/docs/network/overview/choosing-a-mode",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/network/overview/network-components",
|
||||
destination: "/docs/network/infrastructure",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// --- TypeScript SDK: merged pages ---
|
||||
{
|
||||
source: "/docs/developers/typescript/overview",
|
||||
destination: "/docs/developers/typescript",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/installation",
|
||||
destination: "/docs/developers/typescript",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/start",
|
||||
destination: "/docs/developers/typescript",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/FAQ",
|
||||
destination: "/docs/developers/typescript/bundling/bundling",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// --- Rust SDK: deleted mixnet example subpages ---
|
||||
{
|
||||
source: "/docs/developers/rust/mixnet/examples/:path+",
|
||||
destination: "/docs/developers/rust/mixnet/examples",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/rust/mixnet/message-helpers",
|
||||
destination: "/docs/developers/rust/mixnet",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/rust/mixnet/message-types",
|
||||
destination: "/docs/developers/rust/mixnet",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// --- Rust SDK: deleted client-pool subpages ---
|
||||
{
|
||||
source: "/docs/developers/rust/client-pool/architecture",
|
||||
destination: "/docs/developers/rust/client-pool",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/rust/client-pool/example",
|
||||
destination: "/docs/developers/rust/client-pool",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// --- Rust SDK: collapsed TcpProxy subpages ---
|
||||
{
|
||||
source: "/docs/developers/rust/tcpproxy/troubleshooting",
|
||||
destination: "/docs/developers/rust/tcpproxy",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/rust/tcpproxy/tutorial",
|
||||
destination: "/docs/developers/rust/tcpproxy",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/rust/tcpproxy/architecture",
|
||||
destination: "/docs/developers/rust/tcpproxy",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/rust/tcpproxy/examples",
|
||||
destination: "/docs/developers/rust/tcpproxy",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// --- Directory index redirects (directories without index pages) ---
|
||||
{
|
||||
source: "/docs/developers/typescript/bundling",
|
||||
destination: "/docs/developers/typescript/bundling/bundling",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/examples",
|
||||
destination: "/docs/developers/typescript/examples/mix-fetch",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/playground",
|
||||
destination: "/docs/developers/typescript/playground/mixfetch",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/api",
|
||||
destination: "/docs/developers/typescript/api/sdk",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/clients",
|
||||
destination: "/docs/developers/clients/socks5",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/developers/rust/importing",
|
||||
destination: "/docs/developers/rust/importing",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// --- APIs: flattened structure ---
|
||||
{
|
||||
source: "/docs/apis/ns-api/mainnet",
|
||||
destination: "/docs/apis/ns-api",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/apis/ns-api/sandbox",
|
||||
destination: "/docs/apis/ns-api",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/apis/ns-api/ns-api-run-deploy",
|
||||
destination: "/docs/operators/performance-and-testing/ns-api-deployment",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/apis/nym-api/mainnet",
|
||||
destination: "/docs/apis/nym-api",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/apis/explorer-api/mainnet",
|
||||
destination: "/docs/apis/explorer-api",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/apis/explorer-api/sandbox",
|
||||
destination: "/docs/apis/explorer-api",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/apis/cosmos-sdk-nyx/mainnet",
|
||||
destination: "/docs/apis/cosmos-sdk-nyx",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/apis/cosmos-sdk-nyx/sandbox",
|
||||
destination: "/docs/apis/cosmos-sdk-nyx",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// --- Network: archived sections ---
|
||||
{
|
||||
source: "/docs/network/architecture",
|
||||
destination: "/docs/network/overview",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/network/architecture/:path*",
|
||||
destination: "/docs/network/overview",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/network/concepts",
|
||||
destination: "/docs/network/overview",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/network/concepts/:path*",
|
||||
destination: "/docs/network/overview",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/network/traffic",
|
||||
destination: "/docs/network/overview",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/network/traffic/:path*",
|
||||
destination: "/docs/network/overview",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
images: {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"generate:commands": "../scripts/next-scripts/autodoc.sh",
|
||||
"generate:tables": "../scripts/next-scripts/python-prebuild.sh",
|
||||
"predev": "../scripts/next-scripts/python-prebuild.sh",
|
||||
"build": "next build && next-sitemap",
|
||||
"generate:llms": "node ../scripts/next-scripts/generate-llms-txt.mjs",
|
||||
"build": "node ../scripts/next-scripts/generate-llms-txt.mjs && next build && next-sitemap",
|
||||
"dev": " next dev",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
@@ -38,7 +39,7 @@
|
||||
"@nextui-org/accordion": "^2.0.40",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
"@nymproject/contract-clients": ">=1.2.4-rc.2 || ^1",
|
||||
"@nymproject/mix-fetch-full-fat": "^1.4.2",
|
||||
"@nymproject/mix-fetch-full-fat": "^1.4.3",
|
||||
"@nymproject/sdk-full-fat": ">=1.5.1-rc.0 || ^1.4.1",
|
||||
"@redocly/cli": "^1.25.15",
|
||||
"@types/mdx": "^2.0.13",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import type { AppProps } from 'next/app';
|
||||
import './styles.css';
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
@@ -20,6 +20,17 @@ const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
}),
|
||||
[],
|
||||
);
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
const img = e.target as HTMLElement;
|
||||
if (img.tagName === 'IMG' && img.closest('.nextra-content')) {
|
||||
img.classList.toggle('img-expanded');
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handler);
|
||||
return () => document.removeEventListener('click', handler);
|
||||
}, []);
|
||||
|
||||
const AnyComponent = Component as any;
|
||||
return (
|
||||
<ThemeProvider theme={muiTheme}>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"introduction": "Introduction",
|
||||
"ns-api": "Node Status API",
|
||||
"nym-api": "NymAPI Validator Sidecar",
|
||||
"explorer-api": "Explorer API",
|
||||
"nym-api": "NymAPI",
|
||||
"cosmos-sdk-nyx": "Validator REST API",
|
||||
"explorer-api": "Explorer API (Deprecated)",
|
||||
"---": {
|
||||
"type": "separator"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,57 @@
|
||||
---
|
||||
title: "Nyx Validator REST API - Cosmos SDK Endpoints"
|
||||
description: "Reference for the Nyx Validator REST API, providing Cosmos SDK endpoints for account balances, staking, governance, and CosmWasm smart contract queries."
|
||||
schemaType: "TechArticle"
|
||||
section: "APIs"
|
||||
lastUpdated: "2026-03-15"
|
||||
---
|
||||
|
||||
import { RedocStandalone } from 'redoc'
|
||||
|
||||
# Validator REST API
|
||||
|
||||
Since the [Nyx validators](/operators/nodes/validator-setup) are built with the Cosmos SDK, they by default expose a [REST API](https://docs.cosmos.network/api) which can be used to query the state of the chain.
|
||||
Nyx Validators are built with the [Cosmos SDK](https://docs.cosmos.network/) and expose a standard [REST API](https://docs.cosmos.network/api) for querying chain state directly.
|
||||
|
||||
**Key endpoint categories:**
|
||||
- **Accounts**: balances, transaction history
|
||||
- **Staking**: validators, delegations, rewards
|
||||
- **Governance**: proposals, votes
|
||||
- **CosmWasm**: smart contract queries
|
||||
|
||||
For validator setup instructions, see the [Nyx Validator Setup Guide](/operators/nodes/validator-setup).
|
||||
|
||||
Other validator endpoints are listed at [cosmos.directory/nyx](https://cosmos.directory/nyx).
|
||||
|
||||
## Quick examples
|
||||
|
||||
**Query an account balance:**
|
||||
```bash
|
||||
curl https://api.nymtech.net/cosmos/bank/v1beta1/balances/n1...
|
||||
```
|
||||
|
||||
**List active validators:**
|
||||
```bash
|
||||
curl https://api.nymtech.net/cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED
|
||||
```
|
||||
|
||||
## Mainnet endpoints
|
||||
|
||||
- **OpenAPI spec:** [api.nymtech.net/swagger/swagger.yaml](https://api.nymtech.net/swagger/swagger.yaml)
|
||||
- **Swagger UI:** [api.nymtech.net/swagger/](https://api.nymtech.net/swagger/)
|
||||
|
||||
## Full API reference
|
||||
|
||||
<br />
|
||||
|
||||
<RedocStandalone
|
||||
specUrl="https://api.nymtech.net/swagger/swagger.yaml"
|
||||
options={{
|
||||
nativeScrollbars: true,
|
||||
theme: {
|
||||
sidebar: {
|
||||
backgroundColor: '#273239',
|
||||
textColor: '#FCFDFE'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
{
|
||||
"mainnet":"Mainnet Endpoints",
|
||||
"sandbox":"Sandbox Endpoints"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { RedocStandalone } from 'redoc';
|
||||
|
||||
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://api.nymtech.net/swagger/swagger.yaml](https://api.nymtech.net/swagger/swagger.yaml) which is also used to generate the Swagger docs deployed at [https://api.nymtech.net/swagger/](https://api.nymtech.net/swagger/).
|
||||
|
||||
There is also an overview of other Validator endpoints at [https://cosmos.directory/nyx](https://cosmos.directory/nyx).
|
||||
|
||||
<br /><br />
|
||||
|
||||
<RedocStandalone
|
||||
specUrl="https://api.nymtech.net/swagger/swagger.yaml"
|
||||
options={{
|
||||
nativeScrollbars: true,
|
||||
theme: {
|
||||
sidebar: {
|
||||
backgroundColor: '#273239',
|
||||
textColor: '#FCFDFE'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { RedocStandalone } from 'redoc';
|
||||
|
||||
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://api.sandbox.nymtech.net/swagger/swagger.yaml](https://api.sandbox.nymtech.net/swagger/swagger.yaml).
|
||||
|
||||
<br /><br />
|
||||
|
||||
<RedocStandalone
|
||||
specUrl="https://api.sandbox.nymtech.net/swagger/swagger.yaml"
|
||||
options={{
|
||||
nativeScrollbars: true,
|
||||
theme: {
|
||||
sidebar: {
|
||||
backgroundColor: '#273239',
|
||||
textColor: '#FCFDFE'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -1,8 +1,39 @@
|
||||
---
|
||||
title: "Explorer API (Deprecated)"
|
||||
description: "Legacy Explorer API reference for the Nym Mixnet Explorer. Deprecated in favor of the Node Status API."
|
||||
schemaType: "TechArticle"
|
||||
section: "APIs"
|
||||
lastUpdated: "2026-03-15"
|
||||
---
|
||||
|
||||
import { RedocStandalone } from 'redoc'
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Explorer API
|
||||
|
||||
The Explorer API is the backend for the [Mixnet Explorer](https://nym.com/explorer).
|
||||
<Callout type="warning">
|
||||
The Explorer API is deprecated. Use the [Node Status API](/apis/ns-api) instead, which provides the same data and more.
|
||||
</Callout>
|
||||
|
||||
**This will soon be deprecated in favour of the [Node Status API](ns-api.mdx).**
|
||||
The Explorer API is the legacy backend for the [Mixnet Explorer](https://nym.com/explorer).
|
||||
|
||||
## Mainnet endpoints
|
||||
|
||||
- **OpenAPI spec:** [explorer.nymtech.net/api/v1/openapi.json](https://explorer.nymtech.net/api/v1/openapi.json)
|
||||
- **Swagger UI:** [explorer.nymtech.net/api/swagger/index.html](https://explorer.nymtech.net/api/swagger/index.html)
|
||||
|
||||
<br />
|
||||
|
||||
<RedocStandalone
|
||||
specUrl="https://explorer.nymtech.net/api/v1/openapi.json"
|
||||
options={{
|
||||
nativeScrollbars: true,
|
||||
theme: {
|
||||
sidebar: {
|
||||
backgroundColor: '#273239',
|
||||
textColor: '#FCFDFE'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
The code for this service can be found [in our monorepo](https://github.com/nymtech/nym/tree/develop/explorer).
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
{
|
||||
"mainnet":"Mainnet Endpoints",
|
||||
"sandbox":"Sandbox Endpoints"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
import { RedocStandalone } from 'redoc';
|
||||
|
||||
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://explorer.nymtech.net/api/v1/openapi.json](https://explorer.nymtech.net/api/v1/openapi.json) which is also used to generate the Swagger docs deployed at [https://explorer.nymtech.net/api/swagger/index.html](https://explorer.nymtech.net/api/swagger/index.html).
|
||||
|
||||
<br /><br />
|
||||
|
||||
<RedocStandalone
|
||||
specUrl="https://sandbox-explorer.nymtech.net/api/v1/openapi.json"
|
||||
options={{
|
||||
nativeScrollbars: true,
|
||||
theme: {
|
||||
sidebar: {
|
||||
backgroundColor: '#273239',
|
||||
textColor: '#FCFDFE'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { RedocStandalone } from 'redoc';
|
||||
|
||||
The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://sandbox-explorer.nymtech.net/api/v1/openapi.json](https://sandbox-explorer.nymtech.net/api/v1/openapi.json) which is also used to generate the Swagger docs deployed at [https://sandbox-explorer.nymtech.net/api/swagger/index.html](https://sandbox-explorer.nymtech.net/api/swagger/index.html).
|
||||
|
||||
<br /><br />
|
||||
|
||||
<RedocStandalone
|
||||
specUrl="https://sandbox-explorer.nymtech.net/api/v1/openapi.json"
|
||||
options={{
|
||||
nativeScrollbars: true,
|
||||
theme: {
|
||||
sidebar: {
|
||||
backgroundColor: '#273239',
|
||||
textColor: '#FCFDFE'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -1,13 +1,58 @@
|
||||
---
|
||||
title: "Nym API Reference: Network Infrastructure"
|
||||
description: "Interactive API documentation for Nym network infrastructure. Query node status, network topology, blockchain state & mixnet performance programmatically."
|
||||
title: "Nym Network APIs Overview"
|
||||
description: "Overview of Nym HTTP APIs for querying node performance, token supply, credential data, and blockchain state, with guidance on which API to use."
|
||||
schemaType: "TechArticle"
|
||||
section: "APIs"
|
||||
lastUpdated: "2026-02-01"
|
||||
lastUpdated: "2026-03-15"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
This site contains interactive APIs generated from the OpenAPI specs of various API endpoints offered by bits of Nym infrastructure run both by Nym and community operators for both Mainnet and the Sandbox testnet.
|
||||
# APIs
|
||||
|
||||
You can find links to the generated specs for each API on their respective pages as well as their different uses for operators and developers.
|
||||
The Nym network exposes several HTTP APIs for querying network state, node performance, blockchain data, and credential information, with each API serving a different purpose.
|
||||
|
||||
## Which API should I use?
|
||||
|
||||
| I want to... | API |
|
||||
|---|---|
|
||||
| Query node performance, roles, and mixnet statistics | [Node Status API](/apis/ns-api) |
|
||||
| Query circulating NYM supply or zk-nym credential data | [NymAPI](/apis/nym-api) |
|
||||
| Query blockchain state, account balances, or transactions | [Validator REST API](/apis/cosmos-sdk-nyx) |
|
||||
| Query legacy explorer data | [Explorer API](/apis/explorer-api) *(deprecated)* |
|
||||
|
||||
## Node Status API
|
||||
|
||||
The primary API for querying node information. It serves data about individual `nym-node` instances: roles, statistics, Network Requester services, and overall mixnet summaries. If you're building an explorer, analytics dashboard, or monitoring tool, start here.
|
||||
|
||||
<Callout type="info">
|
||||
If you're building a service that makes heavy use of the Node Status API, consider [running your own instance](/operators/performance-and-testing/ns-api-deployment) to distribute load across the network.
|
||||
</Callout>
|
||||
|
||||
**Endpoints:** [OpenAPI spec](https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json) · [Swagger](https://mainnet-node-status-api.nymtech.cc/swagger/)
|
||||
|
||||
## NymAPI
|
||||
|
||||
A sidecar binary operated by Nyx Validators. It caches smart contract data and exposes endpoints for circulating NYM supply, zk-nym credential data, and ticketbook information. Use this when you need token economics or credential data.
|
||||
|
||||
**Endpoints:** [OpenAPI spec](https://validator.nymtech.net/api-docs/openapi.json) · [Swagger](https://validator.nymtech.net/api/swagger/index.html)
|
||||
|
||||
Other NymAPI instances are listed at [cosmos.directory/nyx](https://cosmos.directory/nyx).
|
||||
|
||||
## Validator REST API
|
||||
|
||||
The standard Cosmos SDK REST API exposed by Nyx Validators. Use this for direct chain queries: account balances, transaction history, governance proposals, and staking information.
|
||||
|
||||
**Endpoints:** [OpenAPI spec](https://api.nymtech.net/swagger/swagger.yaml) · [Swagger](https://api.nymtech.net/swagger/)
|
||||
|
||||
Other validator endpoints are listed at [cosmos.directory/nyx](https://cosmos.directory/nyx).
|
||||
|
||||
## Explorer API
|
||||
|
||||
<Callout type="warning">
|
||||
The Explorer API is deprecated. Use the [Node Status API](/apis/ns-api) instead.
|
||||
</Callout>
|
||||
|
||||
The legacy backend for the [Mixnet Explorer](https://nym.com/explorer). This API is being replaced by the Node Status API, which provides the same data and more.
|
||||
|
||||
**Endpoints:** [OpenAPI spec](https://explorer.nymtech.net/api/v1/openapi.json) · [Swagger](https://explorer.nymtech.net/api/swagger/index.html)
|
||||
|
||||
@@ -1,8 +1,57 @@
|
||||
---
|
||||
title: "Node Status API - Nym Node Performance and Mixnet Stats"
|
||||
description: "Reference for the Node Status API, which provides nym-node identity, performance scores, role assignments, and mixnet health summaries."
|
||||
schemaType: "TechArticle"
|
||||
section: "APIs"
|
||||
lastUpdated: "2026-03-15"
|
||||
---
|
||||
|
||||
import { RedocStandalone } from 'redoc'
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Node Status API
|
||||
The Node Status API serves information about individual `nym-nodes` in the Mixnet, such as which role they are operating in, statistics about them, services such as Network Requesters, as well as summaries of the state of the Mixnet.
|
||||
|
||||
The Node Status API serves information about individual `nym-node` instances in the Nym network: which role they are operating in, performance statistics, Network Requester services, and summaries of the overall mixnet state. It is the primary API for anyone building explorers, analytics dashboards, or monitoring tools.
|
||||
|
||||
**Key endpoint categories:**
|
||||
- **Node information**: identity, bonding status, declared roles, build version, host details
|
||||
- **Performance scores**: routing reliability, configuration scores, probe results
|
||||
- **Mixnet summaries**: active set composition, role distribution, network health
|
||||
|
||||
<Callout type="info">
|
||||
We recommend that developers building applications such as explorers or analytics interfaces about the Mixnet run their own instance of the API, in order to promote a robust network of downstream services, and spread the load of API calls amongst as many endpoints as possible.
|
||||
If you're building a service that makes heavy use of this API, consider [running your own instance](/operators/performance-and-testing/ns-api-deployment) to distribute load and promote a robust network of downstream services.
|
||||
</Callout>
|
||||
|
||||
## Quick examples
|
||||
|
||||
**Get a summary of all gateways:**
|
||||
```bash
|
||||
curl https://mainnet-node-status-api.nymtech.cc/api/v1/gateways
|
||||
```
|
||||
|
||||
**Get details for a specific node by identity key:**
|
||||
```bash
|
||||
curl https://mainnet-node-status-api.nymtech.cc/api/v1/gateways/23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb
|
||||
```
|
||||
|
||||
## Mainnet endpoints
|
||||
|
||||
- **OpenAPI spec:** [mainnet-node-status-api.nymtech.cc/api-docs/openapi.json](https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json)
|
||||
- **Swagger UI:** [mainnet-node-status-api.nymtech.cc/swagger/](https://mainnet-node-status-api.nymtech.cc/swagger/)
|
||||
|
||||
## Full API reference
|
||||
|
||||
<br />
|
||||
|
||||
<RedocStandalone
|
||||
specUrl="https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json"
|
||||
options={{
|
||||
nativeScrollbars: true,
|
||||
theme: {
|
||||
sidebar: {
|
||||
backgroundColor: '#273239',
|
||||
textColor: '#FCFDFE'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"ns-api-run-deploy":"Run Instance",
|
||||
"mainnet":"Mainnet Endpoints",
|
||||
"sandbox":"Sandbox Endpoints"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user